├── .github └── ISSUE_TEMPLATE │ └── bug-report.md ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── ascii.conf ├── ascii_art.txt ├── ascii_result.txt ├── assets ├── assets │ ├── avatar.png │ ├── dept_avatar.png │ ├── fileHelper.jpeg │ ├── g_avatar.jpeg │ ├── org_avatar.png │ └── u_10000.png └── web │ ├── css │ ├── index.css │ ├── join_group.css │ ├── report.css │ └── report.dark.css │ ├── i18n │ ├── i18n_cn.json │ └── i18n_en.json │ ├── images │ ├── arrow_right.png │ ├── bottom_arrow_left.png │ ├── bottom_arrow_left.svg │ ├── bottom_arrow_right.png │ ├── img_add.png │ └── report_success.png │ ├── invite_detail.html │ ├── join_group.html │ ├── js │ ├── IMJSBridge.js │ ├── config.js │ ├── index.js │ ├── jquery-3.4.1.min.js │ └── jquery.i18n.min.js │ ├── privacy_policy.html │ ├── report.html │ ├── report_notice.html │ ├── report_success.html │ ├── sdkinfo.html │ ├── success.png │ └── user_agreement.html ├── configs ├── push │ └── push_dev.p12 └── tsdd.yaml ├── docker └── tsdd │ ├── .env │ ├── configs │ ├── tsdd.yaml │ └── wk.yaml │ └── docker-compose.yaml ├── docs ├── architecture.png ├── architecture1.png ├── ascci logo.md ├── banner.png ├── download │ ├── android.png │ └── iOS.png ├── logo.jpg ├── logo.svg ├── screenshot │ ├── apm.webp │ ├── conversationlist.webp │ ├── forward.webp │ ├── group.webp │ ├── messages.webp │ ├── other.webp │ ├── others.webp │ ├── pc11.png │ ├── pc22.png │ ├── pc33.png │ ├── reply.webp │ ├── robot.webp │ ├── typing.webp │ ├── voice.webp │ └── weblogin.webp ├── tsddwx.png └── wkwx.png ├── go.mod ├── go.sum ├── internal └── modules.go ├── main.go ├── modules ├── base │ ├── 1module.go │ ├── app │ │ ├── api.go │ │ ├── const.go │ │ ├── db.go │ │ └── service.go │ ├── common │ │ ├── const.go │ │ ├── service_aliyun.go │ │ ├── service_sms.go │ │ ├── service_smsbao.go │ │ └── service_unisms.go │ ├── elastic │ │ ├── db.go │ │ └── service.go │ ├── event │ │ ├── api.go │ │ ├── db.go │ │ └── handler.go │ └── sql │ │ ├── app-20201103-01.sql │ │ ├── app-20230912-01.sql │ │ ├── event-20191106-01.sql │ │ └── event-20250423-01.sql ├── channel │ ├── 1module.go │ ├── api.go │ ├── db_channel_setting.go │ ├── service.go │ ├── service │ │ └── service.go │ ├── sql │ │ ├── channel-20221124-01.sql │ │ ├── channel-20230920-01.sql │ │ └── channel-20240515-01.sql │ └── swagger │ │ └── api.yaml ├── common │ ├── 1module.go │ ├── api.go │ ├── api_manager.go │ ├── api_test.go │ ├── db.go │ ├── db_app_config.go │ ├── db_shortno.go │ ├── service.go │ ├── sql │ │ ├── common-20210421-01.sql │ │ ├── common-20210818-01.sql │ │ ├── common-20211108-01.sql │ │ ├── common-20220908-01.sql │ │ ├── common-20220916-01.sql │ │ ├── common-20220917-01.sql │ │ ├── common-20221111-01.sql │ │ ├── common-20221114-01.sql │ │ ├── common-20230203-01.sql │ │ ├── common-20240418-01.sql │ │ ├── common-20240506-01.sql │ │ ├── common-20240510-01.sql │ │ └── common-20240528-01.sql │ └── swagger │ │ └── api.yaml ├── file │ ├── 1module.go │ ├── api.go │ ├── const.go │ ├── service.go │ ├── service_minio.go │ ├── service_oss.go │ ├── service_oss_test.go │ ├── service_qiniu.go │ ├── service_seaweedfs.go │ ├── service_test.go │ └── swagger │ │ └── api.yaml ├── group │ ├── 1module.go │ ├── api.go │ ├── api_manager.go │ ├── api_manager_test.go │ ├── api_setting_action.go │ ├── api_test.go │ ├── const.go │ ├── db.go │ ├── db_manager.go │ ├── db_setting.go │ ├── event.go │ ├── invite.go │ ├── invite_db.go │ ├── service.go │ ├── source.go │ ├── sql │ │ ├── group_20191106-01.sql │ │ ├── group_20211202-02.sql │ │ ├── group_20220411-01.sql │ │ ├── group_20220815-01.sql │ │ ├── group_20220818-01.sql │ │ ├── group_20220830-01.sql │ │ ├── group_20231123-01.sql │ │ └── group_20240510-01.sql │ └── swagger │ │ └── api.yaml ├── message │ ├── 1module.go │ ├── api.go │ ├── api_conversation.go │ ├── api_manager.go │ ├── api_pinned.go │ ├── api_reminders.go │ ├── api_test.go │ ├── api_vo.go │ ├── common.go │ ├── const.go │ ├── db.go │ ├── db_channel_offset.go │ ├── db_conversation_extra.go │ ├── db_device_offset.go │ ├── db_manager.go │ ├── db_member_readed.go │ ├── db_message_extra.go │ ├── db_message_reaction.go │ ├── db_message_user_extra.go │ ├── db_pinned.go │ ├── db_reminders.go │ ├── db_user_last_offset.go │ ├── event.go │ ├── service.go │ ├── sql │ │ ├── message-20210305-01.sql │ │ ├── message-20210407-01.sql │ │ ├── message-20210416-01.sql │ │ ├── message-20210813-01.sql │ │ ├── message-20211027-01.sql │ │ ├── message-20220414-01.sql │ │ ├── message-20220418-01.sql │ │ ├── message-20220422-01.sql │ │ ├── message-20220801-01.sql │ │ ├── message-20220810-01.sql │ │ ├── message-20221122-01.sql │ │ └── message-20240510-01.sql │ └── swagger │ │ ├── api.yaml │ │ └── conversation.yaml ├── openapi │ ├── 1module.go │ ├── api.go │ └── swagger │ │ └── api.yaml ├── qrcode │ ├── 1module.go │ ├── api.go │ └── constant.go ├── report │ ├── 1module.go │ ├── api.go │ ├── api_manager.go │ ├── api_manager_test.go │ ├── api_test.go │ ├── db.go │ ├── db_manager.go │ ├── sql │ │ ├── report-202012221237-01.sql │ │ └── report-202211291659-01.sql │ └── swagger │ │ └── api.yaml ├── robot │ ├── 1module.go │ ├── api.go │ ├── api_manager.go │ ├── api_test.go │ ├── const.go │ ├── db.go │ ├── event.go │ ├── model.go │ ├── sql │ │ ├── robot-20210926-01.sql │ │ ├── robot-20211026-01.sql │ │ └── robot-20211105-01.sql │ └── swagger │ │ └── api.yaml ├── source │ └── source.go ├── statistics │ ├── 1module.go │ ├── api.go │ └── api_test.go ├── user │ ├── 1module.go │ ├── api.go │ ├── api_device.go │ ├── api_friend.go │ ├── api_gitee.go │ ├── api_github.go │ ├── api_login_log.go │ ├── api_maillist.go │ ├── api_manager.go │ ├── api_manager_test.go │ ├── api_online.go │ ├── api_setting.go │ ├── api_test.go │ ├── api_usernamelogin.go │ ├── const.go │ ├── db.go │ ├── db_device.go │ ├── db_device_flag.go │ ├── db_friend.go │ ├── db_gitee.go │ ├── db_github.go │ ├── db_identities.go │ ├── db_login_log.go │ ├── db_maillist.go │ ├── db_manager.go │ ├── db_onetime_prekeys.go │ ├── db_online.go │ ├── db_setting.go │ ├── event_friend.go │ ├── friend_test.go │ ├── service.go │ ├── service_online.go │ ├── source.go │ ├── sql │ │ ├── user-20191106-01.sql │ │ ├── user-20210204-01.sql │ │ ├── user-20210405-01.sql │ │ ├── user-20210413-01.sql │ │ ├── user-20210907-01.sql │ │ ├── user-20210916-01.sql │ │ ├── user-20211115-01.sql │ │ ├── user-20220222-01.sql │ │ ├── user-20220609-01.sql │ │ ├── user-20220713-01.sql │ │ ├── user-20220816-01.sql │ │ ├── user-20220906-01.sql │ │ ├── user-20220919-01.sql │ │ ├── user-20230611-01.sql │ │ ├── user-20230911-01.sql │ │ ├── user-20230924-01.sql │ │ └── user-20231127-01.sql │ ├── swagger │ │ ├── api.yaml │ │ └── friend.yaml │ └── webhook.go ├── webhook │ ├── 1module.go │ ├── api.go │ ├── api_datasource.go │ ├── apns.go │ ├── common.go │ ├── db.go │ ├── db_message.go │ ├── db_test.go │ ├── github.go │ ├── push.go │ ├── push_firebase.go │ ├── push_hms.go │ ├── push_iosapns.go │ ├── push_mi.go │ ├── push_oppo.go │ ├── push_test.go │ ├── push_vivo.go │ └── sql │ │ ├── webhook-20210226-01.sql │ │ ├── webhook-20230920-01.sql │ │ └── webhook-20241217-01.sql └── workplace │ ├── 1module.go │ ├── api.go │ ├── api_manager.go │ ├── api_manager_test.go │ ├── api_test.go │ ├── db.go │ ├── db_manager.go │ ├── sql │ ├── workplace-20230823-01.sql │ ├── workplace-20230906-01.sql │ └── workplace-20240113-01.sql │ └── swagger │ └── api.yaml ├── pkg ├── cache │ └── cache.go ├── db │ ├── db.go │ ├── mysql.go │ ├── redis.go │ └── sqlite.go ├── keylock │ └── keylock.go ├── log │ ├── logger.go │ ├── logger_test.go │ └── options.go ├── markdown │ ├── markdown.go │ └── markdown_test.go ├── network │ └── network.go ├── pool │ ├── dispatcher.go │ ├── queue.go │ └── worker.go ├── redis │ └── redis.go ├── register │ └── register.go ├── util │ ├── aes.go │ ├── base62.go │ ├── common.go │ ├── decimal.go │ ├── dh.go │ ├── hash.go │ ├── ip.go │ ├── json.go │ ├── md5.go │ ├── page.go │ ├── qreflect.go │ ├── qreflect_test.go │ ├── sha.go │ ├── sign.go │ ├── string.go │ ├── stringbuffer.go │ ├── time.go │ ├── uuid.go │ └── yuan_cent.go ├── wait │ └── wait.go ├── wkevent │ └── event.go ├── wkhook │ ├── README.md │ ├── webhook.pb.go │ ├── webhook.proto │ └── webhook_grpc.pb.go ├── wkhttp │ └── http.go └── wkrsa │ ├── rsa.go │ └── rsa_test.go └── testenv └── docker-compose.yaml /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 提出提问 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # 现象 11 | 12 | # 日志 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .idea/ 17 | 18 | *.socket 19 | 20 | *.db 21 | 22 | *.sqlite 23 | 24 | *.log 25 | .DS_Store 26 | 27 | .VSCodeCounter/ 28 | 29 | logs/ 30 | 31 | wukongimdata/ -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [], 7 | "name": "Launch", 8 | "type": "go", 9 | "request": "launch", 10 | "mode": "debug", 11 | "program": "${workspaceFolder}/main.go" 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.alternateTools": { 3 | 4 | }, 5 | "go.testFlags": ["-v"] 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 as build 2 | 3 | ENV GOPROXY https://goproxy.cn,direct 4 | ENV GO111MODULE on 5 | 6 | 7 | WORKDIR /go/cache 8 | 9 | 10 | ADD go.mod . 11 | ADD go.sum . 12 | RUN go mod download 13 | 14 | WORKDIR /go/release 15 | 16 | 17 | 18 | # RUN apt-get update && \ 19 | # apt-get -y install ca-certificates 20 | 21 | ADD . . 22 | 23 | # RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-w -extldflags "-static"' -installsuffix cgo -o app ./main.go 24 | 25 | RUN GIT_COMMIT=$(git rev-parse HEAD) && \ 26 | GIT_COMMIT_DATE=$(git log --date=iso8601-strict -1 --pretty=%ct) && \ 27 | GIT_VERSION=$(git describe --tags --abbrev=0) && \ 28 | GIT_TREE_STATE=$(test -n "`git status --porcelain`" && echo "dirty" || echo "clean") && \ 29 | CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -extldflags '-static' -X main.Commit=$GIT_COMMIT -X main.CommitDate=$GIT_COMMIT_DATE -X main.Version=$GIT_VERSION -X main.TreeState=$GIT_TREE_STATE" -installsuffix cgo -o app ./main.go 30 | 31 | 32 | FROM alpine as prod 33 | # Import the user and group files from the builder. 34 | COPY --from=build /etc/passwd /etc/passwd 35 | COPY --from=build /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 36 | RUN \ 37 | mkdir -p /usr/share/zoneinfo/Asia && \ 38 | ln -s /etc/localtime /usr/share/zoneinfo/Asia/Shanghai 39 | COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 40 | WORKDIR /home 41 | COPY --from=build /go/release/app /home 42 | COPY --from=build /go/release/assets /home/assets 43 | COPY --from=build /go/release/configs /home/configs 44 | RUN echo "Asia/Shanghai" > /etc/timezone 45 | ENV TZ=Asia/Shanghai 46 | 47 | # 不加 apk add ca-certificates apns2推送将请求错误 48 | # RUN apk add ca-certificates 49 | 50 | ENTRYPOINT ["/home/app"] 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t tangsengdaodaoserver . 3 | push: 4 | docker tag tangsengdaodaoserver registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoserver:latest 5 | docker push registry.cn-shanghai.aliyuncs.com/wukongim/wukongchatserver:latest 6 | deploy: 7 | docker build -t tangsengdaodaoserver . --platform linux/amd64 8 | docker tag tangsengdaodaoserver registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoserver:latest 9 | docker push registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoserver:latest 10 | deploy-v1.5: 11 | docker build -t tangsengdaodaoserver . --platform linux/amd64 12 | docker tag tangsengdaodaoserver registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoserver:v1.5 13 | docker push registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoserver:v1.5 14 | run-dev: 15 | docker-compose build;docker-compose up -d 16 | stop-dev: 17 | docker-compose stop 18 | env-test: 19 | docker-compose -f ./testenv/docker-compose.yaml up -d -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /ascii_art.txt: -------------------------------------------------------------------------------- 1 | LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL 2 | LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL 3 | LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL 4 | LLLLLLLLLLLL0CLLLLLLLLLLLLLLLLLLLLLLLLLL 5 | LLLLLLLLLL08@880CfLLLLLLLLLLLLLLLLLLLLLL 6 | LLLLLLLLfL8@8@@8LfLLLLLLLLLLLLLLLLLLLLLL 7 | ffffffffft0@@8@8ffffffffffffffffffffffff 8 | fffffffffCCL8@GLLfLLLfffffffffffffffffff 9 | ffffffffCLLC0@GCCLLLLCffffffffffffffffff 10 | ffffffffG0@@@@@@@8Ltffffffffffffffffffff 11 | ffffffftC888888888Gtffffffffffffffffffff 12 | ffffffftttttttttttttffffffffffffffffffff 13 | fffffffttttttttttttfffffffffffffffffffff 14 | tttttttttttttfftffttttttttttttttttfttttt 15 | tttttttttttttttttttttttttttttttttttttttt 16 | tttttttttttttttttttttttttttttttttttttttt 17 | tttttttttttttttttttttttttttttttttttttttt 18 | tttttttttttttttttttttttttttttttttttttttt 19 | tttttttttttttttttttttttttttttttttttttttt 20 | 111t111111111tt1111111tt1111111t11111111 -------------------------------------------------------------------------------- /ascii_result.txt: -------------------------------------------------------------------------------- 1 | [?25l[?7lLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL 2 | LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL 3 | LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL 4 | LLLLLLLLLLLL0CLLLLLLLLLLLLLLLLLLLLLLLLLL 5 | LLLLLLLLLL08@880CfLLLLLLLLLLLLLLLLLLLLLL 6 | LLLLLLLLfL8@8@@8LfLLLLLLLLLLLLLLLLLLLLLL 7 | ffffffffft0@@8@8ffffffffffffffffffffffff 8 | fffffffffCCL8@GLLfLLLfffffffffffffffffff 9 | ffffffffCLLC0@GCCLLLLCffffffffffffffffff 10 | ffffffffG0@@@@@@@8Ltffffffffffffffffffff 11 | ffffffftC888888888Gtffffffffffffffffffff 12 | ffffffftttttttttttttffffffffffffffffffff 13 | fffffffttttttttttttfffffffffffffffffffff 14 | tttttttttttttfftffttttttttttttttttfttttt 15 | tttttttttttttttttttttttttttttttttttttttt 16 | tttttttttttttttttttttttttttttttttttttttt 17 | tttttttttttttttttttttttttttttttttttttttt 18 | tttttttttttttttttttttttttttttttttttttttt 19 | tttttttttttttttttttttttttttttttttttttttt 20 | 111t111111111tt1111111tt1111111t11111111 21 |  22 | WuKongChat is running 23 | --------------------- 24 | Mode: #mode# 25 | App name: #appname# 26 | Version: #version# 27 | Git: #git# 28 | Go build: #gobuild# 29 | IM URL: #imurl# 30 | File Service: #fileService# 31 | The API is listening at: #apiAddr# 32 | 33 |          34 |          35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | [?25h[?7h -------------------------------------------------------------------------------- /assets/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/assets/avatar.png -------------------------------------------------------------------------------- /assets/assets/dept_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/assets/dept_avatar.png -------------------------------------------------------------------------------- /assets/assets/fileHelper.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/assets/fileHelper.jpeg -------------------------------------------------------------------------------- /assets/assets/g_avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/assets/g_avatar.jpeg -------------------------------------------------------------------------------- /assets/assets/org_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/assets/org_avatar.png -------------------------------------------------------------------------------- /assets/assets/u_10000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/assets/u_10000.png -------------------------------------------------------------------------------- /assets/web/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | 7 | .button { 8 | background-color: white; 9 | padding: 8px 20px; 10 | text-align: center; 11 | border-radius: 4px; 12 | cursor: pointer; 13 | 14 | } 15 | 16 | .primary { 17 | background-color: #2F70F5; 18 | color: white; 19 | } -------------------------------------------------------------------------------- /assets/web/css/join_group.css: -------------------------------------------------------------------------------- 1 | 2 | .group-box { 3 | display: flex; 4 | flex-direction: column; 5 | height: 100vh; 6 | } 7 | 8 | .top { 9 | width: 100%; 10 | padding: 40px 0px; 11 | background-color: white; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | border-bottom: 1px #ddd solid; 16 | min-height: 120px; 17 | 18 | } 19 | 20 | .group-info { 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | background-color: white; 26 | } 27 | 28 | .group-info p { 29 | padding: 0; 30 | margin: 10px 0px 0px 0px; 31 | } 32 | 33 | .group-info .name { 34 | font-size: 18px; 35 | } 36 | 37 | .group-info .num { 38 | font-size: 14px; 39 | color: gray; 40 | } 41 | 42 | .group-info img { 43 | width: 64px; 44 | height: 64px; 45 | border-radius: 6px; 46 | } 47 | 48 | .bottom { 49 | background-color: #eee; 50 | width: 100%; 51 | flex: 1; 52 | 53 | } 54 | 55 | .bottom .box { 56 | display: flex; 57 | margin-top: 20px; 58 | flex-direction: column; 59 | align-items: center; 60 | } 61 | 62 | .bottom .box p { 63 | color: gray; 64 | font-size: 20px; 65 | } -------------------------------------------------------------------------------- /assets/web/i18n/i18n_cn.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "report.title": "请选择投诉该账号的原因", 4 | "report.notice": "投诉须知" 5 | } -------------------------------------------------------------------------------- /assets/web/i18n/i18n_en.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "report.title": "Please select the reason", 4 | "report.notice": "Report Notice", 5 | "report.image.evidence": "Photographic evidence (optional)", 6 | "report.content.placeholder": "Complaint content (optional)", 7 | "report.submit": "Submit" 8 | } -------------------------------------------------------------------------------- /assets/web/images/arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/web/images/arrow_right.png -------------------------------------------------------------------------------- /assets/web/images/bottom_arrow_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/web/images/bottom_arrow_left.png -------------------------------------------------------------------------------- /assets/web/images/bottom_arrow_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/web/images/bottom_arrow_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/web/images/bottom_arrow_right.png -------------------------------------------------------------------------------- /assets/web/images/img_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/web/images/img_add.png -------------------------------------------------------------------------------- /assets/web/images/report_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/web/images/report_success.png -------------------------------------------------------------------------------- /assets/web/join_group.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 确认加群 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 |

25 |

26 |
27 |
28 |
29 |
30 |

确认加入群聊

31 |
加入该群聊
32 |
33 |
34 |
35 | 36 | 37 | 58 | 59 | -------------------------------------------------------------------------------- /assets/web/js/config.js: -------------------------------------------------------------------------------- 1 | const apiURL = "http://192.168.3.13:8090/v1/" -------------------------------------------------------------------------------- /assets/web/js/index.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO: 此页面的 invoke 相关的方法应该作废了 应该使用IMJSBridge这个JS 3 | 4 | /** 5 | * 显示会话界面 6 | * @param {*} param0 7 | */ 8 | function showConversation({channelID,channelType}) { 9 | invoke('showConversation',{ 10 | 'forward': 'replace', 11 | 'channel_id': channelID, 12 | 'channel_type': channelType 13 | }) 14 | } 15 | 16 | function pop() { 17 | invoke('pop') 18 | } 19 | 20 | // 关闭 21 | function closeWebView() { 22 | invoke('pop') 23 | } 24 | 25 | function getQueryString(name) 26 | { 27 | var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)"); 28 | var r = window.location.search.substr(1).match(reg); 29 | if(r!=null)return unescape(r[2]); return null; 30 | } 31 | 32 | 33 | function invoke(method,param,callback) { 34 | if(isIOS()) { 35 | invokeIOS(method,param,callback); 36 | } 37 | } 38 | 39 | function invokeIOS(method,param,callback) { 40 | window.WKJSBridge.callNative("LIMCommonPlugin", method, param, function success(res) { 41 | if(callback) { 42 | callback(true,res) 43 | } 44 | }, function fail(res) { 45 | if(callback) { 46 | callback(false,res) 47 | } 48 | }); 49 | } 50 | 51 | function isIOS() { 52 | return true 53 | } 54 | 55 | function isAndroid() { 56 | return false 57 | } 58 | 59 | $.extend({ 60 | postJSON: function (url, body) { 61 | return $.ajax({ 62 | type: 'POST', 63 | url: url, 64 | data: JSON.stringify(body), 65 | contentType: "application/json", 66 | dataType: 'json' 67 | }); 68 | } 69 | }); 70 | 71 | function uuid() { 72 | var s = []; 73 | var hexDigits = "0123456789abcdef"; 74 | for (var i = 0; i < 36; i++) { 75 | s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1); 76 | } 77 | s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010 78 | s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01 79 | s[8] = s[13] = s[18] = s[23] = "-"; 80 | 81 | var uuid = s.join(""); 82 | return uuid; 83 | } -------------------------------------------------------------------------------- /assets/web/js/jquery.i18n.min.js: -------------------------------------------------------------------------------- 1 | (function($){$.fn.extend({i18n:function(options){var defaults={lang:"",defaultLang:"",filePath:"/i18n/",filePrefix:"i18n_",fileSuffix:"",forever:true,callback:function(){}};function getCookie(name){var arr=document.cookie.split('; ');for(var i=0;i 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 投诉 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 |
投诉已提交
24 |
25 | 我们会尽快核实,并通过消息通知你审核结果。感谢你的支持。 26 |
27 |
关闭
28 |
29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /assets/web/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/assets/web/success.png -------------------------------------------------------------------------------- /configs/push/push_dev.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/configs/push/push_dev.p12 -------------------------------------------------------------------------------- /docker/tsdd/.env: -------------------------------------------------------------------------------- 1 | # minio 2 | MINIO_ROOT_USER: minio 3 | MINIO_ROOT_PASSWORD: #MINIO_ROOT_PASSWORD# 4 | MINIO_SERVER_URL: #MINIO_SERVER_URL# 5 | MINIO_BROWSER_REDIRECT_URL: #MINIO_BROWSER_REDIRECT_URL# 6 | 7 | # mysql 8 | MYSQL_ROOT_PASSWORD: #MYSQL_ROOT_PASSWORD# # 数据库的密码 9 | MYSQL_DATABASE: im 10 | 11 | ## web 12 | API_URL: #API_URL# 13 | -------------------------------------------------------------------------------- /docker/tsdd/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | wukongim: # 悟空IM通讯服务 4 | image: registry.cn-shanghai.aliyuncs.com/wukongim/wukongim:latest 5 | restart: always 6 | ports: 7 | - "5100:5100" 8 | - "5200:5200" 9 | - "5300:5300" 10 | volumes: 11 | - ./configs/wk.yaml:/root/wukongim/wk.yaml 12 | - ./logs/wk:/root/wukongim/logs 13 | tangsengdaodaoserver: # 唐僧叨叨的业务服务 14 | image: registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoserver:latest 15 | restart: always 16 | command: "api" 17 | healthcheck: 18 | test: "wget -q -Y off -O /dev/null http://localhost:8090/v1/ping > /dev/null 2>&1" 19 | interval: 10s 20 | timeout: 10s 21 | retries: 3 22 | depends_on: 23 | - redis 24 | - mysql 25 | - wukongim 26 | ports: 27 | - "8090:8090" 28 | volumes: 29 | - ./configs/tsdd.yaml:/home/configs/tsdd.yaml 30 | - ./logs/tsdd:/home/logs 31 | tangsengdaodaoweb: # 唐僧叨叨的web服务 32 | image: registry.cn-shanghai.aliyuncs.com/wukongim/tangsengdaodaoweb:latest 33 | restart: always 34 | environment: 35 | - API_URL 36 | ports: 37 | - "82:80" 38 | minio: # minio文件管理服务 39 | image: minio/minio:latest # use a remote image 40 | expose: 41 | - "9000" 42 | - "9001" 43 | command: "server /data --console-address ':9001'" 44 | ports: 45 | - "9000:9000" 46 | - "9001:9001" 47 | environment: 48 | - MINIO_ROOT_USER 49 | - MINIO_ROOT_PASSWORD 50 | - MINIO_SERVER_URL 51 | - MINIO_BROWSER_REDIRECT_URL 52 | healthcheck: 53 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 54 | interval: 30s 55 | timeout: 20s 56 | retries: 3 57 | volumes: 58 | - ./miniodata:/data 59 | mysql: # mysql数据库 60 | image: mysql:latest 61 | command: --default-authentication-plugin=mysql_native_password 62 | healthcheck: 63 | test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] 64 | environment: 65 | - TZ=Asia/Shanghai 66 | - MYSQL_ROOT_PASSWORD 67 | - MYSQL_DATABASE 68 | volumes: 69 | - ./mysqldata:/var/lib/mysql 70 | redis: # redis 71 | image: redis 72 | restart: always 73 | healthcheck: 74 | test: ["CMD", "redis-cli", "ping"] 75 | interval: 1s 76 | timeout: 3s 77 | retries: 30 78 | adminer: # mysql web管理工具 调试用,为了安全生产不要打开 79 | image: adminer:latest 80 | ports: 81 | - 8306:8080 82 | -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/architecture.png -------------------------------------------------------------------------------- /docs/architecture1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/architecture1.png -------------------------------------------------------------------------------- /docs/ascci logo.md: -------------------------------------------------------------------------------- 1 | 2 | 开源地址: 3 | 4 | 先用image2ascii将图片生成 ascii 5 | https://github.com/qeesung/image2ascii 6 | 7 | 8 | 然后用ascii生成信息 9 | https://github.com/dylanaraps/neofetch 10 | 11 | 12 | // 生成 13 | 14 | image2ascii -f docs/logo.jpg -r 0.08 > ascii_art.txt 15 | 16 | neofetch --config ascii.conf --source ascii_art.txt > ascii_result.txt -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/banner.png -------------------------------------------------------------------------------- /docs/download/android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/download/android.png -------------------------------------------------------------------------------- /docs/download/iOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/download/iOS.png -------------------------------------------------------------------------------- /docs/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/logo.jpg -------------------------------------------------------------------------------- /docs/screenshot/apm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/apm.webp -------------------------------------------------------------------------------- /docs/screenshot/conversationlist.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/conversationlist.webp -------------------------------------------------------------------------------- /docs/screenshot/forward.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/forward.webp -------------------------------------------------------------------------------- /docs/screenshot/group.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/group.webp -------------------------------------------------------------------------------- /docs/screenshot/messages.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/messages.webp -------------------------------------------------------------------------------- /docs/screenshot/other.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/other.webp -------------------------------------------------------------------------------- /docs/screenshot/others.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/others.webp -------------------------------------------------------------------------------- /docs/screenshot/pc11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/pc11.png -------------------------------------------------------------------------------- /docs/screenshot/pc22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/pc22.png -------------------------------------------------------------------------------- /docs/screenshot/pc33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/pc33.png -------------------------------------------------------------------------------- /docs/screenshot/reply.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/reply.webp -------------------------------------------------------------------------------- /docs/screenshot/robot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/robot.webp -------------------------------------------------------------------------------- /docs/screenshot/typing.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/typing.webp -------------------------------------------------------------------------------- /docs/screenshot/voice.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/voice.webp -------------------------------------------------------------------------------- /docs/screenshot/weblogin.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/screenshot/weblogin.webp -------------------------------------------------------------------------------- /docs/tsddwx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/tsddwx.png -------------------------------------------------------------------------------- /docs/wkwx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TangSengDaoDao/TangSengDaoDaoServer/7ff69f5aba43fae7e8c3408fa8e6cbc88dbc14fb/docs/wkwx.png -------------------------------------------------------------------------------- /internal/modules.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | // 引入模块 4 | import ( 5 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/base" 6 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/channel" 7 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/common" 8 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/file" 9 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/group" 10 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/message" 11 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/openapi" 12 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/qrcode" 13 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/report" 14 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/robot" 15 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/statistics" 16 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/user" 17 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/webhook" 18 | _ "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/workplace" 19 | ) 20 | -------------------------------------------------------------------------------- /modules/base/1module.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/base/app" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/register" 9 | ) 10 | 11 | //go:embed sql 12 | var sqlFS embed.FS 13 | 14 | func init() { 15 | 16 | register.AddModule(func(ctx interface{}) register.Module { 17 | 18 | return register.Module{ 19 | SetupAPI: func() register.APIRouter { 20 | return app.New(ctx.(*config.Context)) 21 | }, 22 | SQLDir: register.NewSQLFS(sqlFS), 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /modules/base/app/api.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/wkhttp" 9 | ) 10 | 11 | type App struct { 12 | service IService 13 | } 14 | 15 | func New(ctx *config.Context) *App { 16 | return &App{ 17 | service: NewService(ctx), 18 | } 19 | } 20 | 21 | func (a *App) Route(r *wkhttp.WKHttp) { 22 | r.GET("/v1/apps/:app_id", a.get) 23 | } 24 | 25 | func (a *App) get(c *wkhttp.Context) { 26 | appID := c.Param("app_id") 27 | resp, err := a.service.GetApp(appID) 28 | if err != nil { 29 | c.ResponseError(err) 30 | return 31 | } 32 | if resp.Status == StatusDisable { 33 | c.ResponseError(errors.New("app is disable")) 34 | return 35 | } 36 | c.JSON(http.StatusOK, &appResp{ 37 | AppID: resp.AppID, 38 | AppName: resp.AppName, 39 | AppLogo: resp.AppLogo, 40 | }) 41 | } 42 | 43 | type appResp struct { 44 | AppID string `json:"app_id,omitempty"` 45 | AppName string `json:"app_name,omitempty"` 46 | AppLogo string `json:"app_logo,omitempty"` 47 | } 48 | -------------------------------------------------------------------------------- /modules/base/app/const.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | // Status app状态 4 | type Status int 5 | 6 | const ( 7 | // StatusDisable app被禁用 8 | StatusDisable Status = iota 9 | // StatusEnable app被启用 10 | StatusEnable 11 | ) 12 | 13 | func (s Status) Int() int { 14 | return int(s) 15 | } 16 | 17 | func (s Status) String() string { 18 | switch s { 19 | case StatusDisable: 20 | return "disable" 21 | case StatusEnable: 22 | return "enable" 23 | default: 24 | return "unknown" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/base/app/db.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | // DB DB 10 | type DB struct { 11 | session *dbr.Session 12 | } 13 | 14 | func newDB(session *dbr.Session) *DB { 15 | return &DB{ 16 | session: session, 17 | } 18 | } 19 | 20 | func (d *DB) queryWithAppID(appID string) (*model, error) { 21 | var m *model 22 | _, err := d.session.Select("*").From("app").Where("app_id=?", appID).Load(&m) 23 | return m, err 24 | } 25 | 26 | func (d *DB) existWithAppID(appID string) (bool, error) { 27 | var count int 28 | _, err := d.session.Select("count(*)").From("app").Where("app_id=?", appID).Load(&count) 29 | return count > 0, err 30 | } 31 | 32 | func (d *DB) insert(m *model) error { 33 | _, err := d.session.InsertInto("app").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 34 | return err 35 | } 36 | 37 | type model struct { 38 | AppID string 39 | AppKey string 40 | AppName string 41 | AppLogo string 42 | Status int 43 | db.BaseModel 44 | } 45 | -------------------------------------------------------------------------------- /modules/base/app/service.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 9 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 10 | ) 11 | 12 | // IService 服务接口 13 | type IService interface { 14 | // GetApp 获取app 15 | GetApp(appID string) (*Resp, error) 16 | // 创建app 17 | CreateApp(r Req) (*Resp, error) 18 | } 19 | 20 | // Service app服务 21 | type Service struct { 22 | ctx *config.Context 23 | db *DB 24 | } 25 | 26 | // NewService NewService 27 | func NewService(ctx *config.Context) IService { 28 | return &Service{ 29 | ctx: ctx, 30 | db: newDB(ctx.DB()), 31 | } 32 | } 33 | 34 | // GetApp 获取APP信息 35 | func (s *Service) GetApp(appID string) (*Resp, error) { 36 | appM, err := s.db.queryWithAppID(appID) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if appM == nil { 41 | return nil, fmt.Errorf("app[%s]不存在!", appID) 42 | } 43 | return &Resp{ 44 | AppID: appM.AppID, 45 | AppName: appM.AppName, 46 | AppLogo: appM.AppLogo, 47 | AppKey: appM.AppKey, 48 | Status: Status(appM.Status), 49 | }, nil 50 | } 51 | 52 | // CreateApp 创建APP 幂等 53 | func (s *Service) CreateApp(r Req) (*Resp, error) { 54 | if err := r.Check(); err != nil { 55 | return nil, err 56 | } 57 | appM, err := s.db.queryWithAppID(r.AppID) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | var appKey string 63 | var appID string 64 | if appM == nil { 65 | appKey = util.GenerUUID() 66 | appID = r.AppID 67 | err = s.db.insert(&model{ 68 | AppID: r.AppID, 69 | Status: StatusEnable.Int(), 70 | AppKey: appKey, 71 | }) 72 | if err != nil { 73 | return nil, err 74 | } 75 | } else { 76 | appID = appM.AppID 77 | appKey = appM.AppKey 78 | } 79 | 80 | return &Resp{ 81 | AppID: appID, 82 | AppKey: appKey, 83 | Status: StatusEnable, 84 | }, nil 85 | 86 | } 87 | 88 | type Resp struct { 89 | AppID string 90 | AppKey string 91 | AppName string 92 | AppLogo string 93 | Status Status 94 | } 95 | 96 | type Req struct { 97 | AppID string 98 | } 99 | 100 | func (r Req) Check() error { 101 | if len(strings.TrimSpace(r.AppID)) <= 0 { 102 | return errors.New("appID不能为空!") 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /modules/base/common/const.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // CodeType 验证码类型 4 | type CodeType int 5 | 6 | const ( 7 | // CodeTypeRegister 注册 8 | CodeTypeRegister CodeType = iota 9 | // CodeTypePayPWD 支付密码 10 | CodeTypePayPWD 11 | // CodeTypeForgetLoginPWD 忘记登录密码 12 | CodeTypeForgetLoginPWD 13 | // CodeTypeCheckMobile 校验指定手机号是否正确 14 | CodeTypeCheckMobile 15 | // DestroyAccount 注销账号 16 | CodeTypeDestroyAccount 17 | ) 18 | 19 | const ( 20 | // CacheKeySMSCode 短信验证码的缓存key 21 | CacheKeySMSCode string = "smscode:" 22 | ) 23 | -------------------------------------------------------------------------------- /modules/base/common/service_unisms.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 9 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/log" 10 | unisms "github.com/apistd/uni-go-sdk/sms" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type UnismsProvider struct { 15 | ctx *config.Context 16 | log.Log 17 | } 18 | 19 | // NewUnismsProvider 创建短信服务 20 | func NewUnismsProvider(ctx *config.Context) ISMSProvider { 21 | return &UnismsProvider{ 22 | ctx: ctx, 23 | Log: log.NewTLog("UnismsProvider"), 24 | } 25 | } 26 | 27 | func (u *UnismsProvider) SendSMS(ctx context.Context, zone, phone string, code string) error { 28 | ph := phone 29 | if zone != "0086" { 30 | if len(zone) > 2 { 31 | ph = strings.Replace(zone, "00", "", 1) + phone 32 | } 33 | } 34 | 35 | cli := unisms.NewClient(u.ctx.GetConfig().UniSMS.AccessKeyID, u.ctx.GetConfig().UniSMS.AccessKeySecret) 36 | 37 | // 构建信息 38 | message := unisms.BuildMessage() 39 | message.SetTo(ph) 40 | message.SetSignature(u.ctx.GetConfig().UniSMS.Signature) 41 | message.SetTemplateId(u.ctx.GetConfig().UniSMS.TemplateId) 42 | message.SetTemplateData(map[string]string{"code": code}) // 设置自定义参数 (变量短信) 43 | 44 | // 发送短信 45 | res, err := cli.Send(message) 46 | if err != nil { 47 | u.Error("发送短信失败!", zap.Error(err)) 48 | return err 49 | } 50 | if res.Code != "0" { 51 | u.Error("发送短信失败!", zap.String("message", res.Message)) 52 | return errors.New(res.Message) 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /modules/base/elastic/db.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | // DB DB 10 | type DB struct { 11 | session *dbr.Session 12 | } 13 | 14 | // NewDB NewDB 15 | func NewDB(session *dbr.Session) *DB { 16 | return &DB{ 17 | session: session, 18 | } 19 | } 20 | 21 | // Insert Insert 22 | func (d *DB) Insert(model *IndexerErrorModel) error { 23 | _, err := d.session.InsertInto("indexer_error").Columns(util.AttrToUnderscore(model)...).Record(model).Exec() 24 | return err 25 | } 26 | 27 | // IndexerErrorModel IndexerErrorModel 28 | type IndexerErrorModel struct { 29 | Index string 30 | Action string 31 | DocumentID string 32 | Body string 33 | Error string 34 | db.BaseModel 35 | } 36 | -------------------------------------------------------------------------------- /modules/base/elastic/service.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/group" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/message" 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/common" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/log" 9 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/wkhttp" 10 | "github.com/olivere/elastic" 11 | ) 12 | 13 | // Service Service 14 | type Service struct { 15 | ctx *config.Context 16 | log.Log 17 | messageDB *message.DB 18 | db *DB 19 | groupDB *group.DB 20 | } 21 | 22 | // NewService NewService 23 | func NewService(ctx *config.Context) *Service { 24 | 25 | return &Service{ 26 | ctx: ctx, 27 | Log: log.NewTLog("Eastic"), 28 | messageDB: message.NewDB(ctx), 29 | db: NewDB(ctx.DB()), 30 | groupDB: group.NewDB(ctx), 31 | } 32 | } 33 | 34 | // Route Route 35 | func (s *Service) Route(r *wkhttp.WKHttp) { 36 | 37 | } 38 | 39 | // PushMessageElasticIndexTask 创建消息索引的任务 40 | func (s *Service) PushMessageElasticIndexTask(resps []msgResp) { 41 | if len(resps) <= 0 { 42 | return 43 | } 44 | for _, resp := range resps { 45 | if resp.ChannelType == common.ChannelTypePerson.Uint8() { 46 | elastic.NewBulkIndexRequest().Index(resp.FromUID).Type("message") 47 | } 48 | } 49 | elastic.NewBulkIndexRequest().Index("message") 50 | 51 | } 52 | 53 | // BulkIndexerItem BulkIndexerItem 54 | type msgResp struct { 55 | MessageID uint64 `json:"message_id"` // 服务端的消息ID(全局唯一) 56 | FromUID string `json:"from_uid"` // 发送者UID 57 | ChannelID string `json:"channel_id"` // 频道ID 58 | ChannelType uint8 `json:"channel_type"` // 频道类型 59 | Timestamp int64 `json:"timestamp"` // 服务器消息时间戳(10位,到秒) 60 | Payload []byte `json:"payload"` // 消息内容 61 | } 62 | -------------------------------------------------------------------------------- /modules/base/event/db.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/wkevent" 9 | "github.com/gocraft/dbr/v2" 10 | ) 11 | 12 | // DB 事件的db 13 | type DB struct { 14 | session *dbr.Session 15 | } 16 | 17 | // NewDB 创建DB 18 | func NewDB(session *dbr.Session) *DB { 19 | return &DB{ 20 | session: session, 21 | } 22 | } 23 | 24 | // InsertTx 插入事件 25 | func (d *DB) InsertTx(m *Model, tx *dbr.Tx) (int64, error) { 26 | result, err := tx.InsertInto("event").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 27 | if err != nil { 28 | return 0, err 29 | } 30 | return result.LastInsertId() 31 | } 32 | 33 | // UpdateStatus 更新事件状态 34 | func (d *DB) UpdateStatus(reason string, status int, versionLock int64, id int64) error { 35 | _, err := d.session.Update("event").Set("status", status).Set("reason", reason).Where("id=? and version_lock=?", id, versionLock).Exec() 36 | return err 37 | } 38 | 39 | // QueryWithID 根据id查询事件 40 | func (d *DB) QueryWithID(id int64) (*Model, error) { 41 | var model *Model 42 | _, err := d.session.Select("*").From("event").Where("id=?", id).Load(&model) 43 | return model, err 44 | } 45 | 46 | // QueryAllWait 查询所有等待事件 47 | func (d *DB) QueryAllWait(limit uint64) ([]*Model, error) { 48 | var models []*Model 49 | _, err := d.session.Select("*").From("event").Where("status=? and created_at 0 { 30 | for _, channelSettingM := range channelSettingModels { 31 | channelSettingResps = append(channelSettingResps, newChannelSettingResp(channelSettingM)) 32 | } 33 | } 34 | return channelSettingResps, nil 35 | } 36 | 37 | func (s *service) CreateOrUpdateMsgAutoDelete(channelID string, channelType uint8, msgAutoDelete int64) error { 38 | return s.channelSettingDB.insertOrAddMsgAutoDelete(channelID, channelType, msgAutoDelete) 39 | } 40 | 41 | func newChannelSettingResp(m *channelSettingModel) *chservice.ChannelSettingResp { 42 | 43 | return &chservice.ChannelSettingResp{ 44 | ChannelID: m.ChannelID, 45 | ChannelType: m.ChannelType, 46 | ParentChannelID: m.ParentChannelID, 47 | ParentChannelType: m.ParentChannelType, 48 | OffsetMessageSeq: m.OffsetMessageSeq, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/channel/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | type IService interface { 4 | // 获取频道设置集合 5 | GetChannelSettings(channelIDs []string) ([]*ChannelSettingResp, error) 6 | // 创建或更新频道消息自动删除时间 7 | CreateOrUpdateMsgAutoDelete(channelID string, channelType uint8, msgAutoDelete int64) error 8 | } 9 | 10 | type ChannelSettingResp struct { 11 | ChannelID string 12 | ChannelType uint8 13 | ParentChannelID string 14 | ParentChannelType uint8 15 | OffsetMessageSeq uint32 16 | } 17 | -------------------------------------------------------------------------------- /modules/channel/sql/channel-20221124-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | 4 | create table `channel_setting`( 5 | id integer not null primary key AUTO_INCREMENT, 6 | channel_id VARCHAR(40) not null default '', 7 | channel_type smallint not null default 0, 8 | parent_channel_id VARCHAR(40) not null default '', 9 | parent_channel_type smallint not null default 0, 10 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 11 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 12 | ); 13 | 14 | CREATE UNIQUE INDEX channel_setting_uidx on `channel_setting` (channel_id,channel_type); 15 | -------------------------------------------------------------------------------- /modules/channel/sql/channel-20230920-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | -- 修改channel_setting表 4 | ALTER TABLE `channel_setting` ADD COLUMN msg_auto_delete integer not null DEFAULT 0 COMMENT '消息定时删除时间'; 5 | 6 | -- 修改channel_id字段的长度 7 | ALTER TABLE `channel_setting` MODIFY COLUMN channel_id VARCHAR(80); 8 | 9 | -------------------------------------------------------------------------------- /modules/channel/sql/channel-20240515-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `channel_setting` ADD COLUMN offset_message_seq integer not null DEFAULT 0 COMMENT 'channel消息删除偏移seq'; 4 | -------------------------------------------------------------------------------- /modules/common/1module.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/register" 8 | ) 9 | 10 | //go:embed sql 11 | var sqlFS embed.FS 12 | 13 | //go:embed swagger/api.yaml 14 | var swaggerContent string 15 | 16 | func init() { 17 | register.AddModule(func(ctx interface{}) register.Module { 18 | return register.Module{ 19 | Name: "common", 20 | SetupAPI: func() register.APIRouter { 21 | return New(ctx.(*config.Context)) 22 | }, 23 | SQLDir: register.NewSQLFS(sqlFS), 24 | Swagger: swaggerContent, 25 | } 26 | }) 27 | 28 | register.AddModule(func(ctx interface{}) register.Module { 29 | return register.Module{ 30 | SetupAPI: func() register.APIRouter { 31 | return NewManager(ctx.(*config.Context)) 32 | }, 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /modules/common/api_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 11 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/testutil" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestAddVersion(t *testing.T) { 16 | s, ctx := testutil.NewTestServer() 17 | f := New(ctx) 18 | f.Route(s.GetRoute()) 19 | //清除数据 20 | err := testutil.CleanAllTables(ctx) 21 | assert.NoError(t, err) 22 | w := httptest.NewRecorder() 23 | model := &appVersionReq{ 24 | AppVersion: "1.0", 25 | OS: "android", 26 | DownloadURL: "http://www.githubim.com/download/test.apk", 27 | IsForce: 1, 28 | UpdateDesc: "发布新版本", 29 | } 30 | req, _ := http.NewRequest("POST", "/v1/common/appversion", bytes.NewReader([]byte(util.ToJson(model)))) 31 | req.Header.Set("token", testutil.Token) 32 | s.GetRoute().ServeHTTP(w, req) 33 | assert.Equal(t, http.StatusOK, w.Code) 34 | } 35 | 36 | func TestGetNewVersion(t *testing.T) { 37 | s, ctx := testutil.NewTestServer() 38 | f := New(ctx) 39 | //清除数据 40 | err := testutil.CleanAllTables(ctx) 41 | assert.NoError(t, err) 42 | _, err = f.db.insertAppVersion(&appVersionModel{ 43 | AppVersion: "1.0", 44 | OS: "android", 45 | DownloadURL: "http://www.githubim.com", 46 | IsForce: 1, 47 | UpdateDesc: "发布新版本", 48 | }) 49 | assert.NoError(t, err) 50 | 51 | _, err = f.db.insertAppVersion(&appVersionModel{ 52 | AppVersion: "1.2", 53 | OS: "android", 54 | DownloadURL: "http://www.githubim.com", 55 | IsForce: 1, 56 | UpdateDesc: "发布新版本", 57 | }) 58 | assert.NoError(t, err) 59 | 60 | f.Route(s.GetRoute()) 61 | w := httptest.NewRecorder() 62 | req, _ := http.NewRequest("GET", "/v1/common/appversion/android/1.2", nil) 63 | req.Header.Set("token", testutil.Token) 64 | s.GetRoute().ServeHTTP(w, req) 65 | assert.Equal(t, true, strings.Contains(w.Body.String(), `"app_version":1.0`)) 66 | } 67 | 68 | func TestGetAppConfig(t *testing.T) { 69 | s, ctx := testutil.NewTestServer() 70 | f := New(ctx) 71 | //清除数据 72 | err := testutil.CleanAllTables(ctx) 73 | assert.NoError(t, err) 74 | err = f.appConfigDB.insert(&appConfigModel{ 75 | WelcomeMessage: "欢迎使用唐僧叨叨", 76 | NewUserJoinSystemGroup: 1, 77 | RegisterInviteOn: 1, 78 | InviteSystemAccountJoinGroupOn: 1, 79 | SendWelcomeMessageOn: 1, 80 | }) 81 | assert.NoError(t, err) 82 | //f.Route(s.GetRoute()) 83 | w := httptest.NewRecorder() 84 | req, _ := http.NewRequest("GET", "/v1/common/appconfig", nil) 85 | req.Header.Set("token", testutil.Token) 86 | s.GetRoute().ServeHTTP(w, req) 87 | assert.Equal(t, true, strings.Contains(w.Body.String(), `"invite_system_account_join_group_on":1`)) 88 | } 89 | -------------------------------------------------------------------------------- /modules/common/db_app_config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | ldb "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 7 | "github.com/gocraft/dbr/v2" 8 | ) 9 | 10 | type appConfigDB struct { 11 | session *dbr.Session 12 | ctx *config.Context 13 | } 14 | 15 | func newAppConfigDB(ctx *config.Context) *appConfigDB { 16 | 17 | return &appConfigDB{ 18 | session: ctx.DB(), 19 | ctx: ctx, 20 | } 21 | } 22 | 23 | func (a *appConfigDB) query() (*appConfigModel, error) { 24 | var m *appConfigModel 25 | _, err := a.session.Select("*").From("app_config").OrderDesc("created_at").Load(&m) 26 | return m, err 27 | } 28 | 29 | func (a *appConfigDB) insert(m *appConfigModel) error { 30 | _, err := a.session.InsertInto("app_config").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 31 | return err 32 | } 33 | func (a *appConfigDB) updateWithMap(configMap map[string]interface{}, id int64) error { 34 | _, err := a.session.Update("app_config").SetMap(configMap).Where("id=?", id).Exec() 35 | return err 36 | } 37 | 38 | type appConfigModel struct { 39 | RSAPrivateKey string 40 | RSAPublicKey string 41 | Version int 42 | SuperToken string 43 | SuperTokenOn int 44 | RevokeSecond int // 消息可撤回时长 45 | WelcomeMessage string // 登录欢迎语 46 | NewUserJoinSystemGroup int // 新用户是否加入系统群聊 47 | SearchByPhone int // 是否可通过手机号搜索 48 | RegisterInviteOn int // 开启注册邀请机制 49 | SendWelcomeMessageOn int // 开启注册登录发送欢迎语 50 | InviteSystemAccountJoinGroupOn int // 开启系统账号加入群聊 51 | RegisterUserMustCompleteInfoOn int // 注册用户是否必须完善个人信息 52 | ChannelPinnedMessageMaxCount int // 频道置顶消息最大数量 53 | CanModifyApiUrl int // 是否可以修改API地址 54 | ldb.BaseModel 55 | } 56 | -------------------------------------------------------------------------------- /modules/common/db_shortno.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | dbs "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | type shortnoDB struct { 10 | ctx *config.Context 11 | db *dbr.Session 12 | } 13 | 14 | func newShortnoDB(ctx *config.Context) *shortnoDB { 15 | return &shortnoDB{ 16 | ctx: ctx, 17 | db: ctx.DB(), 18 | } 19 | } 20 | 21 | func (s *shortnoDB) inserts(shortnos []string) error { 22 | if len(shortnos) == 0 { 23 | return nil 24 | } 25 | tx, _ := s.db.Begin() 26 | defer func() { 27 | if err := recover(); err != nil { 28 | tx.RollbackUnlessCommitted() 29 | panic(err) 30 | } 31 | }() 32 | for _, st := range shortnos { 33 | _, err := tx.InsertBySql("insert into shortno(shortno) values(?)", st).Exec() 34 | if err != nil { 35 | tx.Rollback() 36 | return err 37 | } 38 | } 39 | if err := tx.Commit(); err != nil { 40 | tx.Rollback() 41 | return err 42 | } 43 | return nil 44 | 45 | } 46 | 47 | func (s *shortnoDB) queryVail() (*shortnoModel, error) { 48 | var m *shortnoModel 49 | _, err := s.db.Select("*").From("shortno").Where("used=0 and hold=0 and locked=0").Limit(1).Load(&m) 50 | return m, err 51 | } 52 | 53 | func (s *shortnoDB) updateLock(shortno string, lock int) error { 54 | _, err := s.db.Update("shortno").Set("locked", lock).Where("shortno=?", shortno).Exec() 55 | return err 56 | } 57 | 58 | func (s *shortnoDB) updateUsed(shortno string, used int, business string) error { 59 | _, err := s.db.Update("shortno").Set("used", used).Set("business", business).Where("shortno=?", shortno).Exec() 60 | return err 61 | } 62 | func (s *shortnoDB) updateHold(shortno string, hold int) error { 63 | _, err := s.db.Update("shortno").Set("hold", hold).Where("shortno=?", shortno).Exec() 64 | return err 65 | } 66 | 67 | func (s *shortnoDB) queryVailCount() (int64, error) { 68 | var cn int64 69 | _, err := s.db.Select("count(*)").From("shortno").Where("used=0 and hold=0 and locked=0").Load(&cn) 70 | return cn, err 71 | } 72 | 73 | type shortnoModel struct { 74 | Shortno string 75 | Used int 76 | Hold int 77 | Locked int 78 | Business string 79 | dbs.BaseModel 80 | } 81 | -------------------------------------------------------------------------------- /modules/common/sql/common-20210421-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | -- app 版本管理 4 | create table `app_version`( 5 | id integer not null primary key AUTO_INCREMENT, 6 | app_version VARCHAR(40) not null default '', -- app 版本 7 | os varchar(40) not null default '', -- 系统 ios|android 8 | is_force smallint not null default 0, -- 是否强制升级 9 | update_desc varchar(100) not null default '', -- 更新说明 10 | download_url varchar(255) not null default '', -- 下载地址 11 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 12 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 13 | ); 14 | 15 | 16 | -------------------------------------------------------------------------------- /modules/common/sql/common-20210818-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | create table `app_config`( 4 | id integer not null primary key AUTO_INCREMENT, 5 | rsa_private_key varchar(4000) not null default '', -- 系统私钥 (使用来加密cmd类消息内容 防止前端模拟发送) 6 | rsa_public_key varchar(4000) not null default '', -- 系统公钥 7 | `version` integer not null default 0, -- 数据版本 8 | super_token varchar(40) not null default '', -- 超级token 用于操作一些系统api的安全校验 9 | super_token_on smallint not null default 0, -- 是否禁用super_token 0.禁用 1.开启 如果禁用 则一些需要super_token的API将不能使用 默认为禁用 10 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 11 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 12 | ); -------------------------------------------------------------------------------- /modules/common/sql/common-20211108-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | create table `seq`( 4 | id integer not null primary key AUTO_INCREMENT, 5 | `key` varchar(100) not null default '', -- seq的key 6 | `min_seq` bigint not null default 1000000, -- 开始序号 7 | step integer not null default 1000, -- 序号步长 每次启动后 当前序号 应该等于min_seq+ step 8 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 9 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 10 | ); 11 | CREATE UNIQUE INDEX `seq_uidx` on `seq` (`key`); -------------------------------------------------------------------------------- /modules/common/sql/common-20220916-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN revoke_second smallint not null DEFAULT 0 COMMENT '消息可撤回时长'; 4 | ALTER TABLE `app_config` ADD COLUMN welcome_message varchar(2000) not null DEFAULT '' COMMENT '登录欢迎语'; 5 | -------------------------------------------------------------------------------- /modules/common/sql/common-20220917-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN new_user_join_system_group smallint not null DEFAULT 1 COMMENT '注册用户是否默认加入系统群'; 4 | -------------------------------------------------------------------------------- /modules/common/sql/common-20221111-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN search_by_phone smallint not null DEFAULT 0 COMMENT '是否可通过手机号搜索'; 4 | -------------------------------------------------------------------------------- /modules/common/sql/common-20221114-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_version` ADD COLUMN `signature` varchar(1000) not null DEFAULT '' COMMENT '二进制包的签名'; 4 | -------------------------------------------------------------------------------- /modules/common/sql/common-20230203-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | -- app 模块管理 4 | create table `app_module`( 5 | id integer not null primary key AUTO_INCREMENT, 6 | sid varchar(40) not null default '', -- 模块ID 7 | name VARCHAR(40) not null default '', -- 模块名称 8 | `desc` varchar(100) not null default '', -- 模块介绍 9 | status smallint not null default 0, -- 模块状态 1.可用 0.不可用 10 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 11 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 12 | ); 13 | CREATE INDEX app_module_sid_idx on `app_module` (sid); 14 | 15 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('base','基础模块','app基础模块,包含基本的文本消息,图片消息,语音消息,名片消息,联系人,用户资料,个人资料,通用设置等等',2); 16 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('login','登录模块','app基础模块,包含用户登录,注册,授权pc/web登录,修改登录密码,还可以在此开发第三方登录等',2); 17 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('scan','扫一扫模块','app基础模块,扫描二维码添加好友,跳转网页等',2); 18 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('advanced','旗舰模块','高级功能,包含消息已读未读,消息点赞,截屏消息,在线状态,查看会话历史消息(非全局历史消息查询)等等',1); 19 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('groupManager','群管理模块','支持群禁言,群管理添加,群头像设置,开启群成员邀请机制等等',1); 20 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('sticker','表情商店','消息可支持矢量表情和GIF动图,用户可制作表情,添加移除表情,排序表情包等',1); 21 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('rich','富文本','聊天中支持富文本消息,包含加粗,图片,强提醒,斜线,变色,下划线,字体颜色等',1); 22 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('video','小视频模块','聊天中支持视频消息,可录视频,发送小视频消息,播放小视频',1); 23 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('map','地图模块','聊天中支持地理位置消息,发朋友圈也支持显示位置信息等',1); 24 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('file','文件模块','添加文件模块可在聊天中发送文件消息,消息搜索支持文件类型筛选,也可查看收到的文件并打开',1); 25 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('rtc','音视频模块','聊天中支持个人音视频通话,群支持会议模式等',1); 26 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('label','标签模块','将好友进行标签分组管理,可在发朋友圈时选择标签用户过滤可见或不可见等',1); 27 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('security','安全与隐私模块','手机号搜索保护,设备锁,黑名单,聊天密码设置,锁屏密码设置,禁止app截屏录屏等等',1); 28 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('customerService','客服','支持客服分配坐席演示',1); 29 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('moment','朋友圈模块','发布朋友圈,查看朋友圈,评论朋友圈,收到评论动态时显示朋友圈消息红点等等',1); 30 | INSERT INTO `app_module` (sid,name,`desc`,status) VALUES ('favorite','收藏模块','可对聊天中的文本消息,图片消息消息进行收藏,查看收藏列表信息等',1); -------------------------------------------------------------------------------- /modules/common/sql/common-20240418-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN register_invite_on smallint not null DEFAULT 0 COMMENT '是否开启注册邀请'; 4 | ALTER TABLE `app_config` ADD COLUMN send_welcome_message_on smallint not null DEFAULT 1 COMMENT '是否开启登录欢迎语'; 5 | ALTER TABLE `app_config` ADD COLUMN invite_system_account_join_group_on smallint not null DEFAULT 0 COMMENT '是否开启系统账号进入群聊'; 6 | -------------------------------------------------------------------------------- /modules/common/sql/common-20240506-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN register_user_must_complete_info_on smallint not null DEFAULT 0 COMMENT '注册用户是否必须完善信息'; 4 | -------------------------------------------------------------------------------- /modules/common/sql/common-20240510-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN channel_pinned_message_max_count smallint not null DEFAULT 10 COMMENT '频道最多置顶消息数量'; 4 | -------------------------------------------------------------------------------- /modules/common/sql/common-20240528-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `app_config` ADD COLUMN can_modify_api_url smallint not null DEFAULT 0 COMMENT '是否能修改服务器地址'; 4 | -------------------------------------------------------------------------------- /modules/file/1module.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/register" 8 | ) 9 | 10 | //go:embed swagger/api.yaml 11 | var swaggerContent string 12 | 13 | func init() { 14 | 15 | register.AddModule(func(ctx interface{}) register.Module { 16 | return register.Module{ 17 | Name: "file", 18 | SetupAPI: func() register.APIRouter { 19 | return New(ctx.(*config.Context)) 20 | }, 21 | Swagger: swaggerContent, 22 | } 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /modules/file/const.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | // Type 文件类型 4 | type Type string 5 | 6 | const ( 7 | // TypeChat 聊天文件 8 | TypeChat Type = "chat" 9 | // TypeMoment 动态文件 10 | TypeMoment Type = "moment" 11 | // TypeMomentCover 动态封面 12 | TypeMomentCover Type = "momentcover" 13 | // TypeSticker 表情 14 | TypeSticker Type = "sticker" 15 | // TypeReport 举报 16 | TypeReport Type = "report" 17 | // TypeCommon 通用 18 | TypeCommon Type = "common" 19 | // TypeChatBg 聊天背景 20 | TypeChatBg Type = "chatbg" 21 | // TypeDownload 下载文件目录 22 | TypeDownload = "download" 23 | // TypeWorkplaceBanner 24 | TypeWorkplaceBanner Type = "workplacebanner" 25 | // TypeWorkplaceAppIcon 26 | TypeWorkplaceAppIcon Type = "workplaceappicon" 27 | ) 28 | -------------------------------------------------------------------------------- /modules/file/service_oss.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/url" 7 | 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 9 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/log" 10 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type ServiceOSS struct { 15 | log.Log 16 | ctx *config.Context 17 | } 18 | 19 | // NewServiceOSS NewServiceOSS 20 | func NewServiceOSS(ctx *config.Context) *ServiceOSS { 21 | 22 | return &ServiceOSS{ 23 | Log: log.NewTLog("ServiceOSS"), 24 | ctx: ctx, 25 | } 26 | } 27 | 28 | // UploadFile 上传文件 29 | func (s *ServiceOSS) UploadFile(filePath string, contentType string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error) { 30 | ossCfg := s.ctx.GetConfig().OSS 31 | client, err := oss.New(ossCfg.Endpoint, ossCfg.AccessKeyID, ossCfg.AccessKeySecret) 32 | if err != nil { 33 | return nil, err 34 | } 35 | bucketName := s.ctx.GetConfig().OSS.BucketName 36 | // strs := strings.Split(filePath, "/") 37 | // if len(strs) > 0 { 38 | // bucketName = strs[0] 39 | // } 40 | 41 | bucket, err := client.Bucket(bucketName) 42 | if err != nil { 43 | return nil, err 44 | } 45 | if bucket == nil { 46 | err = client.CreateBucket(bucketName, oss.ACL(oss.ACLPublicRead)) 47 | if err != nil { 48 | return nil, err 49 | } 50 | bucket, err = client.Bucket(bucketName) 51 | if err != nil { 52 | return nil, err 53 | } 54 | } 55 | buff := bytes.NewBuffer(make([]byte, 0)) 56 | err = copyFileWriter(buff) 57 | if err != nil { 58 | s.Error("复制文件内容失败!", zap.Error(err)) 59 | return nil, err 60 | } 61 | err = bucket.PutObject(filePath, buff, oss.ContentType(contentType), oss.ContentLength(int64(len(buff.Bytes())))) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return map[string]interface{}{}, nil 67 | } 68 | 69 | func (s *ServiceOSS) DownloadURL(path string, filename string) (string, error) { 70 | ossCfg := s.ctx.GetConfig().OSS 71 | 72 | rpath, _ := url.JoinPath(ossCfg.BucketURL, path) 73 | return rpath, nil 74 | } 75 | -------------------------------------------------------------------------------- /modules/file/service_oss_test.go: -------------------------------------------------------------------------------- 1 | package file_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/file" 9 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 10 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/testutil" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestOSSUpload(t *testing.T) { 15 | cfg := config.New() 16 | ctx := testutil.NewTestContext(cfg) 17 | cfg.OSS.Endpoint = "oss-cn-shanghai.aliyuncs.com" 18 | cfg.OSS.AccessKeyID = "xxxx" 19 | cfg.OSS.AccessKeySecret = "xxxxxx" 20 | 21 | service := file.NewServiceOSS(ctx) 22 | _, err := service.UploadFile("chat/zdd/fjj.txt", "*", func(writer io.Writer) error { 23 | _, err := writer.Write(bytes.NewBufferString("this is test content").Bytes()) 24 | return err 25 | }) 26 | assert.NoError(t, err) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules/file/service_qiniu.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/log" 9 | "github.com/qiniu/go-sdk/v7/auth" 10 | "github.com/qiniu/go-sdk/v7/storage" 11 | "go.uber.org/zap" 12 | "io" 13 | ) 14 | 15 | type ServiceQiniu struct { 16 | log.Log 17 | ctx *config.Context 18 | } 19 | 20 | // NewServiceQiniu NewServiceQiniu 21 | func NewServiceQiniu(ctx *config.Context) *ServiceQiniu { 22 | 23 | return &ServiceQiniu{ 24 | Log: log.NewTLog("ServiceQiniu"), 25 | ctx: ctx, 26 | } 27 | } 28 | 29 | // UploadFile 上传文件 30 | func (s *ServiceQiniu) UploadFile(filePath string, contentType string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error) { 31 | 32 | qiniuCfg := s.ctx.GetConfig().Qiniu 33 | 34 | bucket := qiniuCfg.BucketName 35 | putPolicy := storage.PutPolicy{ 36 | Scope: fmt.Sprintf("%s:%s", bucket, filePath), 37 | } 38 | mac := auth.New(qiniuCfg.AccessKey, qiniuCfg.SecretKey) 39 | upToken := putPolicy.UploadToken(mac) 40 | 41 | cfg := storage.Config{} 42 | // 空间对应的机房 43 | //cfg.Region = &storage.ZoneHuabei 44 | // 是否使用https域名 45 | cfg.UseHTTPS = false 46 | // 上传是否使用CDN上传加速 47 | cfg.UseCdnDomains = false 48 | 49 | formUploader := storage.NewFormUploader(&cfg) 50 | ret := storage.PutRet{} 51 | putExtra := storage.PutExtra{ 52 | Params: map[string]string{}, 53 | } 54 | 55 | data := bytes.NewBuffer(make([]byte, 0)) 56 | err := copyFileWriter(data) 57 | if err != nil { 58 | s.Error("复制文件内容失败!", zap.Error(err)) 59 | return nil, err 60 | } 61 | dataLen := int64(len(data.Bytes())) 62 | 63 | err = formUploader.Put(context.Background(), &ret, upToken, filePath, bytes.NewReader(data.Bytes()), dataLen, &putExtra) 64 | if err != nil { 65 | s.Error("上传失败", zap.Error(err)) 66 | } 67 | fmt.Println(ret.Key, ret.Hash) 68 | return map[string]interface{}{ 69 | "path": ret.Key, 70 | }, err 71 | } 72 | 73 | func (s *ServiceQiniu) DownloadURL(path string, filename string) (string, error) { 74 | qiniuCfg := s.ctx.GetConfig().Qiniu 75 | domain := qiniuCfg.URL 76 | key := path 77 | if key[0:1] == "/" { 78 | key = key[1:] 79 | } 80 | publicAccessURL := storage.MakePublicURL(domain, key) 81 | return publicAccessURL, nil 82 | } 83 | -------------------------------------------------------------------------------- /modules/file/service_seaweedfs.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/url" 7 | "path/filepath" 8 | 9 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 10 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/log" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type SeaweedFS struct { 15 | log.Log 16 | ctx *config.Context 17 | } 18 | 19 | func NewSeaweedFS(ctx *config.Context) *SeaweedFS { 20 | return &SeaweedFS{ 21 | Log: log.NewTLog("SeaweedFS"), 22 | ctx: ctx, 23 | } 24 | } 25 | 26 | // UploadFile 上传文件 27 | func (s *SeaweedFS) UploadFile(filePath string, contentType string, copyFileWriter func(io.Writer) error) (map[string]interface{}, error) { 28 | fileDir, fileName := filepath.Split(filePath) 29 | s.Debug("filePath->", zap.String("filePath", filePath), zap.String("fileDir", fileDir), zap.String("fileName", fileName)) 30 | newFileDir := fileDir 31 | if !filepath.IsAbs(fileDir) { 32 | newFileDir = fmt.Sprintf("/%s", newFileDir) 33 | } 34 | seaweedConfig := s.ctx.GetConfig().Seaweed 35 | resultMap, err := uploadFile(fmt.Sprintf("%s%s", seaweedConfig.URL, newFileDir), fileName, copyFileWriter) 36 | return resultMap, err 37 | } 38 | 39 | func (s *SeaweedFS) DownloadURL(path string, filename string) (string, error) { 40 | seaweedConfig := s.ctx.GetConfig().Seaweed 41 | rpath, _ := url.JoinPath(seaweedConfig.URL, path) 42 | return rpath, nil 43 | } 44 | -------------------------------------------------------------------------------- /modules/file/service_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "crypto/sha512" 5 | "encoding/base64" 6 | "image/png" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestMakeCompose(t *testing.T) { 16 | s := &Service{} 17 | 18 | f1, err := os.OpenFile("../../../assets/assets/fileHelper.jpeg", os.O_RDONLY, 0777) 19 | assert.NoError(t, err) 20 | 21 | f2, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 22 | assert.NoError(t, err) 23 | 24 | f3, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 25 | assert.NoError(t, err) 26 | 27 | f4, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 28 | assert.NoError(t, err) 29 | 30 | f5, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 31 | assert.NoError(t, err) 32 | 33 | f6, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 34 | assert.NoError(t, err) 35 | 36 | f7, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 37 | assert.NoError(t, err) 38 | 39 | f8, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 40 | assert.NoError(t, err) 41 | 42 | f9, err := os.OpenFile("../../../assets/assets/u_10000.png", os.O_RDONLY, 0777) 43 | assert.NoError(t, err) 44 | 45 | img, err := s.MakeCompose([]io.ReadCloser{f1, f2, f3, f4, f5, f6, f7, f8, f9}) 46 | assert.NoError(t, err) 47 | 48 | result, err := os.OpenFile("test.png", os.O_CREATE|os.O_WRONLY, 0777) 49 | assert.NoError(t, err) 50 | err = png.Encode(result, img) 51 | assert.NoError(t, err) 52 | 53 | } 54 | 55 | func TestUploadPCFile(t *testing.T) { 56 | file, err := os.Open("../../assets/assets/TangSengDaoDao-mac-1.0.5-arm64.zip") 57 | assert.NoError(t, err) 58 | bytes, err := ioutil.ReadAll(file) 59 | assert.NoError(t, err) 60 | hash := sha512.Sum512(bytes) 61 | // hash := sha512.New().Sum(bytes[:1024*1024]) 62 | encoded := base64.StdEncoding.EncodeToString(hash[:]) 63 | println("编码结果") 64 | println(encoded) 65 | } 66 | -------------------------------------------------------------------------------- /modules/group/const.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | // 群状态 4 | const ( 5 | // GroupStatusDisabled 已禁用 6 | GroupStatusDisabled = 0 7 | // GroupStatusNormal 正常 8 | GroupStatusNormal = 1 9 | // GroupStatusDisband 解散 10 | GroupStatusDisband = 2 11 | ) 12 | 13 | // 群成员角色 14 | const ( 15 | // MemberRoleCommon 普通成员 16 | MemberRoleCommon = 0 17 | // MemberRoleCreator 创建者 18 | MemberRoleCreator = 1 19 | // MemberRoleManager 管理者 20 | MemberRoleManager = 2 21 | ) 22 | 23 | const ( 24 | // InviteStatusWait 等待确认 25 | InviteStatusWait = 0 26 | // InviteStatusOK 已确认 27 | InviteStatusOK = 1 28 | ) 29 | 30 | // 群类型 31 | type GroupType int 32 | 33 | const ( 34 | GroupTypeCommon GroupType = iota // 普通群 35 | GroupTypeSuper // 超大群 36 | ) 37 | 38 | const ( 39 | ChannelServiceName = "channel" 40 | ) 41 | -------------------------------------------------------------------------------- /modules/group/source.go: -------------------------------------------------------------------------------- 1 | package group 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/source" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // GetGroupMemberByVercode 通过vercode获取群成员 11 | func (g *Group) GetGroupMemberByVercode(vercode string) (*source.GroupMember, error) { 12 | if vercode == "" { 13 | return nil, errors.New("vercode不能为空") 14 | } 15 | model, err := g.db.queryMemberWithVercode(vercode) 16 | if err != nil { 17 | g.Error("通过vercode查询群成员错误", zap.Error(err)) 18 | return nil, err 19 | } 20 | if model == nil { 21 | return nil, nil 22 | } 23 | return &source.GroupMember{UID: model.UID, GroupNo: model.GroupNo, Vercode: model.Vercode}, nil 24 | } 25 | 26 | func (g *Group) GetGroupMemberByVercodes(vercodes []string) ([]*source.GroupMember, error) { 27 | if len(vercodes) == 0 { 28 | return nil, errors.New("vercodes不能为空") 29 | } 30 | models, err := g.db.queryMemberWithVercodes(vercodes) 31 | if err != nil { 32 | g.Error("通过vercodes查询群成员错误", zap.Error(err)) 33 | return nil, err 34 | } 35 | if models == nil { 36 | return nil, nil 37 | } 38 | members := make([]*source.GroupMember, 0, len(models)) 39 | for _, model := range models { 40 | members = append(members, &source.GroupMember{ 41 | UID: model.UID, 42 | GroupNo: model.GroupNo, 43 | Vercode: model.Vercode, 44 | Name: model.GroupName, 45 | }) 46 | } 47 | return members, nil 48 | } 49 | 50 | func (g *Group) GetGroupMemberByUID(uid string, groupNo string) (*source.GroupMember, error) { 51 | if uid == "" { 52 | return nil, errors.New("uid不能为空") 53 | } 54 | if groupNo == "" { 55 | return nil, errors.New("群编号不能为空") 56 | } 57 | model, err := g.db.QueryMemberWithUID(uid, groupNo) 58 | if err != nil { 59 | g.Error("通过用户ID查询群成员错误", zap.Error(err)) 60 | return nil, err 61 | } 62 | if model == nil { 63 | return nil, nil 64 | } 65 | return &source.GroupMember{UID: model.UID, GroupNo: model.GroupNo, Vercode: model.Vercode, Role: model.Role}, nil 66 | 67 | } 68 | 69 | // GetGroupByGroupNo 查询群详情 70 | func (g *Group) GetGroupByGroupNo(groupNo string) (*source.GroupModel, error) { 71 | if groupNo == "" { 72 | return nil, errors.New("群ID不能为空") 73 | } 74 | model, err := g.db.QueryWithGroupNo(groupNo) 75 | if err != nil { 76 | g.Error("查询群编号错误", zap.Error(err)) 77 | return nil, err 78 | } 79 | if model == nil { 80 | return nil, errors.New("群不存在") 81 | } 82 | return &source.GroupModel{ 83 | Name: model.Name, 84 | GroupNo: model.GroupNo, 85 | ForbiddenAddFriend: model.ForbiddenAddFriend, 86 | }, nil 87 | } 88 | -------------------------------------------------------------------------------- /modules/group/sql/group_20211202-02.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group_member` ADD COLUMN forbidden_expir_time integer NOT NULL DEFAULT 0 COMMENT '群成员禁言时长'; 4 | -------------------------------------------------------------------------------- /modules/group/sql/group_20220411-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group` ADD COLUMN avatar VARCHAR(255) NOT NULL DEFAULT '' COMMENT '群头像'; 4 | -------------------------------------------------------------------------------- /modules/group/sql/group_20220815-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group_setting` ADD COLUMN `flame` smallint not null default 0 COMMENT '阅后即焚是否开启 1.开启 0.未开启'; 4 | ALTER TABLE `group_setting` ADD COLUMN `flame_second` smallint not null default 0 COMMENT '阅后即焚销毁秒数'; -------------------------------------------------------------------------------- /modules/group/sql/group_20220818-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group` ADD COLUMN is_upload_avatar smallint not null DEFAULT 0 COMMENT '群头像是否已经被用户上传'; 4 | -------------------------------------------------------------------------------- /modules/group/sql/group_20220830-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group` ADD COLUMN group_type smallint not null DEFAULT 0 COMMENT '群类型 0.普通群 1.超大群'; 4 | -------------------------------------------------------------------------------- /modules/group/sql/group_20231123-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group` ADD COLUMN category VARCHAR(40) not null DEFAULT 0 COMMENT '群分类'; 4 | -------------------------------------------------------------------------------- /modules/group/sql/group_20240510-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `group` ADD COLUMN allow_member_pinned_message smallint not null DEFAULT 0 COMMENT '允许成员置顶聊天消息 0.不允许 1.允许'; 4 | -------------------------------------------------------------------------------- /modules/message/1module.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/register" 8 | ) 9 | 10 | //go:embed sql 11 | var sqlFS embed.FS 12 | 13 | //go:embed swagger/api.yaml 14 | var swaggerContent string 15 | 16 | //go:embed swagger/conversation.yaml 17 | var conversationSwagger string 18 | 19 | func init() { 20 | 21 | register.AddModule(func(ctx interface{}) register.Module { 22 | 23 | return register.Module{ 24 | Name: "message", 25 | SetupAPI: func() register.APIRouter { 26 | return New(ctx.(*config.Context)) 27 | }, 28 | SQLDir: register.NewSQLFS(sqlFS), 29 | Swagger: swaggerContent, 30 | } 31 | }) 32 | 33 | register.AddModule(func(ctx interface{}) register.Module { 34 | 35 | return register.Module{ 36 | Name: "conversation", 37 | SetupAPI: func() register.APIRouter { 38 | return NewConversation(ctx.(*config.Context)) 39 | }, 40 | Swagger: conversationSwagger, 41 | } 42 | }) 43 | register.AddModule(func(ctx interface{}) register.Module { 44 | 45 | return register.Module{ 46 | SetupAPI: func() register.APIRouter { 47 | return NewManager(ctx.(*config.Context)) 48 | }, 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /modules/message/api_vo.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type deleteReq struct { 9 | MessageID string `json:"message_id"` 10 | ChannelID string `json:"channel_id"` 11 | ChannelType uint8 `json:"channel_type"` 12 | MessageSeq uint32 `json:"message_seq"` 13 | } 14 | 15 | func (d *deleteReq) check() error { 16 | if strings.TrimSpace(d.MessageID) == "" { 17 | return errors.New("消息ID不能为空!") 18 | } 19 | if strings.TrimSpace(d.ChannelID) == "" { 20 | return errors.New("频道ID不能为空!") 21 | } 22 | if d.ChannelType == 0 { 23 | return errors.New("频道类型不能为空!") 24 | } 25 | if d.MessageSeq == 0 { 26 | return errors.New("消息序号不能为空!") 27 | } 28 | return nil 29 | } 30 | 31 | type voiceReadedReq struct { 32 | deleteReq 33 | } 34 | -------------------------------------------------------------------------------- /modules/message/common.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | -------------------------------------------------------------------------------- /modules/message/const.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | const ( 4 | // 消息已删除 5 | CMDMessageDeleted = "messageDeleted" 6 | // CMDMessageErase 消息擦除 7 | CMDMessageErase = "messageEerase" 8 | sensitiveWordsVersion = 1 9 | ) 10 | const CacheReadedCountPrefix = "readedCount:" // 消息已读数量 11 | 12 | type ReminderType int 13 | 14 | const ( 15 | ReminderTypeMentionMe = 1 // 有人@我 16 | ReminderTypeApplyJoinGroup = 2 // 申请加群 17 | ) 18 | 19 | var sensitive_words = []string{ 20 | "银行卡", 21 | "微信", 22 | "qq", 23 | "密码", 24 | "支付宝", 25 | "钱包", 26 | "转账", 27 | "彩票", 28 | "股票", 29 | "人民币", 30 | "RMB", 31 | "兼职", 32 | "网络", 33 | "招聘", 34 | "有意者", 35 | "到货", 36 | "本店", 37 | "代购", 38 | "扣扣", 39 | "微店", 40 | "兼值", 41 | "淘宝", 42 | "小姐", 43 | "妓女", 44 | "包夜", 45 | "3P", 46 | "LY", 47 | "JS", 48 | "狼友", 49 | "技师", 50 | "推油", 51 | "胸推", 52 | "BT", 53 | "毒龙", 54 | "口爆", 55 | "楼凤", 56 | "足交", 57 | "口暴", 58 | "口交", 59 | "全套", 60 | "SM", 61 | "桑拿", 62 | "吞精", 63 | "咪咪", 64 | "婊子", 65 | "乳方", 66 | "操逼", 67 | "全职", 68 | "性伴侣", 69 | "网购", 70 | "网络工作", 71 | "代理", 72 | "专业代理", 73 | "帮忙点一下", 74 | "帮忙点下", 75 | "请点击进入", 76 | "详情请进入", 77 | "私人侦探", 78 | "私家侦探", 79 | "针孔摄象", 80 | "调查婚外情", 81 | "信用卡提现", 82 | "无抵押贷款", 83 | "广告代理", 84 | "原音铃声", 85 | "借腹生子", 86 | "找个妈妈", 87 | "找个爸爸", 88 | "代孕妈妈", 89 | "代生孩子", 90 | "代开发票", 91 | "腾讯客服电话", 92 | "销售热线", 93 | "免费订购热线", 94 | "低价出售", 95 | "款到发货", 96 | "回复可见", 97 | "连锁加盟", 98 | "加盟连锁", 99 | "免费二级域名", 100 | "免费使用", 101 | "免费索取", 102 | "蚁力神", 103 | "婴儿汤", 104 | "售肾", 105 | "刻章办", 106 | "买小车", 107 | "套牌车", 108 | "玛雅网", 109 | "电脑传讯", 110 | "视频来源", 111 | "下载速度", 112 | "高清在线", 113 | "全集在线", 114 | "在线播放", 115 | "txt下载", 116 | "六位qq", 117 | "6位qq", 118 | "位的qq", 119 | "个qb", 120 | "送qb", 121 | "用刀横向切腹", 122 | "完全自杀手册", 123 | "四海帮", 124 | "足球投注", 125 | "地下钱庄", 126 | "中国复兴党", 127 | "阿波罗网", 128 | "曾道人", 129 | "六合彩", 130 | "改卷内幕", 131 | "替考试", 132 | "隐形耳机", 133 | "出售答案", 134 | "考中答案", 135 | "答an", 136 | "da案", 137 | "资金周转", 138 | "救市", 139 | "股市圈钱", 140 | "崩盘", 141 | "资金短缺", 142 | "证监会", 143 | "质押贷款", 144 | "小额贷款", 145 | "周小川", 146 | "刘明康", 147 | "尚福林", 148 | "孔丹", 149 | } 150 | -------------------------------------------------------------------------------- /modules/message/db_channel_offset.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "hash/crc32" 6 | 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 9 | "github.com/gocraft/dbr/v2" 10 | ) 11 | 12 | type channelOffsetDB struct { 13 | ctx *config.Context 14 | session *dbr.Session 15 | } 16 | 17 | func newChannelOffsetDB(ctx *config.Context) *channelOffsetDB { 18 | return &channelOffsetDB{ 19 | ctx: ctx, 20 | session: ctx.DB(), 21 | } 22 | } 23 | 24 | func (c *channelOffsetDB) insertOrUpdate(m *channelOffsetModel) error { 25 | sq := fmt.Sprintf("INSERT INTO %s (uid,channel_id,channel_type,message_seq) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE message_seq=IF(message_seqbrowse_to,VALUES(browse_to),browse_to),`keep_message_seq`=VALUES(`keep_message_seq`),keep_offset_y=VALUES(keep_offset_y),draft=VALUES(draft),version=VALUES(version)", model.UID, model.ChannelID, model.ChannelType, model.BrowseTo, model.KeepMessageSeq, model.KeepOffsetY, model.Draft, model.Version).Exec() 24 | return err 25 | } 26 | 27 | func (c *conversationExtraDB) sync(uid string, version int64) ([]*conversationExtraModel, error) { 28 | var models []*conversationExtraModel 29 | _, err := c.session.Select("*").From("conversation_extra").Where("uid=? and version>?", uid, version).Load(&models) 30 | return models, err 31 | } 32 | 33 | func (c *conversationExtraDB) queryWithChannelIDs(uid string, channelIDs []string) ([]*conversationExtraModel, error) { 34 | if len(channelIDs) == 0 { 35 | return nil, nil 36 | } 37 | var models []*conversationExtraModel 38 | _, err := c.session.Select("*").From("conversation_extra").Where("uid=? and channel_id in ?", uid, channelIDs).Load(&models) 39 | return models, err 40 | } 41 | 42 | type conversationExtraModel struct { 43 | UID string 44 | ChannelID string 45 | ChannelType uint8 46 | BrowseTo uint32 47 | KeepMessageSeq uint32 48 | KeepOffsetY int 49 | Draft string // 草稿 50 | Version int64 51 | db.BaseModel 52 | } 53 | -------------------------------------------------------------------------------- /modules/message/db_device_offset.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 5 | "github.com/gocraft/dbr/v2" 6 | ) 7 | 8 | type deviceOffsetDB struct { 9 | session *dbr.Session 10 | } 11 | 12 | func newDeviceOffsetDB(session *dbr.Session) *deviceOffsetDB { 13 | return &deviceOffsetDB{ 14 | session: session, 15 | } 16 | } 17 | 18 | func (d *deviceOffsetDB) insertOrUpdateTx(tx *dbr.Tx, model *deviceOffsetModel) error { 19 | sq := "INSERT INTO device_offset (uid,device_uuid,channel_id,channel_type,message_seq) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE message_seq=IF(message_seq?", channelID, channelType, seq).OrderAsc("seq").Limit(limit).Load(&list) 30 | } 31 | return list, err 32 | } 33 | 34 | func (d *messageReactionDB) queryWithMessageIDs(messageIDs []string) ([]*reactionModel, error) { 35 | if len(messageIDs) <= 0 { 36 | return nil, nil 37 | } 38 | var models []*reactionModel 39 | _, err := d.session.Select("*").From("reaction_users").Where("message_id in ?", messageIDs).Load(&models) 40 | return models, err 41 | } 42 | 43 | // 查询某个用户的回应数据 44 | func (d *messageReactionDB) queryReactionWithUIDAndMessageID(uid string, messageID string) (*reactionModel, error) { 45 | var model *reactionModel 46 | _, err := d.session.Select("*").From("reaction_users").Where("uid=? and message_id=?", uid, messageID).Load(&model) 47 | return model, err 48 | } 49 | 50 | // 新增回应 51 | func (d *messageReactionDB) insertReaction(model *reactionModel) error { 52 | _, err := d.session.InsertInto("reaction_users").Columns(util.AttrToUnderscore(model)...).Record(model).Exec() 53 | return err 54 | } 55 | 56 | // 修改某条消息的回应 57 | func (d *messageReactionDB) updateReactionStatus(model *reactionModel) error { 58 | _, err := d.session.Update("reaction_users").SetMap(map[string]interface{}{ 59 | "is_deleted": model.IsDeleted, 60 | "seq": model.Seq, 61 | "emoji": model.Emoji, 62 | }).Where("message_id=? and uid=?", model.MessageID, model.UID).Exec() 63 | return err 64 | } 65 | 66 | type reactionModel struct { 67 | MessageID string // 消息唯一ID 68 | Seq int64 // 回复序列号 69 | ChannelID string // 频道唯一ID 70 | ChannelType uint8 // 频道类型 71 | UID string // 用户ID 72 | Name string // 用户名称 73 | Emoji string // 回应表情 74 | IsDeleted int // 是否已删除 75 | db.BaseModel 76 | } 77 | -------------------------------------------------------------------------------- /modules/message/db_message_user_extra.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "fmt" 5 | "hash/crc32" 6 | 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 9 | "github.com/gocraft/dbr/v2" 10 | ) 11 | 12 | type messageUserExtraDB struct { 13 | ctx *config.Context 14 | session *dbr.Session 15 | } 16 | 17 | func newMessageUserExtraDB(ctx *config.Context) *messageUserExtraDB { 18 | return &messageUserExtraDB{ 19 | ctx: ctx, 20 | session: ctx.DB(), 21 | } 22 | } 23 | 24 | func (m *messageUserExtraDB) insertOrUpdateDeletedTx(md *messageUserExtraModel, tx *dbr.Tx) error { 25 | sq := fmt.Sprintf("INSERT INTO %s (uid,message_id,message_seq,channel_id,channel_type,message_is_deleted) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE message_is_deleted=VALUES(message_is_deleted)", m.getTable(md.UID)) 26 | _, err := tx.InsertBySql(sq, md.UID, md.MessageID, md.MessageSeq, md.ChannelID, md.ChannelType, md.MessageIsDeleted).Exec() 27 | return err 28 | } 29 | 30 | // 插入或更新消息语音已读状态 31 | func (m *messageUserExtraDB) insertOrUpdateVoiceRead(md *messageUserExtraModel) error { 32 | sq := fmt.Sprintf("INSERT INTO %s (uid,message_id,message_seq,channel_id,channel_type,voice_readed) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE voice_readed=VALUES(voice_readed)", m.getTable(md.UID)) 33 | _, err := m.session.InsertBySql(sq, md.UID, md.MessageID, md.MessageSeq, md.ChannelID, md.ChannelType, md.VoiceReaded).Exec() 34 | return err 35 | } 36 | 37 | // 通过消息id集合和消息拥有者uid查询编辑消息 38 | func (m *messageUserExtraDB) queryWithMessageIDsAndUID(messageIDs []string, uid string) ([]*messageUserExtraModel, error) { 39 | if len(messageIDs) == 0 { 40 | return nil, nil 41 | } 42 | var models []*messageUserExtraModel 43 | _, err := m.session.Select("*").From(m.getTable(uid)).Where("uid=? and message_id in ?", uid, messageIDs).Load(&models) 44 | return models, err 45 | } 46 | 47 | func (m *messageUserExtraDB) getTable(uid string) string { 48 | tableIndex := crc32.ChecksumIEEE([]byte(uid)) % uint32(m.ctx.GetConfig().TablePartitionConfig.MessageUserEditTableCount) 49 | if tableIndex == 0 { 50 | return "message_user_extra" 51 | } 52 | return fmt.Sprintf("message_user_extra%d", tableIndex) 53 | } 54 | 55 | type messageUserExtraModel struct { 56 | UID string 57 | MessageID string 58 | MessageSeq uint32 59 | ChannelID string 60 | ChannelType uint8 61 | VoiceReaded int 62 | MessageIsDeleted int 63 | db.BaseModel 64 | } 65 | -------------------------------------------------------------------------------- /modules/message/db_user_last_offset.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | type userLastOffsetDB struct { 10 | ctx *config.Context 11 | session *dbr.Session 12 | } 13 | 14 | func newUserLastOffsetDB(ctx *config.Context) *userLastOffsetDB { 15 | 16 | return &userLastOffsetDB{ 17 | ctx: ctx, 18 | session: ctx.DB(), 19 | } 20 | } 21 | 22 | func (d *userLastOffsetDB) insertOrUpdateTx(tx *dbr.Tx, model *userLastOffsetModel) error { 23 | sq := "INSERT INTO user_last_offset (uid,channel_id,channel_type,message_seq) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE message_seq=IF(message_seq 0 { 58 | for index, device := range devices { 59 | var selft int 60 | if index == 0 { 61 | selft = 1 62 | } 63 | deviceName := device.DeviceName 64 | if selft == 1 { 65 | deviceName = fmt.Sprintf("%s(本机)", device.DeviceName) 66 | } 67 | deviceResps = append(deviceResps, deviceResp{ 68 | ID: device.Id, 69 | DeviceID: device.DeviceID, 70 | DeviceName: deviceName, 71 | DeviceModel: device.DeviceModel, 72 | Self: selft, 73 | LastLogin: util.ToyyyyMMddHHmm(time.Unix(device.LastLogin, 0)), 74 | }) 75 | } 76 | } 77 | c.Response(deviceResps) 78 | } 79 | 80 | type deviceResp struct { 81 | ID int64 `json:"id"` 82 | DeviceID string `json:"device_id"` // 设备ID 83 | DeviceName string `json:"device_name"` // 设备名称 84 | DeviceModel string `json:"device_model"` // 设备型号 85 | LastLogin string `json:"last_login"` // 设备最后一次登录时间 86 | Self int `json:"self"` // 是否是本机 87 | } 88 | -------------------------------------------------------------------------------- /modules/user/api_login_log.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/log" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | // LoginLog 用户设置 10 | type LoginLog struct { 11 | ctx *config.Context 12 | log.Log 13 | loginLogDB *LoginLogDB 14 | } 15 | 16 | // NewLoginLog 创建 17 | func NewLoginLog(ctx *config.Context) *LoginLog { 18 | return &LoginLog{ctx: ctx, Log: log.NewTLog("loginLog"), loginLogDB: NewLoginLogDB(ctx.DB())} 19 | } 20 | 21 | // add 添加登录日志 22 | func (l *LoginLog) add(uid string, publicIP string) { 23 | err := l.loginLogDB.insert(&LoginLogModel{ 24 | UID: uid, 25 | LoginIP: publicIP, 26 | }) 27 | if err != nil { 28 | l.Error("添加登录日志错误", zap.Error(err)) 29 | } 30 | } 31 | 32 | // getLastLoginIp 获取最后一次登录ip 33 | func (l *LoginLog) getLastLoginIP(uid string) *loginLogResp { 34 | model, err := l.loginLogDB.queryLastLoginIP(uid) 35 | if err != nil { 36 | l.Error("查询登录日志错误", zap.Error(err)) 37 | return nil 38 | } 39 | if model != nil { 40 | return &loginLogResp{ 41 | UID: model.UID, 42 | CreateAt: model.CreatedAt.String(), 43 | LoginIP: model.LoginIP, 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | // loginLogResp 登录日志 50 | type loginLogResp struct { 51 | UID string 52 | CreateAt string 53 | LoginIP string 54 | } 55 | -------------------------------------------------------------------------------- /modules/user/const.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | const ( 4 | // Web3VerifyLogin 校验登录 5 | Web3VerifyLogin string = "login" 6 | // Web3VerifyPassword 校验密码 7 | Web3VerifyPassword string = "password" 8 | ) 9 | 10 | // Status 状态 11 | type Status int 12 | 13 | const ( 14 | // StatusDisable 禁用 15 | StatusDisable Status = iota 16 | // StatusEnable 启用 17 | StatusEnable 18 | ) 19 | const ( 20 | // CacheKeyFriends 好友key 21 | CacheKeyFriends string = "lm-friends:" 22 | ) 23 | 24 | // Int Int 25 | func (s Status) Int() int { 26 | return int(s) 27 | } 28 | 29 | type Category string 30 | 31 | const ( 32 | // CategoryCustomerService 客服 33 | CategoryCustomerService = "customerService" 34 | // CategorySystem 系统账号 35 | CategorySystem = "system" 36 | ) 37 | 38 | // Names 注册用户随机名字 39 | var Names = []string{"龚都", "黄祖", "黄祖", "黄皓", "黄琬", "黄歇", "黄权", "公孙瓒", 40 | "袁绍", "张角", "李儒", "高顺", "马腾", "文丑", "华雄", "颜良", "华佗", 41 | "左慈", "貂蝉", "司马徽", "蔡文姬", "胡车儿", "逢纪", "纪灵", "张绣", "孔融", "张鲁", 42 | "韩遂", "张燕", "张曼成", "审配", "黄甫嵩", "张梁", "张任", "马铁", "沪指", "辟暑大王", 43 | "辟尘大王", "玄鹤老", "玉兔精", "蠹妖", "蛙怪", "麋妖", "古柏老", "灵龟老", "峰五老", "赤蛇精", "虺妖", "蚖妖", 44 | "蝮子怪", "蝎小妖", "狐妖", "凤管娘子", "鸾萧夫人", "七情大王", "六欲大王", "三尸魔王", "阴沉魔王", "独角魔王", 45 | "啸风魔王", "兴云魔王", "六耳魔王", "迷识魔王", "消阳魔王", "铄阴魔王", "耗气魔王", "黑鱼精", "蜂妖", "灵鹊", 46 | "玄武灵", "美蔚君", "福缘君", "善庆君", "孟浪魔王", "慌张魔王", "司视魔", "司听魔", "逐香魔", "具体魔", "驰神魔", 47 | "逐味魔", "千里眼", "顺风耳", "金童", "玉女", "雷公", "电母", "风伯", "雨师", "游奕灵官", "翊圣真君", "大力鬼王", 48 | "七仙女", "太白金星", "赤脚大仙", "嫦娥", "玉兔", "吴刚", "猪八戒", "孙悟空", "唐僧", "沙悟净", "白龙马", "九天玄女", 49 | "九曜星", "日游神", "夜游神", "太阴星君", "太阳星君", "武德星君", "佑圣真君", "李靖", "金吒", "木吒", "哪吒", 50 | "巨灵神", "月老", "左辅右弼", "二郎神杨戬", "萨真人", "文昌帝君", "增长天王", "持国天王", "多闻天王", "广目天王", 51 | "张道陵", "许逊", "邱弘济", "葛洪", "渔人", "林黛玉", "薛宝钗", "贾宝玉", "秦可卿", "贾巧姐", "王熙凤", "史湘云", 52 | "妙玉", "李纨", "贾惜春", "贾探春", "贾迎春", "贾元春", "王妈妈", "西门庆", "武松", "武大郎", "宋江", "鲁智深", 53 | "高俅", "闻太师", "卢俊义", "吴用", "公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "阮小七", "燕青", 54 | "皇甫端", "扈三娘", "王英", "安道全", "金大坚", "萧峰", "段誉", "童猛", "陶宗旺", "郑天寿", "王定六", "段景住", 55 | "寅将军", "黑熊精", "白衣秀士", "凌虚子", "黄风怪", "白骨精", "奎木狼", "金角大王", "银角大王", 56 | } 57 | 58 | const ( 59 | ChannelServiceName = "channel" 60 | ) 61 | 62 | const ( 63 | UserRedDotCategoryFriendApply = "friendApply" 64 | ) 65 | -------------------------------------------------------------------------------- /modules/user/db_device_flag.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | type deviceFlagDB struct { 10 | session *dbr.Session 11 | ctx *config.Context 12 | } 13 | 14 | func newDeviceFlagDB(ctx *config.Context) *deviceFlagDB { 15 | return &deviceFlagDB{ 16 | session: ctx.DB(), 17 | ctx: ctx, 18 | } 19 | } 20 | 21 | func (d *deviceFlagDB) queryAll() ([]*deviceFlagModel, error) { 22 | var deviceFlags []*deviceFlagModel 23 | _, err := d.session.Select("*").From("device_flag").Load(&deviceFlags) 24 | return deviceFlags, err 25 | } 26 | 27 | type deviceFlagModel struct { 28 | DeviceFlag uint8 29 | Weight int 30 | Remark string 31 | db.BaseModel 32 | } 33 | -------------------------------------------------------------------------------- /modules/user/db_gitee.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 7 | "github.com/gocraft/dbr/v2" 8 | ) 9 | 10 | type giteeDB struct { 11 | session *dbr.Session 12 | ctx *config.Context 13 | } 14 | 15 | func newGiteeDB(ctx *config.Context) *giteeDB { 16 | 17 | return &giteeDB{ 18 | ctx: ctx, 19 | session: ctx.DB(), 20 | } 21 | } 22 | 23 | func (d *giteeDB) insert(m *gitUserInfoModel) error { 24 | _, err := d.session.InsertInto("gitee_user").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 25 | return err 26 | } 27 | 28 | func (d *giteeDB) insertTx(m *gitUserInfoModel, tx *dbr.Tx) error { 29 | _, err := tx.InsertInto("gitee_user").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 30 | return err 31 | } 32 | func (d *giteeDB) queryWithLogin(login string) (*gitUserInfoModel, error) { 33 | var m *gitUserInfoModel 34 | _, err := d.session.Select("*").From("gitee_user").Where("login=?", login).Load(&m) 35 | return m, err 36 | } 37 | 38 | type gitUserInfoModel struct { 39 | Id int64 40 | CreatedAt db.Time 41 | UpdatedAt db.Time 42 | Login string 43 | Name string 44 | Email string 45 | Bio string 46 | AvatarURL string 47 | Blog string 48 | EventsURL string 49 | Followers int 50 | FollowersURL string 51 | Following int 52 | FollowingURL string 53 | GistsURL string 54 | HtmlURL string 55 | MemberRole string 56 | OrganizationsURL string 57 | PublicGists int 58 | PublicRepos int 59 | ReceivedEventsURL string 60 | Remark string 61 | ReposURL string 62 | Stared int 63 | StarredURL string 64 | SubscriptionsURL string 65 | URL string 66 | Watched int 67 | Weibo string 68 | Type string 69 | GiteeCreatedAt string 70 | GiteeUpdatedAt string 71 | } 72 | -------------------------------------------------------------------------------- /modules/user/db_github.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 7 | "github.com/gocraft/dbr/v2" 8 | ) 9 | 10 | type githubDB struct { 11 | session *dbr.Session 12 | ctx *config.Context 13 | } 14 | 15 | func newGithubDB(ctx *config.Context) *githubDB { 16 | 17 | return &githubDB{ 18 | ctx: ctx, 19 | session: ctx.DB(), 20 | } 21 | } 22 | 23 | func (d *githubDB) insert(m *githubUserInfoModel) error { 24 | _, err := d.session.InsertInto("github_user").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 25 | return err 26 | } 27 | 28 | func (d *githubDB) insertTx(m *githubUserInfoModel, tx *dbr.Tx) error { 29 | _, err := tx.InsertInto("github_user").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 30 | return err 31 | } 32 | func (d *githubDB) queryWithLogin(login string) (*githubUserInfoModel, error) { 33 | var m *githubUserInfoModel 34 | _, err := d.session.Select("*").From("github_user").Where("login=?", login).Load(&m) 35 | return m, err 36 | } 37 | 38 | type githubUserInfoModel struct { 39 | ID int64 40 | CreatedAt db.Time 41 | UpdatedAt db.Time 42 | Login string 43 | NodeID string 44 | AvatarURL string 45 | GravatarID string 46 | URL string 47 | HtmlUrl string 48 | FollowersURL string 49 | FollowingURL string 50 | GistsURL string 51 | StarredURL string 52 | SubscriptionsURL string 53 | OrganizationsURL string 54 | ReposURL string 55 | EventsURL string 56 | ReceivedEventsURL string 57 | Type string 58 | SiteAdmin bool 59 | Name string 60 | Company string 61 | Blog string 62 | Location string 63 | Email string 64 | Hireable bool 65 | Bio string 66 | TwitterUsername string 67 | PublicRepos int 68 | PublicGists int 69 | Followers int 70 | Following int 71 | GithubCreatedAt string 72 | GithubUpdatedAt string 73 | PrivateGists int 74 | TotalPrivateRepos int 75 | OwnedPrivateRepos int 76 | DiskUsage int 77 | Collaborators int 78 | TwoFactorAuthentication bool 79 | } 80 | -------------------------------------------------------------------------------- /modules/user/db_identities.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | type identitieDB struct { 10 | session *dbr.Session 11 | ctx *config.Context 12 | } 13 | 14 | func newIdentitieDB(ctx *config.Context) *identitieDB { 15 | return &identitieDB{ 16 | session: ctx.DB(), 17 | ctx: ctx, 18 | } 19 | } 20 | 21 | func (i *identitieDB) saveOrUpdateTx(m *identitiesModel, tx *dbr.Tx) error { 22 | _, err := tx.InsertBySql("insert into signal_identities(uid,identity_key,signed_prekey_id,signed_pubkey,signed_signature,registration_id) values(?,?,?,?,?,?) ON DUPLICATE KEY UPDATE identity_key=identity_key,signed_prekey_id=signed_prekey_id,signed_pubkey=signed_pubkey,signed_signature=signed_signature,registration_id=registration_id", m.UID, m.IdentityKey, m.SignedPrekeyID, m.SignedPubkey, m.SignedSignature, m.RegistrationID).Exec() 23 | return err 24 | } 25 | 26 | func (i *identitieDB) deleteWithUID(uid string) error { 27 | _, err := i.session.DeleteFrom("signal_identities").Where("uid=?", uid).Exec() 28 | return err 29 | } 30 | 31 | func (i *identitieDB) queryWithUID(uid string) (*identitiesModel, error) { 32 | var model *identitiesModel 33 | _, err := i.session.Select("*").From("signal_identities").Where("uid=?", uid).Load(&model) 34 | return model, err 35 | } 36 | 37 | type identitiesModel struct { 38 | UID string 39 | RegistrationID uint32 40 | IdentityKey string 41 | SignedPrekeyID int 42 | SignedPubkey string 43 | SignedSignature string 44 | db.BaseModel 45 | } 46 | -------------------------------------------------------------------------------- /modules/user/db_login_log.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | // LoginLogDB 登录日志DB 10 | type LoginLogDB struct { 11 | session *dbr.Session 12 | } 13 | 14 | // NewLoginLogDB NewDB 15 | func NewLoginLogDB(session *dbr.Session) *LoginLogDB { 16 | return &LoginLogDB{ 17 | session: session, 18 | } 19 | } 20 | 21 | // insert 添加登录日志 22 | func (l *LoginLogDB) insert(m *LoginLogModel) error { 23 | _, err := l.session.InsertInto("login_log").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 24 | return err 25 | } 26 | 27 | // queryLastLoginIP 查询最后一次登录日志 28 | func (l *LoginLogDB) queryLastLoginIP(uid string) (*LoginLogModel, error) { 29 | var model *LoginLogModel 30 | _, err := l.session.Select("*").From("login_log").Where("uid=?", uid).OrderDir("created_at", false).Limit(1).Load(&model) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return model, nil 35 | } 36 | 37 | // LoginLogModel 登录日志 38 | type LoginLogModel struct { 39 | LoginIP string //登录IP 40 | UID string 41 | db.BaseModel 42 | } 43 | -------------------------------------------------------------------------------- /modules/user/db_maillist.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/gocraft/dbr/v2" 7 | ) 8 | 9 | type maillistDB struct { 10 | session *dbr.Session 11 | ctx *config.Context 12 | } 13 | 14 | func newMaillistDB(ctx *config.Context) *maillistDB { 15 | return &maillistDB{ 16 | ctx: ctx, 17 | session: ctx.DB(), 18 | } 19 | } 20 | 21 | func (d *maillistDB) insertTx(m *maillistModel, tx *dbr.Tx) error { 22 | _, err := tx.InsertBySql("INSERT INTO user_maillist (zone,phone,name,vercode,uid) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `phone`=VALUES(`phone`)", m.Zone, m.Phone, m.Name, m.Vercode, m.UID).Exec() 23 | return err 24 | } 25 | 26 | func (d *maillistDB) queryWitchVercode(vercode string) (*maillistModel, error) { 27 | var model *maillistModel 28 | _, err := d.session.Select("*").From("user_maillist").Where("vercode=? ", vercode).Load(&model) 29 | return model, err 30 | } 31 | func (d *maillistDB) query(uid string) ([]*maillistModel, error) { 32 | var models []*maillistModel 33 | _, err := d.session.Select("*").From("user_maillist").Where("uid=?", uid).Load(&models) 34 | return models, err 35 | } 36 | 37 | type maillistModel struct { 38 | UID string 39 | Phone string 40 | Zone string 41 | Name string 42 | Vercode string 43 | db.BaseModel 44 | } 45 | -------------------------------------------------------------------------------- /modules/user/db_onetime_prekeys.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/util" 7 | "github.com/gocraft/dbr/v2" 8 | ) 9 | 10 | type onetimePrekeysDB struct { 11 | session *dbr.Session 12 | ctx *config.Context 13 | } 14 | 15 | func newOnetimePrekeysDB(ctx *config.Context) *onetimePrekeysDB { 16 | return &onetimePrekeysDB{ 17 | session: ctx.DB(), 18 | ctx: ctx, 19 | } 20 | } 21 | 22 | func (o *onetimePrekeysDB) insertTx(m *onetimePrekeysModel, tx *dbr.Tx) error { 23 | _, err := tx.InsertInto("signal_onetime_prekeys").Columns(util.AttrToUnderscore(m)...).Record(m).Exec() 24 | return err 25 | } 26 | 27 | func (o *onetimePrekeysDB) delete(uid string, keyID int) error { 28 | _, err := o.session.DeleteFrom("signal_onetime_prekeys").Where("uid=? and key_id=?", uid, keyID).Exec() 29 | return err 30 | } 31 | 32 | func (o *onetimePrekeysDB) deleteWithUID(uid string) error { 33 | _, err := o.session.DeleteFrom("signal_onetime_prekeys").Where("uid=?", uid).Exec() 34 | return err 35 | } 36 | 37 | // 查询用户最小的onetimePreKey 38 | func (o *onetimePrekeysDB) queryMinWithUID(uid string) (*onetimePrekeysModel, error) { 39 | var m *onetimePrekeysModel 40 | _, err := o.session.Select("*").From("signal_onetime_prekeys").Where("uid=?", uid).OrderAsc("key_id").Limit(1).Load(&m) 41 | return m, err 42 | } 43 | 44 | func (o *onetimePrekeysDB) queryCount(uid string) (int, error) { 45 | var cn int 46 | err := o.session.Select("count(*)").From("signal_onetime_prekeys").Where("uid=?", uid).LoadOne(&cn) 47 | return cn, err 48 | } 49 | 50 | type onetimePrekeysModel struct { 51 | UID string 52 | KeyID int 53 | Pubkey string 54 | db.BaseModel 55 | } 56 | -------------------------------------------------------------------------------- /modules/user/sql/user-20210204-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `user` ADD COLUMN app_id VARCHAR(40) NOT NULL DEFAULT '' COMMENT 'app id'; -------------------------------------------------------------------------------- /modules/user/sql/user-20210405-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `user` ADD COLUMN email VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'email地址'; -------------------------------------------------------------------------------- /modules/user/sql/user-20210413-01.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- +migrate Up 4 | 5 | -- 消息表 6 | create table `user_online` 7 | ( 8 | id bigint not null primary key AUTO_INCREMENT, 9 | uid VARCHAR(40) not null default '', -- 用户uid 10 | device_flag smallint not null default 0, -- 设备flag 0.APP 1. WEB 11 | last_online integer not null DEFAULT 0, -- 最后一次在线时间 12 | last_offline integer not null DEFAULT 0, -- 最后一次离线时间 13 | online tinyint(1) not null default 0, -- 用户是否在线 14 | `version` bigint not null default 0, -- 数据版本 15 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 16 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 17 | ); 18 | 19 | CREATE UNIQUE INDEX `uid_device` on `user_online` (`uid`,device_flag); 20 | 21 | CREATE INDEX `online_idx` on `user_online` (`online`); 22 | CREATE INDEX `uid_idx` on `user_online` (`uid`); -------------------------------------------------------------------------------- /modules/user/sql/user-20210907-01.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- +migrate Up 4 | 5 | -- 用户身份表 (signal protocol使用) 6 | create table `signal_identities` 7 | ( 8 | id bigint not null primary key AUTO_INCREMENT, 9 | uid varchar(40) not null DEFAULT '', -- 用户uid 10 | registration_id bigint not null DEFAULT 0, -- 身份ID 11 | identity_key text not null, -- 用户身份公钥 12 | signed_prekey_id integer not null DEFAULT 0, -- 签名key的id 13 | signed_pubkey text not null, -- 签名key的公钥 14 | signed_signature text not null, -- 由身份密钥签名的signed_pubkey 15 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 16 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 17 | ); 18 | 19 | CREATE UNIQUE INDEX identities_index_id ON signal_identities(uid); 20 | 21 | 22 | -- 一次性公钥 23 | create table `signal_onetime_prekeys` 24 | ( 25 | id bigint not null primary key AUTO_INCREMENT, 26 | uid varchar(40) not null DEFAULT '', -- 用户uid 27 | key_id integer not null DEFAULT 0, 28 | pubkey text not null, -- 公钥 29 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 30 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 31 | ); 32 | CREATE UNIQUE INDEX key_id_uid_index_id ON signal_onetime_prekeys(uid,key_id); -------------------------------------------------------------------------------- /modules/user/sql/user-20210916-01.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- +migrate Up 4 | 5 | -- 手机联系人 6 | create table `user_maillist` 7 | ( 8 | id bigint not null primary key AUTO_INCREMENT, 9 | uid VARCHAR(40) not null default '', -- 用户uid 10 | phone VARCHAR(40) not null default '', -- 手机号 11 | zone VARCHAR(40) not null default '', -- 区号 12 | name VARCHAR(40) not null default '', -- 名字 13 | vercode VARCHAR(100) not null default '', -- 验证码 加好友来源 14 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 15 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 16 | ); 17 | 18 | CREATE UNIQUE INDEX `uid_maillist_index` on `user_maillist` (`uid`,`zone`,`phone`); -------------------------------------------------------------------------------- /modules/user/sql/user-20211115-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | CREATE unique INDEX to_uid_uid on `user_setting` (uid, to_uid); 4 | 5 | CREATE unique INDEX to_uid_uid on `friend` (uid, to_uid); -------------------------------------------------------------------------------- /modules/user/sql/user-20220222-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `user` ADD COLUMN is_destroy smallint not null default 0 COMMENT '是否已销毁'; 4 | ALTER TABLE `user` MODIFY COLUMN zone VARCHAR(20); 5 | ALTER TABLE `user` MODIFY COLUMN phone VARCHAR(100); 6 | -------------------------------------------------------------------------------- /modules/user/sql/user-20220609-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `user_setting` ADD COLUMN remark VARCHAR(100) NOT NULL DEFAULT '' COMMENT '用户备注'; 4 | 5 | -- 迁移备注数据 6 | insert into user_setting(uid,to_uid,remark) select uid,to_uid,remark from friend where remark<>'' 7 | on duplicate key update remark=values(remark); -------------------------------------------------------------------------------- /modules/user/sql/user-20220713-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `user` ADD COLUMN wx_openid VARCHAR(100) NOT NULL DEFAULT '' COMMENT '微信openid'; 4 | ALTER TABLE `user` ADD COLUMN wx_unionid VARCHAR(100) NOT NULL DEFAULT '' COMMENT '微信unionid'; 5 | -------------------------------------------------------------------------------- /modules/user/sql/user-20220816-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `user_setting` ADD COLUMN `flame` smallint not null default 0 COMMENT '阅后即焚是否开启 1.开启 0.未开启'; 4 | ALTER TABLE `user_setting` ADD COLUMN `flame_second` smallint not null default 0 COMMENT '阅后即焚销毁秒数'; -------------------------------------------------------------------------------- /modules/user/sql/user-20220906-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | -- 短编号 4 | create table `shortno` 5 | ( 6 | id bigint not null primary key AUTO_INCREMENT, 7 | shortno VARCHAR(40) not null default '' COMMENT '唯一短编号', 8 | used smallint not null default 0 COMMENT '是否被用', 9 | hold smallint not null default 0 COMMENT '保留,保留的号码将不会再被分配', 10 | locked smallint not null default 0 COMMENT '是否被锁定,锁定了的短编号将不再被分配,直到解锁', 11 | business VARCHAR(40) not null default '' COMMENT '被使用的业务,比如 user', 12 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 13 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 14 | ); 15 | 16 | CREATE UNIQUE INDEX `udx_shortno` on `shortno` (`shortno`); -------------------------------------------------------------------------------- /modules/user/sql/user-20220919-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | -- 设备标识 4 | create table `device_flag` 5 | ( 6 | id bigint not null primary key AUTO_INCREMENT, 7 | device_flag smallint not null default 0 COMMENT '设备标记 0. app 1.Web 2.PC', 8 | `weight` integer not null default 0 COMMENT '设备权重 值越大越优先', 9 | remark VARCHAR(100) not null default '' COMMENT '备注', 10 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 11 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 12 | ); 13 | 14 | CREATE UNIQUE INDEX `udx_device_flag` on `device_flag` (`device_flag`); 15 | 16 | 17 | insert into device_flag(device_flag,`weight`,remark) values(2,'80000','PC'); 18 | insert into device_flag(device_flag,`weight`,remark) values(1,'70000','Web'); 19 | insert into device_flag(device_flag,`weight`,remark) values(0,'90000','手机'); 20 | -------------------------------------------------------------------------------- /modules/user/sql/user-20230911-01.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +migrate Up 3 | 4 | ALTER TABLE `user` ADD COLUMN web3_public_key VARCHAR(200) NOT NULL DEFAULT '' COMMENT 'web3公钥'; 5 | -------------------------------------------------------------------------------- /modules/user/sql/user-20230924-01.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +migrate Up 3 | 4 | ALTER TABLE `user` ADD COLUMN msg_expire_second bigint NOT NULL DEFAULT 0 COMMENT '消息过期时长(单位秒)'; 5 | -------------------------------------------------------------------------------- /modules/user/sql/user-20231127-01.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | -- +migrate Up 4 | 5 | -- 好友申请记录 6 | create table `friend_apply_record` 7 | ( 8 | id bigint not null primary key AUTO_INCREMENT, 9 | uid VARCHAR(40) not null default '', -- 用户uid 10 | to_uid VARCHAR(40) not null default '', -- 申请者uid 11 | remark VARCHAR(200) not null default '', -- 申请备注 12 | status smallint not null DEFAULT 1, -- 状态 0.未处理 1.通过 2.拒绝 13 | token VARCHAR(200) not null default '', -- 通过好友所需验证 14 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 15 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 16 | ); 17 | 18 | CREATE INDEX `friend_apply_record_uidx` on `friend_apply_record` (`uid`); 19 | CREATE UNIQUE INDEX `friend_apply_record_uid_touidx` on `friend_apply_record` (`uid`,`to_uid`); 20 | 21 | -- 用户业务红点 22 | CREATE table `user_red_dot`( 23 | id bigint not null primary key AUTO_INCREMENT, 24 | uid VARCHAR(40) not null default '', -- 用户uid 25 | count smallint not null default 0, -- 未读数量 26 | category VARCHAR(40) not null default '', -- 红点分类 27 | is_dot smallint not null default 0, -- 是否显示红点 1.是 0.否 28 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 29 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 30 | ); 31 | 32 | CREATE UNIQUE INDEX `user_red_dot_uid_categoryx` on `user_red_dot` (`uid`,`category`); 33 | -------------------------------------------------------------------------------- /modules/webhook/1module.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/register" 8 | ) 9 | 10 | //go:embed sql 11 | var sqlFS embed.FS 12 | 13 | func init() { 14 | 15 | register.AddModule(func(ctx interface{}) register.Module { 16 | wk := New(ctx.(*config.Context)) 17 | return register.Module{ 18 | SetupAPI: func() register.APIRouter { 19 | 20 | return wk 21 | }, 22 | SQLDir: register.NewSQLFS(sqlFS), 23 | Start: func() error { 24 | return wk.Start() 25 | }, 26 | Stop: func() error { 27 | return wk.Stop() 28 | }, 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /modules/webhook/apns.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/sideshow/apns2" 7 | "github.com/sideshow/apns2/certificate" 8 | ) 9 | 10 | //APNS APNS 11 | type APNS struct { 12 | client *apns2.Client 13 | p12FilePath string 14 | password string 15 | dev bool // 是否是开发环境 16 | 17 | } 18 | 19 | // NewAPNS NewAPNS 20 | func NewAPNS(p12FilePath, password string, dev bool) *APNS { 21 | apns := &APNS{ 22 | p12FilePath: p12FilePath, 23 | password: password, 24 | dev: dev, 25 | } 26 | return apns 27 | } 28 | 29 | func (a *APNS) createClient() (*apns2.Client, error) { 30 | cert, err := certificate.FromP12File(a.p12FilePath, a.password) 31 | if err != nil { 32 | return nil, err 33 | } 34 | var client *apns2.Client 35 | if a.dev { 36 | client = apns2.NewClient(cert).Development() 37 | } else { 38 | client = apns2.NewClient(cert).Production() 39 | } 40 | return client, nil 41 | } 42 | 43 | // Push 推送消息 44 | func (a *APNS) Push(notification *apns2.Notification) error { 45 | var err error 46 | if a.client == nil { 47 | a.client, err = a.createClient() 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | res, err := a.client.Push(notification) 53 | if err != nil { 54 | return err 55 | } 56 | if res.StatusCode != 200 { 57 | return errors.New(res.Reason) 58 | } 59 | 60 | return nil 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/webhook/db_message.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "fmt" 5 | "hash/crc32" 6 | 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 8 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 9 | "github.com/gocraft/dbr/v2" 10 | ) 11 | 12 | type messageDB struct { 13 | ctx *config.Context 14 | db *dbr.Session 15 | } 16 | 17 | func newMessageDB(ctx *config.Context) *messageDB { 18 | 19 | return &messageDB{ 20 | ctx: ctx, 21 | db: ctx.DB(), 22 | } 23 | } 24 | 25 | func (m *messageDB) insertOrUpdateTx(model *messageModel, tx *dbr.Tx) error { 26 | tbl := m.getTable(model.ChannelID) 27 | _, err := tx.InsertBySql(fmt.Sprintf("insert into %s(message_id,message_seq,client_msg_no,header,setting,`signal`,from_uid,channel_id,channel_type,expire,expire_at,timestamp,payload,is_deleted) values(?,?,?,?,?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE payload=payload", tbl), model.MessageID, model.MessageSeq, model.ClientMsgNo, model.Header, model.Setting, model.Signal, model.FromUID, model.ChannelID, model.ChannelType, model.Expire, model.ExpireAt, model.Timestamp, model.Payload, model.IsDeleted).Exec() 28 | return err 29 | } 30 | 31 | // 通过频道ID获取表 32 | func (m *messageDB) getTable(channelID string) string { 33 | tableIndex := crc32.ChecksumIEEE([]byte(channelID)) % uint32(m.ctx.GetConfig().TablePartitionConfig.MessageTableCount) 34 | if tableIndex == 0 { 35 | return "message" 36 | } 37 | return fmt.Sprintf("message%d", tableIndex) 38 | } 39 | 40 | type messageModel struct { 41 | MessageID string 42 | MessageSeq int64 43 | ClientMsgNo string 44 | Header string 45 | Setting uint8 46 | Signal uint8 // 是否signal加密 47 | FromUID string 48 | ChannelID string 49 | ChannelType uint8 50 | Expire uint32 51 | ExpireAt uint32 52 | Timestamp int32 53 | Payload string 54 | IsDeleted int 55 | db.BaseModel 56 | } 57 | -------------------------------------------------------------------------------- /modules/webhook/db_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | // import ( 4 | // "os" 5 | // "testing" 6 | 7 | // "github.com/stretchr/testify/assert" 8 | // "github.com/TangSengDaoDao/TangSengDaoDaoServer/internal/api/group" 9 | // "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/user" 10 | // "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/db" 11 | // ) 12 | 13 | // func TestGetThirdName(t *testing.T) { 14 | // os.Remove("test.db") 15 | // s := db.NewSqlite("test.db", "../../../assets/sql") 16 | // d := NewDB(s) 17 | // udb := user.NewDB(s) 18 | // err := udb.Insert(&user.Model{ 19 | // UID: "1", 20 | // Name: "test1", 21 | // }) 22 | // assert.NoError(t, err) 23 | 24 | // err = udb.Insert(&user.Model{ 25 | // UID: "2", 26 | // Name: "test2", 27 | // }) 28 | // assert.NoError(t, err) 29 | 30 | // fdb := friend.NewDB(s) 31 | // err = fdb.Insert(&friend.Model{ 32 | // UID: "1", 33 | // ToUID: "2", 34 | // Remark: "dddd", 35 | // }) 36 | // assert.NoError(t, err) 37 | 38 | // err = fdb.Insert(&friend.Model{ 39 | // UID: "2", 40 | // ToUID: "1", 41 | // Remark: "11dddd", 42 | // }) 43 | // assert.NoError(t, err) 44 | 45 | // gdb := group.NewDB(s) 46 | // gdb.InsertMember(&group.MemberModel{ 47 | // GroupNo: "g1", 48 | // UID: "1", 49 | // Remark: "g1_name", 50 | // }) 51 | 52 | // name, remark, nameInGroup, err := d.GetThirdName("1", "2", "g1") 53 | // assert.NoError(t, err) 54 | 55 | // assert.Equal(t, "test1", name) 56 | // assert.Equal(t, "11dddd", remark) 57 | // assert.Equal(t, "g1_name", nameInGroup) 58 | 59 | // } 60 | -------------------------------------------------------------------------------- /modules/webhook/github.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/wkhttp" 8 | ) 9 | 10 | func (w *Webhook) github(c *wkhttp.Context) { 11 | fmt.Println("github webhook-->", c.Params) 12 | 13 | result, _ := ioutil.ReadAll(c.Request.Body) 14 | fmt.Println("github-result-->", result) 15 | } 16 | -------------------------------------------------------------------------------- /modules/webhook/push.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "github.com/TangSengDaoDao/TangSengDaoDaoServer/modules/user" 5 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/common" 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | ) 8 | 9 | // Payload 推送内容 10 | type Payload interface { 11 | GetTitle() string // 推送标题 12 | GetContent() string // 推送正文 13 | GetBadge() int // 推送红点 14 | 15 | GetRTCPayload() RTCPayload // 获取rtc的payload 16 | } 17 | 18 | type RTCPayload interface { 19 | GetCallType() common.RTCCallType // 音视频呼叫类型 20 | GetOperation() string // 音视频操作 invite: 邀请音视频 cancel:取消邀请 21 | GetFromUID() string // 发起人的uid 22 | } 23 | 24 | // BasePayload 基础负载 25 | type BasePayload struct { 26 | title string 27 | content string 28 | badge int 29 | } 30 | 31 | // GetTitle 推送标题 32 | func (p *BasePayload) GetTitle() string { 33 | return p.title 34 | } 35 | 36 | // GetContent 推送正文 37 | func (p *BasePayload) GetContent() string { 38 | return p.content 39 | } 40 | 41 | // GetBadge 推送红点 42 | func (p *BasePayload) GetBadge() int { 43 | return p.badge 44 | } 45 | 46 | func (p *BasePayload) GetRTCPayload() RTCPayload { 47 | return nil 48 | } 49 | 50 | type BaseRTCPayload struct { 51 | BasePayload 52 | callType common.RTCCallType 53 | operation string 54 | fromUID string 55 | } 56 | 57 | func (b *BaseRTCPayload) GetCallType() common.RTCCallType { 58 | return b.callType 59 | } 60 | 61 | func (b *BaseRTCPayload) GetOperation() string { 62 | return b.operation 63 | } 64 | 65 | func (b *BaseRTCPayload) GetFromUID() string { 66 | return b.fromUID 67 | } 68 | 69 | func (b *BaseRTCPayload) GetRTCPayload() RTCPayload { 70 | return b 71 | } 72 | 73 | // Push Push 74 | type Push interface { 75 | GetPayload(msg msgOfflineNotify, ctx *config.Context, toUser *user.Resp) (Payload, error) 76 | Push(deviceToken string, payload Payload) error 77 | } 78 | -------------------------------------------------------------------------------- /modules/webhook/push_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHMSPush(t *testing.T) { 11 | hms := NewHMSPush("101827411", "649dedfb617dbd699715c05b9b430ce54013ad404cea0a5a2a16302fb01911a2", "com.xinbida.wukongchat") 12 | accessToken, _, err := hms.GetHMSAccessToken() 13 | assert.NoError(t, err) 14 | payloadInfo := &PayloadInfo{ 15 | Title: "title", 16 | Content: "content2222", 17 | Badge: 1, 18 | } 19 | err = hms.Push("ANqYJlGemvmj_H5U8L3629mb-OT7slBYJTdB8-vfpveu-oQzsJH8qtxCmEfzEiUemP1Gc7KV5M32rbiuhafNaZSu2VRPxAASLp3c_1_Ky-kUPN8FU06fZWHxLlA-6tJjCg", NewHMSPayload(payloadInfo, accessToken)) 20 | assert.NoError(t, err) 21 | } 22 | 23 | func TestMIPush(t *testing.T) { 24 | mi := NewMIPush("2882303761519001722", "XIf41QWNIBRZPJVKUOOoYQ==", "com.xinbida.wukongchat", "") 25 | 26 | payloadInfo := &PayloadInfo{ 27 | Title: "title", 28 | Content: "content", 29 | Badge: 1, 30 | } 31 | 32 | err := mi.Push("deviceToken", NewMIPayload(payloadInfo, "11")) 33 | assert.NoError(t, err) 34 | } 35 | 36 | func TestOPPOPush(t *testing.T) { 37 | oppo := NewOPPOPush("30755393", "aece2f965eb64a9a82e01db87b23030e", "d7205515e1ab4fe6ace46f0f5df1105f", "dd6e2ec2e89e4669bb4afe4433b28ac1", &config.Context{}) 38 | payloadInfo := &PayloadInfo{ 39 | Title: "标题", 40 | Content: "内容", 41 | Badge: 1, 42 | } 43 | err := oppo.Push("OPPO_CN_5831bbbefd00814c2bd82dbd40382869", NewOPPOPayload(payloadInfo, "11")) 44 | assert.NoError(t, err) 45 | } 46 | 47 | func TestVIVOPush(t *testing.T) { 48 | vivo := NewVIVOPush("105542118", "d7aacd9d36621e75a9efb7ce69b5c567", "be82d800-0078-42cf-91d2-4127781361a9", &config.Context{}) 49 | payloadInfo := &PayloadInfo{ 50 | Title: "标题", 51 | Content: "内容", 52 | Badge: 1, 53 | } 54 | err := vivo.Push("16569158930074211800064", NewVIVOPayload(payloadInfo, "11")) 55 | assert.NoError(t, err) 56 | } 57 | 58 | func TestFirebasePush(t *testing.T) { 59 | // 请使用你本地的绝对路径进行测试 60 | mi := NewFIREBASEPush("service_Account_json_Path", "bobo", "这个值请从json里面获取", "") 61 | 62 | payloadInfo := &PayloadInfo{ 63 | Title: "title", 64 | Content: "content", 65 | Badge: 1, 66 | } 67 | // 这个device token是 firebase的token 不是app的device token,请前端老师帮忙提供即可。 68 | err := mi.Push("请前端开发给你提供这个值", NewFIREBASEPayload(payloadInfo, "11")) 69 | assert.NoError(t, err) 70 | } 71 | -------------------------------------------------------------------------------- /modules/webhook/sql/webhook-20230920-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | 4 | 5 | ALTER TABLE `message` ADD COLUMN expire integer not null DEFAULT 0 COMMENT '消息过期时长'; 6 | ALTER TABLE `message` ADD COLUMN expire_at BIGINT not null DEFAULT 0 COMMENT '消息过期时间'; 7 | 8 | ALTER TABLE `message1` ADD COLUMN expire integer not null DEFAULT 0 COMMENT '消息过期时长'; 9 | ALTER TABLE `message1` ADD COLUMN expire_at BIGINT not null DEFAULT 0 COMMENT '消息过期时间'; 10 | 11 | ALTER TABLE `message2` ADD COLUMN expire integer not null DEFAULT 0 COMMENT '消息过期时长'; 12 | ALTER TABLE `message2` ADD COLUMN expire_at BIGINT not null DEFAULT 0 COMMENT '消息过期时间'; 13 | 14 | ALTER TABLE `message3` ADD COLUMN expire integer not null DEFAULT 0 COMMENT '消息过期时长'; 15 | ALTER TABLE `message3` ADD COLUMN expire_at BIGINT not null DEFAULT 0 COMMENT '消息过期时间'; 16 | 17 | ALTER TABLE `message4` ADD COLUMN expire integer not null DEFAULT 0 COMMENT '消息过期时长'; 18 | ALTER TABLE `message4` ADD COLUMN expire_at BIGINT not null DEFAULT 0 COMMENT '消息过期时间'; -------------------------------------------------------------------------------- /modules/webhook/sql/webhook-20241217-01.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | 3 | ALTER TABLE `message` MODIFY COLUMN client_msg_no VARCHAR(100) not null default ''; 4 | ALTER TABLE `message1` MODIFY COLUMN client_msg_no VARCHAR(100) not null default ''; 5 | ALTER TABLE `message2` MODIFY COLUMN client_msg_no VARCHAR(100) not null default ''; 6 | ALTER TABLE `message3` MODIFY COLUMN client_msg_no VARCHAR(100) not null default ''; 7 | ALTER TABLE `message4` MODIFY COLUMN client_msg_no VARCHAR(100) not null default ''; -------------------------------------------------------------------------------- /modules/workplace/1module.go: -------------------------------------------------------------------------------- 1 | package workplace 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/config" 7 | "github.com/TangSengDaoDao/TangSengDaoDaoServerLib/pkg/register" 8 | ) 9 | 10 | //go:embed sql 11 | var sqlFS embed.FS 12 | 13 | //go:embed swagger/api.yaml 14 | var swaggerContent string 15 | 16 | func init() { 17 | register.AddModule(func(ctx interface{}) register.Module { 18 | return register.Module{ 19 | Name: "workplace", 20 | SetupAPI: func() register.APIRouter { 21 | return New(ctx.(*config.Context)) 22 | }, 23 | SQLDir: register.NewSQLFS(sqlFS), 24 | Swagger: swaggerContent, 25 | } 26 | }) 27 | 28 | // 工作台管理模块 29 | register.AddModule(func(ctx interface{}) register.Module { 30 | return register.Module{ 31 | Name: "workplace_manager", 32 | SetupAPI: func() register.APIRouter { 33 | return NewManager(ctx.(*config.Context)) 34 | }, 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /modules/workplace/sql/workplace-20230906-01.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +migrate Up 3 | 4 | 5 | -- 工作台分类下app 6 | create table `workplace_category_app`( 7 | id bigint not null primary key AUTO_INCREMENT, 8 | category_no VARCHAR(40) not null DEFAULT '', -- 分类编号 9 | app_id VARCHAR(40) not null DEFAULT '', -- appid 10 | sort_num integer not null DEFAULT 0, -- 排序编号 11 | created_at timeStamp not null DEFAULT CURRENT_TIMESTAMP, -- 创建时间 12 | updated_at timeStamp not null DEFAULT CURRENT_TIMESTAMP -- 更新时间 13 | ); 14 | 15 | CREATE unique INDEX workplace_category_app_cno_aid on `workplace_category_app` (category_no,app_id); 16 | -------------------------------------------------------------------------------- /modules/workplace/sql/workplace-20240113-01.sql: -------------------------------------------------------------------------------- 1 | 2 | -- +migrate Up 3 | ALTER TABLE `workplace_banner` ADD COLUMN `sort_num` integer not null default 0 COMMENT '排序号'; 4 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "time" 4 | 5 | // Cache 缓存接口 6 | type Cache interface { 7 | // Set 设置key value 8 | Set(key string, value string) error 9 | // 删除key 10 | Delete(key string) error 11 | // SetAndExpire 设置key value 并支持过期时间 12 | SetAndExpire(key string, value string, expire time.Duration) error 13 | // 获取key对应的值 14 | Get(key string) (string, error) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | timeFormart = "2006-01-02 15:04:05" 9 | ) 10 | 11 | type Time time.Time 12 | 13 | type BaseModel struct { 14 | Id int64 15 | CreatedAt Time 16 | UpdatedAt Time 17 | } 18 | 19 | func (t *Time) UnmarshalJSON(data []byte) (err error) { 20 | now, err := time.ParseInLocation(`"`+timeFormart+`"`, string(data), time.Local) 21 | *t = Time(now) 22 | return 23 | } 24 | 25 | func (t Time) MarshalJSON() ([]byte, error) { 26 | b := make([]byte, 0, len(timeFormart)+2) 27 | b = append(b, '"') 28 | b = time.Time(t).AppendFormat(b, timeFormart) 29 | b = append(b, '"') 30 | return b, nil 31 | } 32 | 33 | func (t Time) String() string { 34 | return time.Time(t).Format(timeFormart) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/db/redis.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import "github.com/TangSengDaoDao/TangSengDaoDaoServer/pkg/redis" 4 | 5 | func NewRedis(addr string, password string) *redis.Conn { 6 | return redis.New(addr, password) 7 | } 8 | -------------------------------------------------------------------------------- /pkg/db/sqlite.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/gocraft/dbr/v2" 8 | _ "github.com/mattn/go-sqlite3" 9 | migrate "github.com/rubenv/sql-migrate" 10 | ) 11 | 12 | // NewSqlite 创建一个sqlite db,[path]db存储路径 [sqlDir]sql脚本目录 13 | func NewSqlite(filepath string, sqlDir string) *dbr.Session { 14 | 15 | err := os.Mkdir(path.Dir(filepath), os.ModePerm) 16 | if err != nil && !os.IsExist(err) { 17 | panic(err) 18 | } 19 | conn, err := dbr.Open("sqlite3", filepath, nil) 20 | if err != nil { 21 | panic(err) 22 | } 23 | session := conn.NewSession(nil) 24 | migrations := &migrate.FileMigrationSource{ 25 | Dir: sqlDir, 26 | } 27 | _, err = migrate.Exec(session.DB, "sqlite3", migrations, migrate.Up) 28 | if err != nil { 29 | panic(err) 30 | } 31 | return session 32 | } 33 | -------------------------------------------------------------------------------- /pkg/keylock/keylock.go: -------------------------------------------------------------------------------- 1 | package keylock 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultCleanInterval = 24 * time.Hour //默认24小时清理一次 11 | ) 12 | 13 | // KeyLock KeyLock 14 | type KeyLock struct { 15 | locks map[string]*innerLock //关键字锁map 16 | cleanInterval time.Duration //定时清除时间间隔 17 | stopChan chan struct{} //停止信号 18 | mutex sync.RWMutex //全局读写锁 19 | } 20 | 21 | // NewKeyLock NewKeyLock 22 | func NewKeyLock() *KeyLock { 23 | return &KeyLock{ 24 | locks: make(map[string]*innerLock), 25 | cleanInterval: defaultCleanInterval, 26 | stopChan: make(chan struct{}), 27 | } 28 | } 29 | 30 | //Lock 根据关键字加锁 31 | func (l *KeyLock) Lock(key string) { 32 | l.mutex.RLock() 33 | keyLock, ok := l.locks[key] 34 | if ok { 35 | keyLock.add() 36 | } 37 | l.mutex.RUnlock() 38 | if !ok { 39 | l.mutex.Lock() 40 | keyLock, ok = l.locks[key] 41 | if !ok { 42 | keyLock = newInnerLock() 43 | l.locks[key] = keyLock 44 | } 45 | keyLock.add() 46 | l.mutex.Unlock() 47 | } 48 | keyLock.Lock() 49 | } 50 | 51 | //Unlock 根据关键字解锁 52 | func (l *KeyLock) Unlock(key string) { 53 | l.mutex.RLock() 54 | keyLock, ok := l.locks[key] 55 | if ok { 56 | keyLock.done() 57 | } 58 | l.mutex.RUnlock() 59 | if ok { 60 | keyLock.Unlock() 61 | } 62 | } 63 | 64 | //Clean 清理空闲锁 65 | func (l *KeyLock) Clean() { 66 | l.mutex.Lock() 67 | for k, v := range l.locks { 68 | if v.count == 0 { 69 | delete(l.locks, k) 70 | } 71 | } 72 | l.mutex.Unlock() 73 | } 74 | 75 | //StartCleanLoop 开启清理协程 76 | func (l *KeyLock) StartCleanLoop() { 77 | go l.cleanLoop() 78 | } 79 | 80 | //StopCleanLoop 停止清理协程 81 | func (l *KeyLock) StopCleanLoop() { 82 | close(l.stopChan) 83 | } 84 | 85 | //清理循环 86 | func (l *KeyLock) cleanLoop() { 87 | ticker := time.NewTicker(l.cleanInterval) 88 | for { 89 | select { 90 | case <-ticker.C: 91 | l.Clean() 92 | case <-l.stopChan: 93 | ticker.Stop() 94 | return 95 | } 96 | } 97 | } 98 | 99 | //内部锁信息 100 | type innerLock struct { 101 | count int64 102 | sync.Mutex 103 | } 104 | 105 | //新建内部锁 106 | func newInnerLock() *innerLock { 107 | return &innerLock{} 108 | } 109 | 110 | func (il *innerLock) add() { 111 | atomic.AddInt64(&il.count, 1) 112 | } 113 | 114 | func (il *innerLock) done() { 115 | atomic.AddInt64(&il.count, -1) 116 | } 117 | -------------------------------------------------------------------------------- /pkg/log/logger_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/zap" 7 | ) 8 | 9 | func TestLogger(t *testing.T) { 10 | opts := NewOptions() 11 | opts.Level = zap.DebugLevel 12 | opts.LineNum = true 13 | Configure(opts) 14 | 15 | Info("this is info") 16 | Debug("this is debug") 17 | Error("this is error", zap.String("key", "value")) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/log/options.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "go.uber.org/zap/zapcore" 4 | 5 | type Options struct { 6 | Level zapcore.Level 7 | LogDir string 8 | LineNum bool 9 | } 10 | 11 | func NewOptions() *Options { 12 | 13 | return &Options{} 14 | } 15 | -------------------------------------------------------------------------------- /pkg/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/gomarkdown/markdown" 8 | "github.com/gomarkdown/markdown/ast" 9 | "github.com/gomarkdown/markdown/html" 10 | "github.com/sourcegraph/syntaxhighlight" 11 | ) 12 | 13 | func ToHtml(v string) string { 14 | if v == "" { 15 | return "" 16 | } 17 | htmlFlags := html.CommonFlags | html.HrefTargetBlank 18 | opts := html.RendererOptions{ 19 | Flags: htmlFlags, 20 | RenderNodeHook: renderHookCodeBlock, 21 | } 22 | renderer := html.NewRenderer(opts) 23 | 24 | return string(markdown.ToHTML([]byte(v), nil, renderer)) 25 | } 26 | 27 | func renderHookCodeBlock(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) { 28 | 29 | _, ok := node.(*ast.Code) 30 | if ok { 31 | fmt.Println("code-------------->") 32 | w.Write([]byte(fmt.Sprintf("
%s
", string(node.AsLeaf().Literal)))) 33 | return ast.GoToNext, true 34 | } 35 | 36 | _, ok = node.(*ast.CodeBlock) 37 | if ok { 38 | syncHtml, _ := syntaxhighlight.AsHTML(node.AsLeaf().Literal, func(options *syntaxhighlight.HTMLConfig) { 39 | options.Type = "pl-en" 40 | options.Keyword = "pl-k" 41 | options.Plaintext = "pl-s1" 42 | options.String = "pl-s1" 43 | options.Comment = "pl-c" 44 | }) 45 | w.Write([]byte(fmt.Sprintf("
%s
", string(syncHtml)))) 46 | return ast.GoToNext, true 47 | } 48 | // test := syntaxhighlight.HTMLConfig{ 49 | // String: "str", 50 | // Keyword: "kwd", 51 | // Comment: "com", 52 | // Type: "typ", 53 | // Literal: "lit", 54 | // Punctuation: "pun", 55 | // Plaintext: "pln", 56 | // Tag: "tag", 57 | // HTMLTag: "htm", 58 | // HTMLAttrName: "atn", 59 | // HTMLAttrValue: "atv", 60 | // Decimal: "dec", 61 | // Whitespace: "", 62 | // } 63 | return ast.GoToNext, false 64 | 65 | } 66 | -------------------------------------------------------------------------------- /pkg/markdown/markdown_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestToHtml(t *testing.T) { 9 | 10 | htm := ToHtml("a\n```go\n /** test **/ func Test(v []byte) (error){ fmt.Println(\"zdsdsdsd\")}\n```\nb `测试`") 11 | 12 | fmt.Println("htm--->", htm) 13 | 14 | } 15 | -------------------------------------------------------------------------------- /pkg/pool/dispatcher.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | var WorkerChannel = make(chan chan *Job) 8 | 9 | type JobStatistics struct { 10 | Executing int64 // Total number of jobs executed 11 | Total int64 12 | } 13 | 14 | type Collector struct { 15 | Work chan *Job // receives jobs to send to workers 16 | End chan bool // when receives bool stops workers 17 | jobS *JobStatistics 18 | queue *Queue 19 | } 20 | 21 | func StartDispatcher(workerCount int64) Collector { 22 | var i int64 23 | var workers []Worker 24 | input := make(chan *Job) // channel to recieve work 25 | end := make(chan bool) // channel to spin down workers 26 | jobFinished := make(chan bool) 27 | collector := Collector{ 28 | Work: input, 29 | End: end, 30 | jobS: &JobStatistics{}, 31 | queue: NewQueue(), 32 | } 33 | 34 | go collector.loopPop() 35 | 36 | for i < workerCount { 37 | i++ 38 | worker := Worker{ 39 | ID: i, 40 | Channel: make(chan *Job), 41 | WorkerChannel: WorkerChannel, 42 | End: make(chan struct{}), 43 | jobFinished: jobFinished, 44 | } 45 | worker.Start() 46 | workers = append(workers, worker) // stores worker 47 | } 48 | go func() { 49 | for { 50 | select { 51 | case <-jobFinished: // job finished 52 | atomic.AddInt64(&collector.jobS.Executing, -1) 53 | } 54 | } 55 | }() 56 | 57 | // start collector 58 | go func() { 59 | for { 60 | select { 61 | case <-end: 62 | for _, w := range workers { 63 | w.Stop() // stop worker 64 | } 65 | return 66 | case job := <-input: 67 | collector.queue.Push(job) 68 | } 69 | } 70 | }() 71 | 72 | return collector 73 | } 74 | 75 | func (c Collector) loopPop() { 76 | for { 77 | jobObj := c.queue.Pop() 78 | atomic.AddInt64(&c.jobS.Total, 1) 79 | worker := <-WorkerChannel // wait for available channel 80 | atomic.AddInt64(&c.jobS.Executing, 1) 81 | worker <- jobObj.(*Job) // dispatch work to worker 82 | } 83 | 84 | } 85 | 86 | func (c Collector) GetStatistics() *JobStatistics { 87 | return c.jobS 88 | } 89 | 90 | func (c Collector) Waiting() int { 91 | return c.queue.Len() 92 | } 93 | -------------------------------------------------------------------------------- /pkg/pool/queue.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "github.com/eapache/queue" 5 | "runtime" 6 | "sync" 7 | ) 8 | 9 | 10 | type Queue struct { 11 | sync.Mutex 12 | popable *sync.Cond 13 | buffer *queue.Queue 14 | closed bool 15 | } 16 | 17 | func NewQueue() *Queue { 18 | e := &Queue{ 19 | buffer: queue.New(), 20 | } 21 | e.popable = sync.NewCond(&e.Mutex) 22 | return e 23 | } 24 | 25 | func (e *Queue) Push(v interface{}) { 26 | e.Mutex.Lock() 27 | defer e.Mutex.Unlock() 28 | if !e.closed { 29 | e.buffer.Add(v) 30 | e.popable.Signal() 31 | } 32 | } 33 | func (e *Queue) Close() { 34 | e.Mutex.Lock() 35 | defer e.Mutex.Unlock() 36 | if !e.closed { 37 | e.closed = true 38 | e.popable.Broadcast() //广播 39 | } 40 | } 41 | 42 | //Pop 取出队列,(阻塞模式) 43 | func (e *Queue) Pop() (v interface{}) { 44 | c := e.popable 45 | buffer := e.buffer 46 | 47 | e.Mutex.Lock() 48 | defer e.Mutex.Unlock() 49 | 50 | for buffer.Length() == 0 && !e.closed { 51 | c.Wait() 52 | } 53 | 54 | if e.closed { //已关闭 55 | return 56 | } 57 | 58 | if buffer.Length() > 0 { 59 | v = buffer.Peek() 60 | buffer.Remove() 61 | } 62 | return 63 | } 64 | 65 | //试着取出队列(非阻塞模式)返回ok == false 表示空 66 | func (e *Queue) TryPop() (v interface{}, ok bool) { 67 | buffer := e.buffer 68 | 69 | e.Mutex.Lock() 70 | defer e.Mutex.Unlock() 71 | 72 | if buffer.Length() > 0 { 73 | v = buffer.Peek() 74 | buffer.Remove() 75 | ok = true 76 | } else if e.closed { 77 | ok = true 78 | } 79 | 80 | return 81 | } 82 | 83 | // 获取队列长度 84 | func (e *Queue) Len() int { 85 | return e.buffer.Length() 86 | } 87 | 88 | //Wait 等待队列消费完成 89 | func (e *Queue) Wait() { 90 | for { 91 | if e.closed || e.buffer.Length() == 0 { 92 | break 93 | } 94 | 95 | runtime.Gosched() //出让时间片 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /pkg/pool/worker.go: -------------------------------------------------------------------------------- 1 | package pool 2 | 3 | import ( 4 | "log" 5 | ) 6 | 7 | type JobFunc func(id int64, data interface{}) 8 | 9 | type Job struct { 10 | Data interface{} 11 | JobFunc JobFunc 12 | } 13 | 14 | type Worker struct { 15 | ID int64 16 | WorkerChannel chan chan *Job // used to communicate between dispatcher and workers 17 | Channel chan *Job 18 | End chan struct{} 19 | jobFinished chan bool 20 | } 21 | 22 | // start worker 23 | func (w *Worker) Start() { 24 | go func() { 25 | for { 26 | w.WorkerChannel <- w.Channel // when the worker is available place channel in queue 27 | select { 28 | case job := <-w.Channel: // worker has received job 29 | if job != nil { 30 | job.JobFunc(w.ID, job.Data) // do work 31 | w.jobFinished <- true 32 | } 33 | 34 | case <-w.End: 35 | return 36 | } 37 | } 38 | }() 39 | } 40 | 41 | // end worker 42 | func (w *Worker) Stop() { 43 | log.Printf("worker [%d] is stopping", w.ID) 44 | w.End <- struct{}{} 45 | } 46 | -------------------------------------------------------------------------------- /pkg/register/register.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import "github.com/TangSengDaoDao/TangSengDaoDaoServer/pkg/wkhttp" 4 | 5 | // APIRouter api路由者 6 | type APIRouter interface { 7 | Route(r *wkhttp.WKHttp) 8 | } 9 | 10 | var apiRoutes = make([]APIRouter, 0) 11 | 12 | // Add 添加api 13 | func Add(r APIRouter) { 14 | apiRoutes = append(apiRoutes, r) 15 | } 16 | 17 | var taskRoutes = make([]TaskRouter, 0) 18 | 19 | // GetRoutes 获取所有路由者 20 | func GetRoutes() []APIRouter { 21 | return apiRoutes 22 | } 23 | 24 | // TaskRouter task路由者 25 | type TaskRouter interface { 26 | RegisterTasks() 27 | } 28 | 29 | // AddTask 添加任务 30 | func AddTask(task TaskRouter) { 31 | taskRoutes = append(taskRoutes, task) 32 | } 33 | 34 | // GetTasks 获取所有任务 35 | func GetTasks() []TaskRouter { 36 | return taskRoutes 37 | } 38 | -------------------------------------------------------------------------------- /pkg/util/base62.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Ten2Hex 十进制转换为62进制 8 | func Ten2Hex(ten int64) string { 9 | hex := "" 10 | var divValue, resValue int64 11 | for ten >= 62 { 12 | divValue = ten / int64(62) 13 | resValue = ten % 62 14 | hex = tenValue2Char(resValue) + hex 15 | ten = divValue 16 | } 17 | if ten != 0 { 18 | hex = tenValue2Char(ten) + hex 19 | } 20 | return hex 21 | } 22 | 23 | func tenValue2Char(ten int64) string { 24 | switch ten { 25 | case 0: 26 | case 1: 27 | case 2: 28 | case 3: 29 | case 4: 30 | case 5: 31 | case 6: 32 | case 7: 33 | case 8: 34 | case 9: 35 | return fmt.Sprintf("%d", ten) 36 | case 10: 37 | return "a" 38 | case 11: 39 | return "b" 40 | case 12: 41 | return "c" 42 | case 13: 43 | return "d" 44 | case 14: 45 | return "e" 46 | case 15: 47 | return "f" 48 | case 16: 49 | return "g" 50 | case 17: 51 | return "h" 52 | case 18: 53 | return "i" 54 | case 19: 55 | return "j" 56 | case 20: 57 | return "k" 58 | case 21: 59 | return "l" 60 | case 22: 61 | return "m" 62 | case 23: 63 | return "n" 64 | case 24: 65 | return "o" 66 | case 25: 67 | return "p" 68 | case 26: 69 | return "q" 70 | case 27: 71 | return "r" 72 | case 28: 73 | return "s" 74 | case 29: 75 | return "t" 76 | case 30: 77 | return "u" 78 | case 31: 79 | return "v" 80 | case 32: 81 | return "w" 82 | case 33: 83 | return "s" 84 | case 34: 85 | return "y" 86 | case 35: 87 | return "z" 88 | case 36: 89 | return "A" 90 | case 37: 91 | return "B" 92 | case 38: 93 | return "C" 94 | case 39: 95 | return "D" 96 | case 40: 97 | return "E" 98 | case 41: 99 | return "F" 100 | case 42: 101 | return "G" 102 | case 43: 103 | return "H" 104 | case 44: 105 | return "I" 106 | case 45: 107 | return "J" 108 | case 46: 109 | return "K" 110 | case 47: 111 | return "L" 112 | case 48: 113 | return "M" 114 | case 49: 115 | return "N" 116 | case 50: 117 | return "O" 118 | case 51: 119 | return "P" 120 | case 52: 121 | return "Q" 122 | case 53: 123 | return "R" 124 | case 54: 125 | return "S" 126 | case 55: 127 | return "T" 128 | case 56: 129 | return "U" 130 | case 57: 131 | return "V" 132 | case 58: 133 | return "W" 134 | case 59: 135 | return "S" 136 | case 60: 137 | return "Y" 138 | case 61: 139 | return "Z" 140 | default: 141 | return "" 142 | } 143 | return "" 144 | } 145 | -------------------------------------------------------------------------------- /pkg/util/common.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | // CheckErr CheckErr 9 | func CheckErr(err error) { 10 | if err != nil { 11 | panic(err) 12 | } 13 | } 14 | 15 | // Substr Substr 16 | func Substr(str string, start, length int) string { 17 | if length == 0 { 18 | return "" 19 | } 20 | runeStr := []rune(str) 21 | lenStr := len(runeStr) 22 | 23 | if start < 0 { 24 | start = lenStr + start 25 | } 26 | if start > lenStr { 27 | start = lenStr 28 | } 29 | end := start + length 30 | if end > lenStr { 31 | end = lenStr 32 | } 33 | if length < 0 { 34 | end = lenStr + length 35 | } 36 | if start > end { 37 | start, end = end, start 38 | } 39 | return string(runeStr[start:end]) 40 | } 41 | 42 | func objToStr(v interface{}) string { 43 | var strV string 44 | switch v.(type) { 45 | 46 | case int: 47 | strV = fmt.Sprintf("%d", v) 48 | break 49 | case uint: 50 | strV = fmt.Sprintf("%d", v) 51 | break 52 | case int64: 53 | strV = fmt.Sprintf("%d", v) 54 | break 55 | case uint64: 56 | strV = fmt.Sprintf("%d", v) 57 | break 58 | case int8: 59 | strV = fmt.Sprintf("%d", v) 60 | break 61 | case uint8: 62 | strV = fmt.Sprintf("%d", v) 63 | break 64 | case int16: 65 | strV = fmt.Sprintf("%d", v) 66 | break 67 | case uint16: 68 | strV = fmt.Sprintf("%d", v) 69 | break 70 | case int32: 71 | strV = fmt.Sprintf("%d", v) 72 | break 73 | case uint32: 74 | strV = fmt.Sprintf("%s", v) 75 | break 76 | case string: 77 | strV = fmt.Sprintf("%s", v) 78 | break 79 | case float32: 80 | strV = fmt.Sprintf("%s", v) 81 | break 82 | case float64: 83 | strV = fmt.Sprintf("%s", v) 84 | break 85 | default: 86 | strV = fmt.Sprintf("%s", v) 87 | 88 | } 89 | return strV 90 | } 91 | 92 | // Sign Sign 93 | func Sign(params map[string]interface{}, appKey string) string { 94 | signStr := MapToQueryParamSort(params) 95 | return MD5(fmt.Sprintf("%s&key=%s", signStr, appKey)) 96 | } 97 | 98 | // MapToQueryParamSort map 以 key1=value1 & key2=value2形式排序拼接 99 | func MapToQueryParamSort(params map[string]interface{}) string { 100 | if len(params) == 0 { 101 | return "" 102 | } 103 | strs := "" 104 | keys := make([]string, 0, len(params)) 105 | for k := range params { 106 | keys = append(keys, k) 107 | } 108 | sort.Strings(keys) 109 | 110 | for _, k := range keys { 111 | v := params[k] 112 | if v == "" { 113 | continue 114 | } 115 | //strs = strs+k+"&"+v 116 | strs = fmt.Sprintf("%s%s=%s%s", strs, k, objToStr(v), "&") 117 | } 118 | if len(strs) > 0 { 119 | strs = strs[0 : len(strs)-1] 120 | } 121 | return strs 122 | } 123 | -------------------------------------------------------------------------------- /pkg/util/dh.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | 7 | "golang.org/x/crypto/curve25519" 8 | ) 9 | 10 | // GetCurve25519KeypPair GetCurve25519KeypPair 11 | func GetCurve25519KeypPair() (Aprivate, Apublic [32]byte) { 12 | //产生随机数 13 | if _, err := io.ReadFull(rand.Reader, Aprivate[:]); err != nil { 14 | panic(err) 15 | } 16 | curve25519.ScalarBaseMult(&Apublic, &Aprivate) 17 | return 18 | } 19 | 20 | // GetCurve25519Key GetCurve25519Key 21 | func GetCurve25519Key(private, public [32]byte) (Key [32]byte) { 22 | //产生随机数 23 | curve25519.ScalarMult(&Key, &private, &public) 24 | return 25 | } 26 | -------------------------------------------------------------------------------- /pkg/util/hash.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "hash/crc32" 4 | 5 | // HashCrc32 通过字符串获取32位数字 6 | func HashCrc32(str string) uint32 { 7 | 8 | return crc32.ChecksumIEEE([]byte(str)) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/util/json.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | //将对象转换为JSON 9 | func ToJson(obj interface{}) string { 10 | jsonData, err := json.Marshal(obj) 11 | if err != nil { 12 | return "" 13 | } 14 | return string(jsonData) 15 | } 16 | 17 | func JsonToMap(json string) (map[string]interface{}, error) { 18 | var resultMap map[string]interface{} 19 | err := ReadJsonByByte([]byte(json), &resultMap) 20 | return resultMap, err 21 | } 22 | func ReadJsonByByte(body []byte, obj interface{}) error { 23 | mdz := json.NewDecoder(bytes.NewBuffer(body)) 24 | 25 | mdz.UseNumber() 26 | err := mdz.Decode(obj) 27 | 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/util/md5.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "fmt" 10 | ) 11 | 12 | //MD5 加密 13 | func MD5(str string) string { 14 | h := md5.New() 15 | h.Write([]byte(str)) // 需要加密的字符串 16 | passwordmdsBys := h.Sum(nil) 17 | return hex.EncodeToString(passwordmdsBys) 18 | } 19 | 20 | // SHA1加密 21 | func SHA1(str string) string { 22 | fmt.Println("str:", str) 23 | h := sha1.New() 24 | h.Write([]byte(str)) 25 | bs := h.Sum(nil) 26 | return hex.EncodeToString(bs) 27 | } 28 | 29 | func HMACSHA1(keyStr string, data string) string { 30 | //hmac ,use sha1 31 | key := []byte(keyStr) 32 | mac := hmac.New(sha1.New, key) 33 | mac.Write([]byte(data)) 34 | srcBytes := mac.Sum(nil) 35 | return base64.StdEncoding.EncodeToString(srcBytes) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/util/page.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strconv" 4 | 5 | // Page Page 6 | type Page struct { 7 | PageSize uint64 `json:"page_size"` 8 | PageIndex uint64 `json:"page_index"` 9 | Total uint64 `json:"total"` 10 | Data interface{} `json:"data"` 11 | } 12 | 13 | // NewPage NewPage 14 | func NewPage(pageIndex uint64, pageSize uint64, total uint64, data interface{}) *Page { 15 | 16 | return &Page{PageIndex: pageIndex, PageSize: pageSize, Data: data, Total: total} 17 | } 18 | 19 | //ToPageNumOrDefault 将字符串转换为数字类型 如果字符串为空 则赋值分页默认参数 20 | func ToPageNumOrDefault(pageIndex string, pageSize string) (pIndex64 uint64, pSize64 uint64) { 21 | var pageIndex64 uint64 22 | var pageSize64 uint64 23 | if pageIndex == "" { 24 | pageIndex64 = 1 25 | } else { 26 | pageIndex64, _ = strconv.ParseUint(pageIndex, 10, 64) 27 | } 28 | 29 | if pageSize == "" { 30 | pageSize64 = 10 31 | } else { 32 | pageSize64, _ = strconv.ParseUint(pageSize, 10, 64) 33 | } 34 | 35 | return pageIndex64, pageSize64 36 | } 37 | -------------------------------------------------------------------------------- /pkg/util/qreflect.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // AttrToUnderscore 获取struct的所有属性并转为下划线模式 8 | func AttrToUnderscore(st interface{}) []string { 9 | t := reflect.ValueOf(st) 10 | vType := t.Elem().Type() 11 | names := make([]string, 0) 12 | for i := 0; i < vType.NumField(); i++ { 13 | if vType.Field(i).Type.Kind() == reflect.Struct { 14 | continue 15 | } 16 | name := vType.Field(i).Name 17 | if name != "" { 18 | names = append(names, UnderscoreName(name)) 19 | } 20 | } 21 | return names 22 | } 23 | -------------------------------------------------------------------------------- /pkg/util/qreflect_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "gopkg.in/go-playground/assert.v1" 5 | "testing" 6 | ) 7 | 8 | func TestStructAttrToUnderscore(t *testing.T) { 9 | 10 | names := AttrToUnderscore(&struct { 11 | MessageID uint64 12 | Name string 13 | UserAge int 14 | }{}) 15 | assert.Equal(t, "message_id", names[0]) 16 | assert.Equal(t, "name", names[1]) 17 | assert.Equal(t, "user_age", names[2]) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/util/sha.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/hex" 8 | ) 9 | 10 | func HmacSha256(message string, secret string) string { 11 | key := []byte(secret) 12 | h := hmac.New(sha256.New, key) 13 | h.Write([]byte(message)) 14 | sha := hex.EncodeToString(h.Sum(nil)) 15 | return base64.StdEncoding.EncodeToString([]byte(sha)) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/util/sign.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | func GetSignStr(params map[string]interface{}) string { 9 | var signStr string 10 | keys := make([]string, 0, len(params)) 11 | for k := range params { 12 | keys = append(keys, k) 13 | } 14 | sort.Strings(keys) 15 | 16 | for i, k := range keys { 17 | v := params[k] 18 | if v == "" { 19 | continue 20 | } 21 | vs := ObjToStr(v) 22 | 23 | signStr = fmt.Sprintf("%s=%s", k, vs) 24 | 25 | if i != len(keys)-1 { 26 | signStr += "&" 27 | } 28 | } 29 | return signStr 30 | } 31 | 32 | func ObjToStr(v interface{}) string { 33 | var strV string 34 | switch v.(type) { 35 | 36 | case int: 37 | strV = fmt.Sprintf("%d", v) 38 | case uint: 39 | strV = fmt.Sprintf("%d", v) 40 | case int64: 41 | strV = fmt.Sprintf("%d", v) 42 | case uint64: 43 | strV = fmt.Sprintf("%d", v) 44 | case int8: 45 | strV = fmt.Sprintf("%d", v) 46 | case uint8: 47 | strV = fmt.Sprintf("%d", v) 48 | case int16: 49 | strV = fmt.Sprintf("%d", v) 50 | case uint16: 51 | strV = fmt.Sprintf("%d", v) 52 | case int32: 53 | strV = fmt.Sprintf("%d", v) 54 | case uint32: 55 | strV = fmt.Sprintf("%s", v) 56 | case string: 57 | strV = fmt.Sprintf("%s", v) 58 | case float32: 59 | strV = fmt.Sprintf("%s", v) 60 | case float64: 61 | strV = fmt.Sprintf("%s", v) 62 | default: 63 | strV = fmt.Sprintf("%s", v) 64 | } 65 | return strV 66 | } 67 | -------------------------------------------------------------------------------- /pkg/util/stringbuffer.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "strconv" 7 | ) 8 | 9 | // 内嵌bytes.Buffer,支持连写 10 | type Buffer struct { 11 | *bytes.Buffer 12 | } 13 | 14 | func NewBuffer() *Buffer { 15 | return &Buffer{Buffer: new(bytes.Buffer)} 16 | } 17 | 18 | func (b *Buffer) Append(i interface{}) *Buffer { 19 | switch val := i.(type) { 20 | case int: 21 | b.append(strconv.Itoa(val)) 22 | case int64: 23 | b.append(strconv.FormatInt(val, 10)) 24 | case uint: 25 | b.append(strconv.FormatUint(uint64(val), 10)) 26 | case uint64: 27 | b.append(strconv.FormatUint(val, 10)) 28 | case string: 29 | b.append(val) 30 | case []byte: 31 | b.Write(val) 32 | case rune: 33 | b.WriteRune(val) 34 | } 35 | 36 | return b 37 | } 38 | 39 | func (b *Buffer) append(s string) *Buffer { 40 | defer func() { 41 | if err := recover(); err != nil { 42 | log.Println("*****内存不够了!******") 43 | } 44 | }() 45 | 46 | b.WriteString(s) 47 | return b 48 | } -------------------------------------------------------------------------------- /pkg/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "time" 4 | 5 | func ToyyyyMMddHHmm(tm time.Time) string { 6 | 7 | return tm.Format("2006-01-02 15:04") 8 | } 9 | 10 | func ToyyyyMMddHHmmss(tm time.Time) string { 11 | 12 | return tm.Format("2006-01-02 15:04:05") 13 | } 14 | 15 | func ToyyyyMM2(tm time.Time) string { 16 | 17 | return tm.Format("200601") 18 | } 19 | 20 | func ToyyyyMMdd(tm time.Time) string { 21 | 22 | return tm.Format("20060102") 23 | } 24 | 25 | func PareTimeStrForYYYYMMdd(timeStr string) (time.Time, error) { 26 | return time.Parse("20060102", timeStr) 27 | } 28 | 29 | //带-的日期 30 | func Toyyyy_MM_dd(tm time.Time) string { 31 | 32 | return tm.Format("2006-01-02") 33 | } 34 | 35 | func Toyyyy_MM(tm time.Time) string { 36 | 37 | return tm.Format("2006-01") 38 | } 39 | 40 | func PareTimeStrForYYYY_mm_dd(timeStr string) (time.Time, error) { 41 | return time.Parse("2006-01-02", timeStr) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/util/yuan_cent.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "fmt" 4 | 5 | // YuanToCent 元转分 6 | func YuanToCent(yuan float64) int64 { 7 | dec, err := NewFromString(fmt.Sprintf("%0.2f", yuan)) 8 | CheckErr(err) 9 | m, err := NewFromString("100") 10 | CheckErr(err) 11 | 12 | return dec.Mul(m).IntPart() 13 | } 14 | 15 | // CentToYuan 分转元 16 | func CentToYuan(cent int64) float64 { 17 | centDec, err := NewFromString(fmt.Sprintf("%d", cent)) 18 | CheckErr(err) 19 | mDec, err := NewFromString(fmt.Sprintf("%d", 100)) 20 | CheckErr(err) 21 | 22 | result, _ := centDec.Div(mDec).Round(2).Float64() 23 | CheckErr(err) 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /pkg/wait/wait.go: -------------------------------------------------------------------------------- 1 | package wait 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | ) 7 | 8 | const ( 9 | // To avoid lock contention we use an array of list struct (rw mutex & map) 10 | // for the id argument, we apply mod operation and uses its remainder to 11 | // index into the array and find the corresponding element. 12 | defaultListElementLength = 64 13 | ) 14 | 15 | // Wait Wait 16 | type Wait interface { 17 | // Register waits returns a chan that waits on the given ID. 18 | // The chan will be triggered when Trigger is called with 19 | // the same ID. 20 | Register(id uint64) <-chan interface{} 21 | // Trigger triggers the waiting chans with the given ID. 22 | Trigger(id uint64, x interface{}) 23 | IsRegistered(id uint64) bool 24 | } 25 | 26 | type list struct { 27 | e []listElement 28 | } 29 | 30 | type listElement struct { 31 | l sync.RWMutex 32 | m map[uint64]chan interface{} 33 | } 34 | 35 | // New creates a Wait. 36 | func New() Wait { 37 | res := list{ 38 | e: make([]listElement, defaultListElementLength), 39 | } 40 | for i := 0; i < len(res.e); i++ { 41 | res.e[i].m = make(map[uint64]chan interface{}) 42 | } 43 | return &res 44 | } 45 | 46 | func (w *list) Register(id uint64) <-chan interface{} { 47 | idx := id % defaultListElementLength 48 | newCh := make(chan interface{}, 1) 49 | w.e[idx].l.Lock() 50 | defer w.e[idx].l.Unlock() 51 | if _, ok := w.e[idx].m[id]; !ok { 52 | w.e[idx].m[id] = newCh 53 | } else { 54 | log.Panicf("dup id %x", id) 55 | } 56 | return newCh 57 | } 58 | 59 | func (w *list) Trigger(id uint64, x interface{}) { 60 | idx := id % defaultListElementLength 61 | w.e[idx].l.Lock() 62 | ch := w.e[idx].m[id] 63 | delete(w.e[idx].m, id) 64 | w.e[idx].l.Unlock() 65 | if ch != nil { 66 | ch <- x 67 | close(ch) 68 | } 69 | } 70 | 71 | func (w *list) IsRegistered(id uint64) bool { 72 | idx := id % defaultListElementLength 73 | w.e[idx].l.RLock() 74 | defer w.e[idx].l.RUnlock() 75 | _, ok := w.e[idx].m[id] 76 | return ok 77 | } 78 | -------------------------------------------------------------------------------- /pkg/wkevent/event.go: -------------------------------------------------------------------------------- 1 | package wkevent 2 | 3 | import "github.com/gocraft/dbr/v2" 4 | 5 | type Type int 6 | 7 | const ( 8 | // None 无 9 | None Type = iota 10 | // Message 发送消息事件 11 | Message 12 | // CMD CMD 13 | CMD 14 | ) 15 | 16 | func (t Type) Int() int { 17 | return int(t) 18 | } 19 | 20 | type Status int 21 | 22 | const ( 23 | Wait Type = iota // 等待发布 24 | Success // 发布重构 25 | Fail 26 | ) 27 | 28 | func (s Status) Int() int { 29 | return int(s) 30 | } 31 | 32 | type Data struct { 33 | Event string // 事件标示 34 | Type Type // 事件类型 35 | Data interface{} // 事件数据 36 | } 37 | type Event interface { 38 | // 开启事件 39 | Begin(data *Data, tx *dbr.Tx) (int64, error) 40 | // 提交事件 41 | Commit(eventId int64) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/wkhook/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./pkg/wkhook/webhook.proto -------------------------------------------------------------------------------- /pkg/wkhook/webhook.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wkhook; 4 | 5 | option go_package = "./;wkhook"; 6 | 7 | service WebhookService { 8 | // 发送webhook事件 9 | rpc SendWebhook (EventReq) returns (EventResp); 10 | } 11 | 12 | enum EventStatus { 13 | Error = 0; 14 | Success = 1; 15 | } 16 | 17 | message EventReq { 18 | string event = 1; 19 | bytes data = 2; 20 | } 21 | 22 | message EventResp { 23 | EventStatus status = 1; 24 | bytes data = 2; 25 | } -------------------------------------------------------------------------------- /pkg/wkrsa/rsa.go: -------------------------------------------------------------------------------- 1 | package wkrsa 2 | 3 | import ( 4 | "crypto" 5 | "crypto/md5" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/x509" 9 | "encoding/base64" 10 | "encoding/pem" 11 | "errors" 12 | ) 13 | 14 | // Sign rsa签名 15 | // pemPrivKey 私钥key 类似 -----BEGIN RSA PRIVATE KEY----- 16 | // xxxx 17 | // -----END RSA PRIVATE KEY----- 18 | func SignWithMD5(data []byte, pemPrivKey []byte) (string, error) { 19 | hashMd5 := md5.Sum(data) 20 | hashed := hashMd5[:] 21 | block, _ := pem.Decode(pemPrivKey) 22 | if block == nil { 23 | return "", errors.New("private key error") 24 | } 25 | privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) 26 | if err != nil { 27 | return "", err 28 | } 29 | signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.MD5, hashed) 30 | return base64.StdEncoding.EncodeToString(signature), err 31 | } 32 | -------------------------------------------------------------------------------- /pkg/wkrsa/rsa_test.go: -------------------------------------------------------------------------------- 1 | package wkrsa 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSignWithMD5(t *testing.T) { 9 | 10 | dz := `-----BEGIN RSA PRIVATE KEY----- 11 | MIIEowIBAAKCAQEAxGbKnrOumh0r4FfRDL3NIGP7scxxvsVupktTdFqf8TgHLT67 12 | qfMXxM/JSAmNxJVz4zS8zVla4FlTJ4kyh3IJW+65vzXKX/errnI0gNk0p3IME+Wd 13 | LDetwy04Dk40B4GhMrfnomMff6W7UXG4fyLX9gzCIhkAeLNNl6eZS8IOgFULWZRO 14 | pYdlylncY4EQiv6ljXQtKmTIxvi7ivS/9sSFqgS+qnbd0QexrZlM1D2O4abFgrjs 15 | BaFHC4zGrg3TztiWynGUSOpjFv2v92doxDgSxP69xNry9Vq+6kERJpbqvJ3ujUEY 16 | T4bLk8avXNjyMg79/xNeiQ4gxvSHj3v3YOOlIQIDAQABAoIBADbY9fDIARSs3Nnz 17 | 7D+Aqc5H3bxTedhqznHGS3IM9OmqWea6xDG734Fo/a8Oa/bgPdLPoYI/V++bQmui 18 | FuhYYmC4FEtfvDp8sgcvgZYSEnBImzLbRr9YdUAyWps0H7eQ7fF6BkgFIoDFScB+ 19 | 36Uxl9nwyi43iTgr6plVhqvvb5lKqPQTZWWI50hs0EYcADngy2st6+sJxJXylCI4 20 | w3vjXhE9WdwBk54QD6FPPrzRHWpqHDjPcuyxbVijEoz0YMdO+tnHweUn+YnQnHxi 21 | rswF7b9/DL8mhIX67SKqF7wM+RCgrweVswNCcEsZyfCtMKgI7uwDjaDkvhI+XaYq 22 | vfNUe9kCgYEAzK+Zb3A4ZxPWhYrubqmGYa7KvmtetRO5LAyWXMFH5vzU+TKzDcJm 23 | qZTxhzH8yt43PgrjOL5C+X2cIkNfEXHl9oIuuAMbMDSqThvox+o8Lsz5gTKy9/rR 24 | sucBRc41N/axLalVpbevD3Q3+2WjH25ap7h0RsZsladshEoxXI8jpCMCgYEA9aOD 25 | SSoywbVrsMXhr2wgxNXUuQGxWH6x3ZbAG86xxerElVwT06SbowDO77xFmiQsUvlW 26 | BpYrVPBg48B/Sgvvb0mKHhXVe8Maegq6bCofVpAU9IhDZokwIGJgEm2MgziZJAe+ 27 | 3/2DCFV22olrkqDhllqBTenuSyiIfWcGHr+zM+sCgYB0EWNVgPJK6UHtajH4iKMO 28 | Q1rujd4fmnaXlu+w211Vi6uNQAWu2Lz0juRDQMJTm50BzpS4uZMq/OKLv15qewbn 29 | OT0a1ZAWTtcAAe2HZ7kG5O7bJ4+69PzykPH0zpD5Eie4d9x8Y2OexM11/lV43lAD 30 | 6aHt/FjYqB7uCVBiZzzTtwKBgQDGY+jN9+IEp4UxwbCUYQ1aTKXBQoe8xJ7dLDs+ 31 | ekMEaaeaRkLRJdp53VZFM9c3Nk4COdTr/u9Ca96lM7zazib0x/1gbRv+GEbTGMUW 32 | RTMIU9hI46EkOFsBXNLhL09UUCsHeaYE/JiO64/R0zlptLxeFfznM6+9TiBmwAWm 33 | YgfXPwKBgDn1n2aWpGpH/cz8AQapMxqo7YK2Lc/Vwqw7O4mmNQ0TOJOgECHrWJX2 34 | 2UfOnyh8mDNFq9Udt4XMkfH5aPmWF7ejl2ddz3A4kdz5DMaVgRPktJh6nFAllKz/ 35 | R0a+FMM2fsqqRjYN4Zf84pJvWnIy7dG/pXKq8AAWkV7iJNVpshGb 36 | -----END RSA PRIVATE KEY-----` 37 | fmt.Println("privateKeyBuff.Bytes()-->", dz) 38 | 39 | _, err := SignWithMD5([]byte("test"), []byte(dz)) 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /testenv/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | redis: 4 | image: redis 5 | restart: always 6 | ports: 7 | - 6379:6379 8 | mysql: 9 | image: mysql:8.0.33 10 | command: --default-authentication-plugin=mysql_native_password 11 | restart: always 12 | environment: 13 | MYSQL_ROOT_PASSWORD: demo 14 | MYSQL_DATABASE: test 15 | ports: 16 | - 3306:3306 17 | wukongim: # wukongim底层通讯服务 18 | image: wukongim/wukongim:latest 19 | restart: always 20 | volumes: 21 | - ./wukongimdata:/root/wukongim 22 | environment: 23 | WK_MODE: "debug" 24 | WK_EXTERNAL_IP: "192.168.99.219" 25 | ports: 26 | - 5001:5001 # http api 端口 27 | - 5100:5100 # tcp端口 28 | - 5200:5200 # websocket端口 29 | - 5300:5300 # monitor端口 30 | minio: # minio文件管理服务 31 | image: minio/minio:latest # use a remote image 32 | restart: always 33 | command: "server /data --console-address ':9001'" 34 | ports: 35 | - "9000:9000" 36 | - "9001:9001" 37 | environment: 38 | - MINIO_ROOT_USER=admin 39 | - MINIO_ROOT_PASSWORD=12345678 40 | volumes: 41 | - ./miniodata:/data --------------------------------------------------------------------------------