├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── main.go ├── config ├── config.yaml ├── model.conf └── viper.go ├── deploy ├── filebeat │ └── conf │ │ └── filebeat.yml ├── grafana │ └── provisioning │ │ ├── dashboards │ │ ├── dashboard-config.yaml │ │ └── dashboard.json │ │ └── datasources │ │ └── datasource.yaml ├── init │ ├── linkme.sql │ ├── linux_init.sh │ └── windows_init.bat ├── logstash │ ├── conf │ │ └── logstash.yml │ └── pipeline │ │ └── logstash.conf ├── nginx │ └── conf.d │ │ └── linkme-gateway.conf ├── prometheus │ └── server │ │ └── prometheus.yml └── yaml │ ├── canal.yaml │ ├── es.yaml │ ├── grafana.yaml │ ├── kafka.yaml │ ├── kibana.yaml │ ├── logstash.yaml │ ├── mongo.yaml │ ├── mysql.yaml │ ├── prometheus.yaml │ └── redis.yaml ├── doc ├── LinkMe项目启动文档.md ├── function_module.md └── project_interview_highlights.md ├── docker-compose-env.yaml ├── docker-compose.yaml ├── go.mod ├── go.sum ├── internal ├── api │ ├── activity.go │ ├── api.go │ ├── check.go │ ├── comment.go │ ├── history.go │ ├── lotteryDraw.go │ ├── menu.go │ ├── permission.go │ ├── plate.go │ ├── post.go │ ├── ranking.go │ ├── relation.go │ ├── req │ │ ├── activity_req.go │ │ ├── check_req.go │ │ ├── comment_req.go │ │ ├── history_req.go │ │ ├── lotteryDraw_req.go │ │ ├── plate_req.go │ │ ├── post_req.go │ │ ├── ranking_req.go │ │ ├── relation_req.go │ │ ├── role_req.go │ │ ├── search_req.go │ │ └── user_req.go │ ├── role.go │ ├── search.go │ └── user.go ├── constants │ ├── activity.go │ ├── check.go │ ├── comment.go │ ├── general.go │ ├── history.go │ ├── lotteryDraw.go │ ├── plate.go │ ├── post.go │ ├── ranking.go │ ├── relation.go │ ├── search.go │ ├── sms.go │ └── user.go ├── domain │ ├── activity.go │ ├── all.go │ ├── check.go │ ├── comment.go │ ├── events │ │ ├── canal │ │ │ ├── consumer_test.go │ │ │ └── kafka_connector搭建 │ │ ├── check │ │ │ ├── consumer.go │ │ │ ├── dlq_processor.go │ │ │ └── producer.go │ │ ├── comment │ │ │ ├── consumer.go │ │ │ └── producer.go │ │ ├── email │ │ │ ├── consumer.go │ │ │ └── producer.go │ │ ├── es │ │ │ ├── consumer.go │ │ │ ├── consumer_test.go │ │ │ └── process_util.go │ │ ├── post │ │ │ ├── consumer.go │ │ │ ├── dlq_processor.go │ │ │ └── producer.go │ │ ├── publish │ │ │ ├── consumer.go │ │ │ ├── dlq_process.go │ │ │ └── producer.go │ │ ├── sms │ │ │ ├── consumer.go │ │ │ └── producer.go │ │ └── types.go │ ├── history.go │ ├── log.go │ ├── lotteryDraw.go │ ├── mysql_job.go │ ├── plate.go │ ├── post.go │ ├── rankingparameters.go │ ├── relation.go │ ├── role.go │ ├── search.go │ ├── sms.go │ └── user.go ├── job │ ├── interfaces │ │ └── ranking.go │ ├── refresh_cache.go │ ├── routes.go │ ├── scheduler.go │ ├── timed_task.go │ └── types.go ├── mock │ └── user.go ├── repository │ ├── activity.go │ ├── api.go │ ├── cache │ │ ├── check.go │ │ ├── comment.go │ │ ├── email.go │ │ ├── history.go │ │ ├── interactive.go │ │ ├── local.go │ │ ├── lotteryDraw.go │ │ ├── post.go │ │ ├── ranking.go │ │ ├── redis_cmd.go │ │ ├── relation.go │ │ ├── script │ │ │ └── commit.lua │ │ ├── sms.go │ │ └── user.go │ ├── check.go │ ├── comment.go │ ├── dao │ │ ├── activity.go │ │ ├── api.go │ │ ├── check.go │ │ ├── comment.go │ │ ├── init.go │ │ ├── interactive.go │ │ ├── lotteryDraw.go │ │ ├── menu.go │ │ ├── permission.go │ │ ├── plate.go │ │ ├── post.go │ │ ├── rankingparameters.go │ │ ├── relation.go │ │ ├── role.go │ │ ├── search.go │ │ ├── sms.go │ │ ├── sql_job.go │ │ └── user.go │ ├── email.go │ ├── history.go │ ├── interactive.go │ ├── lotteryDraw.go │ ├── menu.go │ ├── permission.go │ ├── plate.go │ ├── post.go │ ├── ranking.go │ ├── rankingparameters.go │ ├── relation.go │ ├── role.go │ ├── search.go │ ├── search_test.go │ ├── sms.go │ ├── sms_test.go │ ├── sql_job.go │ └── user.go └── service │ ├── activity.go │ ├── api.go │ ├── check.go │ ├── comment.go │ ├── history.go │ ├── im.go │ ├── interactive.go │ ├── lotteryDraw.go │ ├── menu.go │ ├── mocks │ ├── sms.mock.go │ └── user.mock.go │ ├── permission.go │ ├── plate.go │ ├── post.go │ ├── ranking.go │ ├── relation.go │ ├── role.go │ ├── search.go │ ├── sql_job.go │ └── user.go ├── ioc ├── asynq.go ├── casbin.go ├── cmd.go ├── db.go ├── es.go ├── kafka.go ├── liniter.go ├── logger.go ├── middleware.go ├── mongo.go ├── redis.go ├── sms.go ├── snowflake.go ├── web.go ├── wire.go └── wire_gen.go ├── middleware ├── log.go ├── login.go └── role.go ├── modd.conf ├── pkg ├── apiresponse │ └── apiresponse.go ├── cachep │ ├── bloom │ │ └── cachebloom.go │ ├── local │ │ └── local.go │ └── prometheus │ │ └── prometheus.go ├── canalp │ └── message.go ├── change │ └── change.go ├── email │ ├── qq.go │ └── qqEmail_test.go ├── general │ └── general.go ├── ginp │ ├── prometheus │ │ └── prometheus.go │ ├── result.go │ └── wrapper.go ├── gormp │ └── prometheus │ │ └── prometheus.go ├── limiterp │ ├── limit.go │ └── limit_slide_window.lua ├── priorityqueue │ └── priority_queue.go ├── samarap │ ├── handler.go │ ├── handler_batch.go │ └── prometheus │ │ └── prometheus.go ├── slicetools │ └── slice_tools.go └── sms │ └── tencentSms.go └── utils ├── AiCheck ├── doubao.go └── doubao_test.go ├── contentfilter ├── contentfilter.go └── sensitive-words.txt ├── jwt └── jwt.go ├── sms.go └── test └── testMySQL.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.3-alpine 2 | 3 | ENV GOPROXY=https://goproxy.cn,direct 4 | ENV TZ=Asia/Shanghai 5 | 6 | RUN apk update --no-cache && apk add --no-cache tzdata 7 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ >/etc/timezone 8 | RUN go install github.com/cortesi/modd/cmd/modd@latest 9 | 10 | WORKDIR /go 11 | 12 | CMD ["modd"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Bamboo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME=linkme/gomodd:v1.22.3 2 | 3 | # 创建数据目录并提权 4 | init: 5 | mkdir -p ./data/kafka/data && chmod -R 777 ./data/kafka 6 | mkdir -p ./data/es/data && chmod -R 777 ./data/es 7 | 8 | # 启动项目依赖 9 | env-up: 10 | docker-compose -f docker-compose-env.yaml up -d 11 | 12 | # 构建 Docker 镜像 13 | build: 14 | docker build -t $(IMAGE_NAME) . 15 | 16 | # 启动项目 17 | up: build 18 | docker-compose up -d 19 | 20 | # 停止项目 21 | down: 22 | docker-compose down --remove-orphans 23 | 24 | # 重新构建并启动 25 | rebuild: down build up 26 | 27 | # 清理所有容器和数据 28 | clean: 29 | docker-compose -f docker-compose-env.yaml down --remove-orphans 30 | docker-compose down --remove-orphans 31 | rm -rf ./data 32 | 33 | # 一键部署 - 执行完整的部署流程 34 | deploy: init env-up build up 35 | @echo "项目部署完成!" 36 | @echo "访问 http://localhost:8888 查看项目" 37 | 38 | # 一键重新部署 - 清理后重新部署 39 | redeploy: clean init deploy 40 | 41 | # 一键更新 - 拉取最新代码并重新部署 42 | update: 43 | git pull 44 | make redeploy 45 | 46 | # 显示所有容器状态 47 | status: 48 | docker-compose ps 49 | docker-compose -f docker-compose-env.yaml ps 50 | 51 | # 查看项目日志 52 | logs: 53 | docker-compose logs -f -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinkMe - 开源论坛项目 2 | 3 | ![LinkMe](https://socialify.git.ci/wangzijian2002/LinkMe/image?description=1&font=Source%20Code%20Pro&forks=1&issues=1&language=1&logo=https%3A%2F%2Fgithub.com%2Fwangzijian2002%2FLinkMe%2Fassets%2F71474660%2F22ef2063-ab82-481f-898f-29d95fa70236&name=1&pattern=Solid&pulls=1&stargazers=1&theme=Dark) 4 | 5 | ## 项目简介 6 | LinkMe 是一个使用 Go 语言开发的论坛项目。它旨在为用户提供一个简洁、高效、可扩展的在线交流平台。本项目使用DDD领域设计理念,采用模块化的设计,使得添加新功能或进行定制化修改变得非常容易。LinkMe 支持多种数据库后端,并且可以通过 Kubernetes 进行部署。 7 | 8 | ## 在线地址 9 | http://www.simplecloud.top/(前端重构中...暂时停用) 10 | 超级管理员:admin/admin 11 | 12 | ## 项目部署文档 13 | [启动文档](./doc/LinkMe项目启动文档.md) 14 | 15 | ## 微服务项目地址 16 | https://github.com/GoSimplicity/LinkMe-microservices 17 | 18 | ## 前端项目地址 19 | https://github.com/GoSimplicity/LinkMe-web 20 | 21 | ## 功能特性 22 | - 用户注册、登录、注销 23 | - 用户角色RBAC 24 | - 用户关系 25 | - 用户评论 26 | - 发布帖子、评论、点赞 27 | - 历史记录 28 | - 帖子审核 29 | - 用户个人资料编辑 30 | - 论坛版块管理 31 | - 自动获取热门榜单 32 | - 用户、帖子搜索 33 | - Kubernetes 一键部署 34 | - 前后端分离架构 35 | 36 | ## 技术栈 37 | - Go 语言 38 | - Gin Web 框架 39 | - Wire 依赖注入 40 | - Kubernetes 集群管理 41 | - MySQL 数据库 42 | - Redis 缓存数据库 43 | - MongoDB 文档数据库 44 | - Kafka 消息队列 45 | - Prometheus 监控 46 | - ELK 日志收集 47 | - Canal 数据同步 48 | - ElasticSearch 搜索引擎 49 | - Docker 容器化 50 | - 随项目进度技术栈实时更新.. 51 | 52 | ## 目录结构 53 | ``` 54 | . 55 | ├── config # 项目配置文件目录 56 | ├── deploy # docker及k8s部署文件目录 57 | ├── doc # 项目文档目录 58 | ├── internal # 项目内部包,含核心业务逻辑 59 | ├── ioc # IoC容器配置,负责依赖注入设置 60 | ├── pkg # 自定义工具包与库 61 | ├── job # 定时任务目录 62 | ├── logs # 项目日志目录 63 | ├── utils # 项目工具包目录 64 | ├── main.go # 项目入口文件 65 | ├── middleware # 中间件目录 66 | ├── tmp # 临时文件目录 67 | ├── wire_gen.go # Wire工具生成的代码 68 | ├── wire.go # Wire配置,声明依赖注入关系 69 | ``` 70 | 71 | ## 如何贡献 72 | 我们欢迎任何形式的贡献,包括但不限于: 73 | - 提交代码(Pull Requests) 74 | - 报告问题(Issues) 75 | - 文档改进 76 | - 功能建议 77 | 请确保在贡献代码之前阅读了我们的[贡献指南](#贡献指南)。 78 | 79 | ## 贡献指南 80 | - Fork 本仓库 81 | - 创建您的特性分支 (`git checkout -b my-new-feature`) 82 | - 提交您的改动 (`git commit -am 'Add some feature'`) 83 | - 将您的分支推送到 GitHub (`git push origin my-new-feature`) 84 | - 创建一个 Pull Request 85 | ## 开始使用 86 | 87 | ### 克隆项目 88 | ```bash 89 | git@github.com:GoSimplicity/LinkMe.git 90 | ``` 91 | 92 | ### 环境要求 93 | - Docker 20.10.0+ 94 | - Docker Compose 2.0.0+ 95 | 96 | ### 部署步骤(一键部署) 97 | ```bash 98 | make deploy 99 | ``` 100 | 101 | #### 一键重新部署 102 | ```bash 103 | make redeploy 104 | ``` 105 | 106 | ### 部署步骤(手动部署) 107 | 108 | #### 创建数据目录并提权 109 | ```bash 110 | mkdir -p ./data/kafka/data && chmod -R 777 ./data/kafka 111 | ``` 112 | 113 | #### 启动项目依赖 114 | ```bash 115 | docker-compose -f docker-compose-env.yaml up -d 116 | ``` 117 | 118 | #### 构建镜像 119 | ```bash 120 | docker build -t linkme/gomodd:v1.22.3 . 121 | ``` 122 | 123 | #### 启动项目 124 | ```bash 125 | docker-compose up -d 126 | ``` 127 | 128 | ### 项目超级管理员账号 129 | ```bash 130 | admin/admin 131 | ``` 132 | 133 | 134 | ## 许可证 135 | 本项目使用 MIT 许可证,详情请见 [LICENSE](./LICENSE) 文件。 136 | 137 | ## 联系方式 138 | - Email: [wzijian62@gmail.com](mailto:wzijian62@gmail.com) 139 | - 为了方便交流,可以加我vx:GoSimplicity 我拉你进微信群 140 | 141 | ## 致谢 142 | 感谢所有为本项目做出贡献的人! 143 | 144 | --- 145 | 欢迎来到 LinkMe,让我们一起构建更好的论坛社区! 146 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "github.com/GoSimplicity/LinkMe/internal/domain/events" 12 | "github.com/GoSimplicity/LinkMe/ioc" 13 | "github.com/spf13/viper" 14 | 15 | "github.com/GoSimplicity/LinkMe/config" 16 | "github.com/gin-gonic/gin" 17 | "github.com/prometheus/client_golang/prometheus/promhttp" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | func main() { 22 | Init() 23 | } 24 | 25 | func Init() { 26 | // 初始化配置 27 | config.InitViper() 28 | 29 | // 初始化 Web 服务器和其他组件 30 | cmd := ioc.InitWebServer() 31 | 32 | server := cmd.Server 33 | server.GET("/headers", printHeaders) 34 | 35 | // 创建一个用于接收系统信号的通道 36 | quit := make(chan os.Signal, 1) 37 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 38 | 39 | // 启动 Prometheus 监控 40 | go func() { 41 | 42 | if err := startMetricsServer(); err != nil { 43 | zap.L().Fatal("启动监控服务器失败", zap.Error(err)) 44 | } 45 | }() 46 | 47 | // 启动定时任务和worker 48 | go func() { 49 | if err := cmd.Scheduler.RegisterTimedTasks(); err != nil { 50 | zap.L().Fatal("注册定时任务失败", zap.Error(err)) 51 | } 52 | 53 | if err := cmd.Scheduler.Run(); err != nil { 54 | zap.L().Fatal("启动定时任务失败", zap.Error(err)) 55 | } 56 | }() 57 | 58 | // 启动消费者 59 | for _, s := range cmd.Consumer { 60 | go func(consumer events.Consumer) { 61 | if err := consumer.Start(context.Background()); err != nil { 62 | zap.L().Fatal("启动消费者失败", zap.Error(err)) 63 | } 64 | }(s) 65 | } 66 | 67 | // 注册任务处理器并启动异步任务服务器 68 | go func() { 69 | mux := cmd.Routes.RegisterHandlers() 70 | if err := cmd.Asynq.Run(mux); err != nil { 71 | zap.L().Fatal("启动异步任务服务器失败", zap.Error(err)) 72 | } 73 | }() 74 | 75 | // 在新的goroutine中启动服务器 76 | go func() { 77 | if err := server.Run(viper.GetString("server.addr")); err != nil { 78 | zap.L().Fatal("启动Web服务器失败", zap.Error(err)) 79 | } 80 | }() 81 | 82 | // 等待中断信号 83 | <-quit 84 | zap.L().Info("正在关闭服务器...") 85 | 86 | // 关闭异步任务服务器 87 | cmd.Asynq.Shutdown() 88 | 89 | cmd.Scheduler.Stop() 90 | 91 | zap.L().Info("服务器已成功关闭") 92 | os.Exit(0) 93 | } 94 | 95 | // printHeaders 打印请求头信息 96 | func printHeaders(c *gin.Context) { 97 | headers := c.Request.Header 98 | for key, values := range headers { 99 | for _, value := range values { 100 | c.String(http.StatusOK, "%s: %s\n", key, value) 101 | } 102 | } 103 | } 104 | 105 | // startMetricsServer 启动 Prometheus 监控服务器 106 | func startMetricsServer() error { 107 | http.Handle("/metrics", promhttp.Handler()) 108 | // 启动 HTTP 服务器并捕获可能的错误 109 | if err := http.ListenAndServe(":9091", nil); err != nil { 110 | log.Fatalf("Prometheus 启动失败: %v", err) 111 | return err 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | addr: ":9999" 3 | 4 | jwt: 5 | auth_key: "ebe3vxIP7sblVvUHXb7ZaiMPuz4oXo0l" 6 | refresh_key: "sadfkhjlkkljKFJDSLAFUDASLFJKLjfj113d2" 7 | issuer: "K5mBPBYNQeNWEBvCTE5msog3KSGTdhmI" 8 | auth_expire: 30 # 分钟 9 | refresh_expire: 150 # 小时 10 | 11 | log: 12 | dir: "logs" 13 | 14 | db: 15 | # dsn: "root:v6SxhWHyZC7S@tcp(mysql:3306)/linkme?charset=utf8mb4&parseTime=True&loc=Local" 16 | dsn: "root:root@tcp(localhost:3306)/linkme?charset=utf8mb4&parseTime=True&loc=Local" 17 | 18 | redis: 19 | # addr: "redis:6379" 20 | addr: "localhost:6379" 21 | password: "v6SxhWHyZC7S" 22 | 23 | kafka: 24 | # addr: "kafka:9092" 25 | addr: "localhost:9092" 26 | 27 | es: 28 | addr: "http://localhost:9200" 29 | 30 | sms: 31 | tencent: 32 | secretId: "" 33 | secretKey: "" 34 | endPoint: "" 35 | smsID: "" 36 | sign: "" 37 | templateID: "" 38 | 39 | ark_api: 40 | key: "b3977816-2a07-44df-9fe2-4ec02224e147" -------------------------------------------------------------------------------- /config/model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act -------------------------------------------------------------------------------- /config/viper.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // InitViper 初始化viper配置 9 | func InitViper() { 10 | configFile := pflag.String("config", "config/config.yaml", "配置文件路径") 11 | pflag.Parse() 12 | viper.SetConfigFile(*configFile) 13 | err := viper.ReadInConfig() 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /deploy/filebeat/conf/filebeat.yml: -------------------------------------------------------------------------------- 1 | filebeat.inputs: 2 | - type: log 3 | enabled: true 4 | paths: 5 | # 容器日志 6 | - /var/lib/docker/containers/*/*-json.log 7 | 8 | filebeat.config.modules: 9 | path: ${path.config}/modules.d/*.yml 10 | reload.enabled: false 11 | 12 | processors: 13 | - add_cloud_metadata: ~ 14 | - add_docker_metadata: ~ 15 | 16 | output.kafka: 17 | enabled: true 18 | hosts: ["kafka:9092"] 19 | topic: "linkme-log" 20 | # 分区哈希 21 | partition.hash: 22 | reachable_only: true 23 | compression: gzip 24 | # 最大消息字节 25 | max_message_bytes: 1000000 26 | required_acks: 1 27 | -------------------------------------------------------------------------------- /deploy/grafana/provisioning/dashboards/dashboard-config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | providers: 3 | - name: 'default' 4 | orgId: 1 5 | folder: '' 6 | type: file 7 | disableDeletion: false 8 | updateIntervalSeconds: 10 9 | options: 10 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /deploy/grafana/provisioning/datasources/datasource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | datasources: 3 | - name: Prometheus 4 | type: prometheus 5 | access: proxy 6 | url: http://prometheus:9090 7 | isDefault: true -------------------------------------------------------------------------------- /deploy/init/linux_init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS linkme DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;" 3 | USER_EXISTS=$(mysql -uroot -proot -sse "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = 'canal');") 4 | if [ "$USER_EXISTS" != 1 ]; then 5 | mysql -uroot -proot -e "CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';" 6 | fi 7 | mysql -uroot -proot -e "GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' WITH GRANT OPTION;" 8 | mysql -uroot -proot -e "FLUSH PRIVILEGES;" 9 | mysql -uroot -proot linkme < /docker-entrypoint-initdb.d/linkme.sql 10 | echo "Database import complete." -------------------------------------------------------------------------------- /deploy/init/windows_init.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM 创建数据库 3 | mysql -uroot -proot -e "CREATE DATABASE IF NOT EXISTS linkme DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;" 4 | 5 | REM 检查用户是否存在 6 | for /f "tokens=*" %%i in ('mysql -uroot -proot -sse "SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = 'canal');"') do set USER_EXISTS=%%i 7 | 8 | if "%USER_EXISTS%"=="0" ( 9 | mysql -uroot -proot -e "CREATE USER 'canal'@'%%' IDENTIFIED BY 'canal';" 10 | ) 11 | 12 | REM 授予权限 13 | mysql -uroot -proot -e "GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%%' WITH GRANT OPTION;" 14 | mysql -uroot -proot -e "FLUSH PRIVILEGES;" 15 | 16 | REM 获取当前目录 17 | set currentDir=%~dp0 18 | 19 | REM 导入当前目录下的SQL文件 20 | mysql -uroot -proot linkme < "%currentDir%linkme.sql" 21 | 22 | REM 提示完成 23 | echo Database import complete. 24 | pause -------------------------------------------------------------------------------- /deploy/logstash/conf/logstash.yml: -------------------------------------------------------------------------------- 1 | http.host: "0.0.0.0" 2 | path.config: /usr/share/logstash/pipeline 3 | xpack.monitoring.enabled: false 4 | pipeline.workers: 2 5 | pipeline.batch.size: 125 6 | pipeline.batch.delay: 50 7 | queue.type: memory 8 | queue.max_bytes: 1024mb 9 | log.level: info -------------------------------------------------------------------------------- /deploy/logstash/pipeline/logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | kafka { 3 | bootstrap_servers => "kafka:9092" 4 | topics => ["linkme-log"] 5 | group_id => "logstash" 6 | consumer_threads => 2 7 | codec => "json" 8 | } 9 | } 10 | 11 | filter { 12 | date { 13 | match => ["@timestamp", "ISO8601"] 14 | } 15 | 16 | mutate { 17 | remove_field => ["@version", "kafka"] 18 | } 19 | 20 | if [level] == "error" { 21 | mutate { 22 | add_tag => ["error"] 23 | } 24 | } 25 | } 26 | 27 | output { 28 | elasticsearch { 29 | hosts => ["http://elasticsearch:9200"] 30 | index => "linkme-logs-%{+YYYY.MM.dd}" 31 | } 32 | 33 | if "error" in [tags] { 34 | elasticsearch { 35 | hosts => ["http://elasticsearch:9200"] 36 | index => "linkme-error-logs-%{+YYYY.MM.dd}" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /deploy/nginx/conf.d/linkme-gateway.conf: -------------------------------------------------------------------------------- 1 | server{ 2 | listen 8081; 3 | access_log /var/log/nginx/linkme.com_access.log; 4 | error_log /var/log/nginx/linkme.com_error.log; 5 | 6 | 7 | location ~ /api/ { 8 | proxy_set_header Host $http_host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header REMOTE-HOST $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | proxy_pass http://linkme:9999; 13 | } 14 | } -------------------------------------------------------------------------------- /deploy/prometheus/server/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 30s 3 | evaluation_interval: 30s 4 | 5 | alerting: 6 | alertmanagers: 7 | - static_configs: 8 | - targets: [] 9 | 10 | scrape_configs: 11 | - job_name: "prometheus" 12 | static_configs: 13 | - targets: ["localhost:9091"] 14 | 15 | - job_name: "linkme" 16 | metrics_path: /metrics 17 | static_configs: 18 | - targets: ["linkme:9091"] 19 | -------------------------------------------------------------------------------- /deploy/yaml/canal.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: canal-pv 5 | namespace: linkme 6 | spec: 7 | capacity: 8 | storage: 10Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: /data/canal 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: canal-pvc 18 | namespace: linkme 19 | spec: 20 | accessModes: 21 | - ReadWriteOnce 22 | resources: 23 | requests: 24 | storage: 10Gi 25 | --- 26 | apiVersion: apps/v1 27 | kind: Deployment 28 | metadata: 29 | name: linkme-canal 30 | namespace: linkme 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | app: canal 36 | template: 37 | metadata: 38 | labels: 39 | app: canal 40 | spec: 41 | containers: 42 | - name: canal 43 | image: canal/canal-server 44 | ports: 45 | - containerPort: 11111 46 | volumeMounts: 47 | - name: canal-main-config 48 | mountPath: /home/admin/canal-server/conf/canal.properties 49 | - name: canal-sync-config 50 | mountPath: /home/admin/canal-server/conf/linkme_sync/instance.properties 51 | - name: canal-log-storage 52 | mountPath: /home/admin/canal-server/logs 53 | - name: canal-destinations-storage 54 | mountPath: /home/admin/canal-server/destinations 55 | volumes: 56 | - name: canal-main-config 57 | hostPath: 58 | path: /data/canal/conf/canal.properties 59 | - name: canal-sync-config 60 | hostPath: 61 | path: /data/canal/conf/sync/instance.properties 62 | - name: canal-log-storage 63 | hostPath: 64 | path: /data/canal/logs 65 | - name: canal-destinations-storage 66 | hostPath: 67 | path: /data/canal/destinations 68 | 69 | --- 70 | apiVersion: v1 71 | kind: Service 72 | metadata: 73 | name: canal-service 74 | namespace: linkme 75 | spec: 76 | type: NodePort 77 | ports: 78 | - port: 11111 79 | nodePort: 30887 80 | selector: 81 | app: canal -------------------------------------------------------------------------------- /deploy/yaml/es.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: es-pv 5 | namespace: linkme 6 | spec: 7 | capacity: 8 | storage: 10Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: /data/es/data 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: es-pvc 18 | namespace: linkme 19 | spec: 20 | accessModes: 21 | - ReadWriteOnce 22 | resources: 23 | requests: 24 | storage: 10Gi 25 | --- 26 | apiVersion: apps/v1 27 | kind: Deployment 28 | metadata: 29 | name: linkme-es 30 | namespace: linkme 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | app: es 36 | template: 37 | metadata: 38 | labels: 39 | app: es 40 | spec: 41 | containers: 42 | - name: es 43 | image: docker.elastic.co/elasticsearch/elasticsearch:8.12.2 44 | securityContext: 45 | runAsGroup: 0 46 | runAsUser: 0 47 | imagePullPolicy: IfNotPresent 48 | ports: 49 | - containerPort: 9200 50 | - containerPort: 9300 51 | env: 52 | - name: discovery.type 53 | value: single-node 54 | - name: ES_JAVA_OPTS 55 | value: "-Xms1g -Xmx1g" 56 | volumeMounts: 57 | - name: es-storage 58 | mountPath: /usr/share/elasticsearch/data 59 | - name: es-config 60 | mountPath: /usr/share/elasticsearch/config/elasticsearch.yml 61 | subPath: elasticsearch.yml 62 | volumes: 63 | - name: es-storage 64 | persistentVolumeClaim: 65 | claimName: es-pvc 66 | - name: es-config 67 | configMap: 68 | name: es-config 69 | --- 70 | apiVersion: v1 71 | kind: Service 72 | metadata: 73 | name: es-service 74 | namespace: linkme 75 | spec: 76 | type: NodePort 77 | ports: 78 | - port: 9200 79 | name: es-http 80 | nodePort: 30885 81 | - port: 9300 82 | name: es-tcp 83 | nodePort: 30886 84 | selector: 85 | app: es 86 | --- 87 | apiVersion: v1 88 | kind: ConfigMap 89 | metadata: 90 | name: es-config 91 | namespace: linkme 92 | data: 93 | elasticsearch.yml: | 94 | cluster.name: "docker-cluster" 95 | network.host: 0.0.0.0 96 | http.port: 9200 97 | xpack.security.enabled: false # 禁用安全认证 98 | xpack.security.transport.ssl.enabled: false # 禁用传输层安全 -------------------------------------------------------------------------------- /deploy/yaml/grafana.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: linkme-grafana 6 | name: linkme-grafana 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: linkmed-grafana 12 | template: 13 | metadata: 14 | labels: 15 | app: linkme-grafana 16 | spec: 17 | containers: 18 | - image: grafana/grafana:latest 19 | securityContext: 20 | runAsGroup: 0 21 | runAsUser: 0 22 | name: grafana 23 | ports: 24 | - containerPort: 3000 25 | imagePullPolicy: IfNotPresent 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: linkme-grafana 31 | spec: 32 | selector: 33 | app: linkme-grafana 34 | ports: 35 | - protocol: TCP 36 | port: 3000 37 | targetPort: 3000 38 | nodePort: 30900 39 | name: "3000" 40 | type: NodePort 41 | -------------------------------------------------------------------------------- /deploy/yaml/kafka.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: kafka-pv 5 | namespace: linkme 6 | spec: 7 | capacity: 8 | storage: 10Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: /data/kafka 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: kafka-pvc 18 | namespace: linkme 19 | spec: 20 | accessModes: 21 | - ReadWriteOnce 22 | resources: 23 | requests: 24 | storage: 10Gi 25 | --- 26 | apiVersion: apps/v1 27 | kind: Deployment 28 | metadata: 29 | name: linkme-kafka 30 | namespace: linkme 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | app: kafka 36 | template: 37 | metadata: 38 | labels: 39 | app: kafka 40 | spec: 41 | containers: 42 | - name: kafka 43 | image: "bitnami/kafka:3.6.0" 44 | securityContext: 45 | runAsGroup: 0 46 | runAsUser: 0 47 | imagePullPolicy: IfNotPresent 48 | ports: 49 | - containerPort: 9092 50 | - containerPort: 9094 51 | env: 52 | - name: KAFKA_CFG_NODE_ID 53 | value: "0" 54 | - name: KAFKA_CREATE_TOPICS 55 | value: "linkme_binlog:3:1,linkme-sync:3:1,linkme-cache:3:1,linkme-es:3:1" 56 | - name: KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE 57 | value: "true" 58 | - name: KAFKA_CFG_PROCESS_ROLES 59 | value: "controller,broker" 60 | - name: KAFKA_CFG_LISTENERS 61 | value: "PLAINTEXT://0.0.0.0:9092,CONTROLLER://:9093,EXTERNAL://0.0.0.0:9094" 62 | - name: KAFKA_CFG_ADVERTISED_LISTENERS 63 | value: "PLAINTEXT://192.168.1.11:30880,EXTERNAL://192.168.1.11:9094" # 需将此处改为你的宿主机ip 64 | - name: KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP 65 | value: "CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,PLAINTEXT:PLAINTEXT" 66 | - name: KAFKA_CFG_CONTROLLER_QUORUM_VOTERS 67 | value: "0@localhost:9093" 68 | - name: KAFKA_CFG_CONTROLLER_LISTENER_NAMES 69 | value: "CONTROLLER" 70 | - name: KAFKA_CFG_MESSAGE_MAX_BYTES 71 | value: "20971520" 72 | - name: KAFKA_CFG_LOG_DIRS 73 | value: "/bitnami/kafka/data" 74 | volumeMounts: 75 | - name: kafka-storage 76 | mountPath: /bitnami/kafka/data 77 | volumes: 78 | - name: kafka-storage 79 | persistentVolumeClaim: 80 | claimName: kafka-pvc 81 | --- 82 | apiVersion: v1 83 | kind: Service 84 | metadata: 85 | name: kafka-service 86 | namespace: linkme 87 | spec: 88 | type: NodePort 89 | ports: 90 | - name: broker-port 91 | port: 9092 92 | nodePort: 30880 93 | - name: external-port 94 | port: 9094 95 | nodePort: 30881 96 | selector: 97 | app: kafka -------------------------------------------------------------------------------- /deploy/yaml/kibana.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: linkme-kibana 5 | namespace: linkme 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: kibana 11 | template: 12 | metadata: 13 | labels: 14 | app: kibana 15 | spec: 16 | containers: 17 | - name: kibana 18 | image: kibana:8.12.2 19 | imagePullPolicy: IfNotPresent 20 | ports: 21 | - containerPort: 5601 # Kibana 默认端口 22 | volumeMounts: 23 | - name: kibana-config 24 | mountPath: /usr/share/kibana/config/kibana.yml 25 | subPath: kibana.yml 26 | volumes: 27 | - name: kibana-config 28 | configMap: 29 | name: kibana-config 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: kibana-service 35 | namespace: linkme 36 | spec: 37 | type: NodePort 38 | ports: 39 | - port: 5601 40 | name: http 41 | nodePort: 30889 42 | selector: 43 | app: kibana 44 | --- 45 | apiVersion: v1 46 | kind: ConfigMap 47 | metadata: 48 | name: kibana-config 49 | namespace: linkme 50 | data: 51 | kibana.yml: | 52 | server.name: linkme-kibana 53 | server.host: "0.0.0.0" # 监听所有网络接口 54 | elasticsearch.hosts: [ "http://192.168.1.11:30885" ] 55 | xpack.monitoring.ui.container.elasticsearch.enabled: true 56 | i18n.locale: "zh-CN" -------------------------------------------------------------------------------- /deploy/yaml/logstash.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: linkme-logstash 5 | namespace: linkme 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: logstash 11 | template: 12 | metadata: 13 | labels: 14 | app: logstash 15 | spec: 16 | containers: 17 | - name: logstash 18 | image: docker.elastic.co/logstash/logstash:8.12.2 19 | imagePullPolicy: IfNotPresent 20 | ports: 21 | - containerPort: 5044 # Beats input port 22 | - containerPort: 9600 # HTTP monitoring port 23 | volumeMounts: 24 | - name: logstash-config-yml 25 | mountPath: /usr/share/logstash/config/logstash.yml 26 | - name: logstash-config 27 | mountPath: /usr/share/logstash/pipeline/logstash.conf 28 | - name: logstash-storage 29 | mountPath: /data/logstash 30 | volumes: 31 | - name: logstash-config-yml 32 | hostPath: 33 | path: /data/logstash/conf/logstash.yml # 指定配置文件路径 34 | - name: logstash-config 35 | hostPath: 36 | path: /data/logstash/conf/logstash.conf # 指定配置文件路径 37 | - name: logstash-storage 38 | hostPath: 39 | path: /data/logstash # 指定数据持久化目录 40 | type: DirectoryOrCreate 41 | --- 42 | apiVersion: v1 43 | kind: Service 44 | metadata: 45 | name: logstash-service 46 | namespace: linkme 47 | spec: 48 | type: NodePort 49 | ports: 50 | - port: 5044 51 | name: beats-input 52 | nodePort: 30888 53 | - port: 9600 54 | name: http-monitoring 55 | nodePort: 30444 56 | selector: 57 | app: logstash -------------------------------------------------------------------------------- /deploy/yaml/mongo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: mongo-pv 5 | namespace: linkme 6 | spec: 7 | capacity: 8 | storage: 10Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: /data/mongo 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: mongo-pvc 18 | namespace: linkme 19 | spec: 20 | accessModes: 21 | - ReadWriteOnce 22 | resources: 23 | requests: 24 | storage: 10Gi 25 | --- 26 | apiVersion: apps/v1 27 | kind: Deployment 28 | metadata: 29 | name: linkme-mongo 30 | namespace: linkme 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | app: mongo 36 | template: 37 | metadata: 38 | labels: 39 | app: mongo 40 | spec: 41 | containers: 42 | - name: mongo 43 | image: mongo:latest 44 | securityContext: 45 | runAsGroup: 0 46 | runAsUser: 0 47 | imagePullPolicy: IfNotPresent 48 | ports: 49 | - containerPort: 27017 50 | volumeMounts: 51 | - name: mongo-storage 52 | mountPath: /data/db 53 | volumes: 54 | - name: mongo-storage 55 | persistentVolumeClaim: 56 | claimName: mongo-pvc 57 | --- 58 | apiVersion: v1 59 | kind: Service 60 | metadata: 61 | name: mongo-service 62 | namespace: linkme 63 | spec: 64 | type: NodePort 65 | ports: 66 | - port: 27017 67 | nodePort: 30883 68 | selector: 69 | app: mongo -------------------------------------------------------------------------------- /deploy/yaml/mysql.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: mysql-pv 5 | namespace: linkme 6 | spec: 7 | capacity: 8 | storage: 10Gi 9 | accessModes: 10 | - ReadWriteOnce 11 | hostPath: 12 | path: /data/mysql 13 | --- 14 | apiVersion: v1 15 | kind: PersistentVolumeClaim 16 | metadata: 17 | name: mysql-pvc 18 | namespace: linkme 19 | spec: 20 | accessModes: 21 | - ReadWriteOnce 22 | resources: 23 | requests: 24 | storage: 10Gi 25 | --- 26 | apiVersion: apps/v1 27 | kind: Deployment 28 | metadata: 29 | name: linkme-mysql 30 | namespace: linkme 31 | spec: 32 | replicas: 1 33 | selector: 34 | matchLabels: 35 | app: mysql 36 | template: 37 | metadata: 38 | labels: 39 | app: mysql 40 | spec: 41 | containers: 42 | - name: mysql 43 | image: mysql:8.0 44 | securityContext: 45 | runAsGroup: 0 46 | runAsUser: 0 47 | imagePullPolicy: IfNotPresent 48 | ports: 49 | - containerPort: 3306 50 | env: 51 | - name: MYSQL_ROOT_PASSWORD 52 | value: "root" 53 | - name: MYSQL_DATABASE 54 | value: "linkme" 55 | volumeMounts: 56 | - name: mysql-storage 57 | mountPath: /var/lib/mysql 58 | - name: init-storage 59 | mountPath: /docker-entrypoint-initdb.d 60 | volumes: 61 | - name: mysql-storage 62 | persistentVolumeClaim: 63 | claimName: mysql-pvc 64 | - name: init-storage 65 | hostPath: 66 | path: /data/mysql/init 67 | --- 68 | apiVersion: v1 69 | kind: Service 70 | metadata: 71 | name: mysql-service 72 | namespace: linkme 73 | spec: 74 | type: NodePort 75 | ports: 76 | - port: 3306 77 | nodePort: 30882 78 | selector: 79 | app: mysql -------------------------------------------------------------------------------- /deploy/yaml/prometheus.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: linkme-prometheus 5 | labels: 6 | app: linkme-prometheus 7 | namespace: linkme 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: linkme-prometheus 13 | template: 14 | metadata: 15 | name: linkme-prometheus 16 | labels: 17 | app: linkme-prometheus 18 | spec: 19 | containers: 20 | - name: linkme-prometheus 21 | image: bitnami/prometheus:latest 22 | imagePullPolicy: IfNotPresent 23 | securityContext: 24 | runAsGroup: 0 25 | runAsUser: 0 26 | ports: 27 | - containerPort: 9090 28 | volumeMounts: 29 | - mountPath: /bitnami/prometheus/data 30 | name: prometheus-data 31 | - mountPath: /etc/prometheus/prometheus.yml 32 | name: prometheus-yml 33 | volumes: 34 | - name: prometheus-data 35 | persistentVolumeClaim: 36 | claimName: linkme-prometheus-pvc 37 | - name: prometheus-yml 38 | hostPath: 39 | path: /data/prometheus/prometheus.yml 40 | restartPolicy: Always 41 | --- 42 | apiVersion: v1 43 | kind: PersistentVolume 44 | metadata: 45 | name: linkme-prometheus-pv 46 | namespace: linkme 47 | spec: 48 | storageClassName: record 49 | capacity: 50 | storage: 1Gi 51 | accessModes: 52 | - ReadWriteOnce 53 | hostPath: 54 | path: "/data/prometheus" 55 | --- 56 | apiVersion: v1 57 | kind: PersistentVolumeClaim 58 | metadata: 59 | name: linkme-prometheus-pvc 60 | namespace: linkme 61 | spec: 62 | storageClassName: record 63 | accessModes: 64 | - ReadWriteOnce 65 | resources: 66 | requests: 67 | storage: 1Gi 68 | --- 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | name: linkme-prometheus 73 | namespace: linkme 74 | spec: 75 | selector: 76 | app: linkme-prometheus 77 | ports: 78 | - protocol: TCP 79 | port: 9090 80 | targetPort: 9090 81 | nodePort: 30900 82 | type: NodePort 83 | -------------------------------------------------------------------------------- /deploy/yaml/redis.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: linkme-redis 5 | namespace: linkme 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: redis 11 | template: 12 | metadata: 13 | labels: 14 | app: redis 15 | spec: 16 | containers: 17 | - name: redis 18 | image: redis:latest 19 | securityContext: 20 | runAsGroup: 0 21 | runAsUser: 0 22 | imagePullPolicy: IfNotPresent 23 | ports: 24 | - containerPort: 6379 25 | command: 26 | - "redis-server" 27 | - "--bind" 28 | - "0.0.0.0" 29 | - "--protected-mode" 30 | - "no" 31 | - "--port" 32 | - "6379" 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: redis-service 38 | namespace: linkme 39 | spec: 40 | type: NodePort 41 | ports: 42 | - port: 6379 43 | nodePort: 30884 44 | selector: 45 | app: redis -------------------------------------------------------------------------------- /doc/function_module.md: -------------------------------------------------------------------------------- 1 | # 功能模块 2 | 3 | ## 用户模块 4 | 5 | - **用户注册**:用户可以通过填写邮箱、密码、昵称等基本信息来创建账户✅ 6 | - **用户登录**:用户可以使用短信验证或电子邮箱以及密码登录到论坛✅ 7 | - **用户注销**:用户可以选择注销账户,注销后账户信息将被删除或标记为不可用✅ 8 | - **个人资料编辑**:用户可以编辑个人资料,如昵称、头像、个人描述等✅ 9 | - **密码找回**:用户提供电子邮箱或手机号,通过验证后可以重置密码✅ 10 | 11 | ## 论坛模块 12 | 13 | - **发布帖子**:用户可以在论坛上发布新帖子,包括文本、图片、视频等✅ 14 | - **评论管理**:用户可以对帖子进行评论,并管理自己的评论✅ 15 | - **点赞功能**:用户可以对帖子或评论进行点赞,点赞后的帖子可在点赞列表中找到✅ 16 | - **收藏功能**:用户可以对帖子进行收藏,收藏后的帖子可以在收藏夹中找到✅ 17 | - **历史记录**:用户浏览过的帖子可在历史记录中查看✅ 18 | - **版块管理**:管理员可以创建、编辑和删除论坛的版块✅ 19 | - **内容审核**:引入内容审核机制,接入ai辅助管理员进行审核 20 | 21 | ## 内容管理模块 22 | 23 | - **帖子搜索**:用户可以通过关键词搜索帖子(进行中) 24 | - **热门帖子**:根据点赞数、评论数、收藏数等指标,根据热榜算法展示热门帖子,热门帖子每日上午十点更新✅ 25 | - **版块分类**:帖子按版块分类,方便用户按兴趣浏览✅ 26 | 27 | ## 安全与隐私模块 28 | 29 | - **权限控制**:区分用户角色,如普通用户、管理员等,不同角色有不同的操作权限✅ 30 | - **数据加密**:用户敏感信息如密码、邮箱等进行加密存储✅ 31 | - **隐私设置**:用户可以设置个人资料的隐私级别,如公开、仅自己可见、仅好友可见等 32 | 33 | ## 系统管理模块 34 | 35 | - **系统监控**:监控系统运行状态,包括服务器负载、响应时间等 36 | - **日志记录**:记录用户操作日志和系统运行日志,便于审计和问题追踪✅ 37 | - **数据库管理**:支持多种数据库后端,如 MySQL、MongoDB 等✅ 38 | 39 | ## 互动模块 40 | 41 | - **私信功能**:用户之间可以发送私信进行交流 42 | - **好友系统**:用户可以添加好友,形成社交网络 43 | - **通知系统**:系统会推送通知给用户,包括新消息、新评论等 44 | 45 | ## 部署与运维模块 46 | 47 | - **容器化部署**:使用 Docker 容器化应用,便于部署和维护✅ 48 | - **Kubernetes 集群管理**:支持 Kubernetes 一键部署,实现高可用和自动扩展✅ 49 | - **监控与告警**:集成 Prometheus 监控和 ELK 日志收集,及时发现和解决问题 50 | 51 | ## API 和集成模块 52 | 53 | - **RESTful API 设计**:提供 RESTful API,便于前后端分离开发和其他系统集成✅ 54 | - **第三方登录**:支持通过 OAuth2 等协议实现第三方账号登录,如:微信扫码登陆 55 | 56 | ## 未来规划 57 | 58 | - **移动应用支持**:开发移动端应用,提供更便捷的访问体验 59 | - **社交分享**:集成社交分享功能,允许用户将内容分享到其他社交平台 60 | - **高级搜索**:提供更强大的搜索引擎,支持复杂查询和全文搜索 61 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | nginx-gateway: 5 | image: nginx:1.21.5 6 | container_name: nginx-gateway 7 | restart: always 8 | privileged: true 9 | environment: 10 | - TZ=Asia/Shanghai 11 | ports: 12 | - 8888:8081 13 | volumes: 14 | - ./deploy/nginx/conf.d:/etc/nginx/conf.d 15 | - ./data/nginx/log:/var/log/nginx 16 | networks: 17 | - linkme_net 18 | depends_on: 19 | - linkme 20 | 21 | linkme: 22 | # 使用项目根目录下的 Dockerfile 自行构建镜像 23 | image: linkme/gomodd:v1.22.3 24 | container_name: linkme 25 | environment: 26 | TZ: Asia/Shanghai 27 | GOPROXY: https://goproxy.cn,direct 28 | working_dir: /go/linkme 29 | volumes: 30 | - .:/go/linkme 31 | privileged: true 32 | restart: always 33 | networks: 34 | - linkme_net 35 | 36 | networks: 37 | linkme_net: 38 | driver: bridge 39 | ipam: 40 | config: 41 | - subnet: 172.20.0.0/16 42 | 43 | -------------------------------------------------------------------------------- /internal/api/activity.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | "github.com/GoSimplicity/LinkMe/internal/constants" 6 | "github.com/GoSimplicity/LinkMe/internal/service" 7 | "github.com/GoSimplicity/LinkMe/middleware" 8 | . "github.com/GoSimplicity/LinkMe/pkg/ginp" 9 | "github.com/casbin/casbin/v2" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type ActivityHandler struct { 14 | ce *casbin.Enforcer 15 | svc service.ActivityService 16 | } 17 | 18 | func NewActivityHandler(svc service.ActivityService, ce *casbin.Enforcer) *ActivityHandler { 19 | return &ActivityHandler{ 20 | svc: svc, 21 | ce: ce, 22 | } 23 | } 24 | 25 | func (ah *ActivityHandler) RegisterRoutes(server *gin.Engine) { 26 | casbinMiddleware := middleware.NewCasbinMiddleware(ah.ce) 27 | historyGroup := server.Group("/api/activity") 28 | historyGroup.GET("/recent", casbinMiddleware.CheckCasbin(), WrapQuery(ah.GetRecentActivity)) // 获取最近的活动记录 29 | } 30 | 31 | // GetRecentActivity 获取最近的活动记录 32 | func (ah *ActivityHandler) GetRecentActivity(ctx *gin.Context, _ req.GetRecentActivityReq) (Result, error) { 33 | activity, err := ah.svc.GetRecentActivity(ctx) 34 | if err != nil { 35 | return Result{ 36 | Code: constants.GetRecentActivityERRORCode, 37 | Msg: constants.GetRecentActivityERROR, 38 | }, err 39 | } 40 | return Result{ 41 | Code: constants.RequestsOK, 42 | Msg: constants.GetCheckSuccess, 43 | Data: activity, 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/api/check.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/service" 7 | "github.com/GoSimplicity/LinkMe/pkg/apiresponse" 8 | ijwt "github.com/GoSimplicity/LinkMe/utils/jwt" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type CheckHandler struct { 13 | svc service.CheckService 14 | } 15 | 16 | func NewCheckHandler(svc service.CheckService) *CheckHandler { 17 | return &CheckHandler{ 18 | svc: svc, 19 | } 20 | } 21 | 22 | func (ch *CheckHandler) RegisterRoutes(server *gin.Engine) { 23 | checkGroup := server.Group("/api/checks") 24 | 25 | checkGroup.POST("/approve", ch.ApproveCheck) 26 | checkGroup.POST("/reject", ch.RejectCheck) 27 | checkGroup.GET("/list", ch.ListChecks) 28 | checkGroup.GET("/detail", ch.CheckDetail) 29 | } 30 | 31 | // ApproveCheck 审核通过 32 | func (ch *CheckHandler) ApproveCheck(ctx *gin.Context) { 33 | var req req.ApproveCheckReq 34 | if err := ctx.ShouldBindJSON(&req); err != nil { 35 | apiresponse.ErrorWithData(ctx, err) 36 | return 37 | } 38 | 39 | uc := ctx.MustGet("user").(ijwt.UserClaims) 40 | 41 | err := ch.svc.ApproveCheck(ctx, req.CheckID, req.Remark, uc.Uid) 42 | if err != nil { 43 | apiresponse.ErrorWithData(ctx, err) 44 | return 45 | } 46 | 47 | apiresponse.Success(ctx) 48 | } 49 | 50 | // RejectCheck 审核拒绝 51 | func (ch *CheckHandler) RejectCheck(ctx *gin.Context) { 52 | var req req.RejectCheckReq 53 | if err := ctx.ShouldBindJSON(&req); err != nil { 54 | apiresponse.ErrorWithData(ctx, err) 55 | return 56 | } 57 | 58 | uc := ctx.MustGet("user").(ijwt.UserClaims) 59 | 60 | err := ch.svc.RejectCheck(ctx, req.CheckID, req.Remark, uc.Uid) 61 | if err != nil { 62 | apiresponse.ErrorWithData(ctx, err) 63 | return 64 | } 65 | 66 | apiresponse.Success(ctx) 67 | } 68 | 69 | // ListChecks 获取审核列表 70 | func (ch *CheckHandler) ListChecks(ctx *gin.Context) { 71 | var req req.ListCheckReq 72 | if err := ctx.ShouldBindQuery(&req); err != nil { 73 | apiresponse.ErrorWithData(ctx, err) 74 | return 75 | } 76 | 77 | checks, err := ch.svc.ListChecks(ctx, domain.Pagination{ 78 | Page: req.Page, 79 | Size: req.Size, 80 | }) 81 | if err != nil { 82 | apiresponse.ErrorWithData(ctx, err) 83 | return 84 | } 85 | 86 | apiresponse.SuccessWithData(ctx, checks) 87 | } 88 | 89 | // CheckDetail 获取审核详情 90 | func (ch *CheckHandler) CheckDetail(ctx *gin.Context) { 91 | var req req.CheckDetailReq 92 | if err := ctx.ShouldBindQuery(&req); err != nil { 93 | apiresponse.ErrorWithData(ctx, err) 94 | return 95 | } 96 | 97 | check, err := ch.svc.CheckDetail(ctx, req.CheckID) 98 | if err != nil { 99 | apiresponse.ErrorWithData(ctx, err) 100 | return 101 | } 102 | 103 | apiresponse.SuccessWithData(ctx, check) 104 | } 105 | -------------------------------------------------------------------------------- /internal/api/history.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | . "github.com/GoSimplicity/LinkMe/internal/constants" 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/service" 8 | "github.com/GoSimplicity/LinkMe/pkg/apiresponse" 9 | ijwt "github.com/GoSimplicity/LinkMe/utils/jwt" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type HistoryHandler struct { 14 | svc service.HistoryService 15 | } 16 | 17 | func NewHistoryHandler(svc service.HistoryService) *HistoryHandler { 18 | return &HistoryHandler{ 19 | svc: svc, 20 | } 21 | } 22 | 23 | func (h *HistoryHandler) RegisterRoutes(server *gin.Engine) { 24 | historyGroup := server.Group("/api/history") 25 | 26 | historyGroup.POST("/list", h.GetHistory) 27 | historyGroup.DELETE("/delete", h.DeleteOneHistory) 28 | historyGroup.DELETE("/delete/all", h.DeleteAllHistory) 29 | } 30 | 31 | // GetHistory 获取历史记录 32 | func (h *HistoryHandler) GetHistory(ctx *gin.Context) { 33 | var req req.ListHistoryReq 34 | 35 | if err := ctx.ShouldBindJSON(&req); err != nil { 36 | apiresponse.ErrorWithData(ctx, err) 37 | return 38 | } 39 | 40 | uc := ctx.MustGet("user").(ijwt.UserClaims) 41 | history, err := h.svc.GetHistory(ctx, domain.Pagination{ 42 | Page: req.Page, 43 | Size: req.Size, 44 | Uid: uc.Uid, 45 | }) 46 | if err != nil { 47 | apiresponse.ErrorWithMessage(ctx, "获取历史记录失败") 48 | return 49 | } 50 | 51 | apiresponse.SuccessWithData(ctx, history) 52 | } 53 | 54 | // DeleteOneHistory 删除一条历史记录 55 | func (h *HistoryHandler) DeleteOneHistory(ctx *gin.Context) { 56 | var req req.DeleteHistoryReq 57 | 58 | if err := ctx.ShouldBindJSON(&req); err != nil { 59 | apiresponse.ErrorWithData(ctx, err) 60 | return 61 | } 62 | 63 | uc := ctx.MustGet("user").(ijwt.UserClaims) 64 | if err := h.svc.DeleteOneHistory(ctx, req.PostId, uc.Uid); err != nil { 65 | apiresponse.ErrorWithMessage(ctx, HistoryDeleteError) 66 | return 67 | } 68 | 69 | apiresponse.Success(ctx) 70 | } 71 | 72 | // DeleteAllHistory 删除所有历史记录 73 | func (h *HistoryHandler) DeleteAllHistory(ctx *gin.Context) { 74 | var req req.DeleteHistoryAllReq 75 | 76 | if err := ctx.ShouldBindJSON(&req); err != nil { 77 | apiresponse.ErrorWithData(ctx, err) 78 | return 79 | } 80 | 81 | uc := ctx.MustGet("user").(ijwt.UserClaims) 82 | if req.IsDeleteAll { 83 | if err := h.svc.DeleteAllHistory(ctx, uc.Uid); err != nil { 84 | apiresponse.ErrorWithMessage(ctx, HistoryDeleteError) 85 | return 86 | } 87 | } 88 | 89 | apiresponse.Success(ctx) 90 | } 91 | -------------------------------------------------------------------------------- /internal/api/permission.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | "github.com/GoSimplicity/LinkMe/internal/service" 6 | "github.com/GoSimplicity/LinkMe/pkg/apiresponse" 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type PermissionHandler struct { 12 | svc service.PermissionService 13 | l *zap.Logger 14 | } 15 | 16 | func NewPermissionHandler(svc service.PermissionService, l *zap.Logger) *PermissionHandler { 17 | return &PermissionHandler{ 18 | svc: svc, 19 | l: l, 20 | } 21 | } 22 | 23 | func (h *PermissionHandler) RegisterRoutes(server *gin.Engine) { 24 | permissionGroup := server.Group("/api/permissions") 25 | 26 | permissionGroup.POST("/user/assign", h.AssignUserRole) 27 | permissionGroup.POST("/users/assign", h.AssignUsersRole) 28 | } 29 | 30 | // AssignUserRole 为单个用户分配角色和权限 31 | func (h *PermissionHandler) AssignUserRole(c *gin.Context) { 32 | var r req.AssignUserRoleRequest 33 | // 绑定请求参数 34 | if err := c.ShouldBindJSON(&r); err != nil { 35 | h.l.Error("绑定请求参数失败", zap.Error(err)) 36 | apiresponse.Error(c) 37 | return 38 | } 39 | 40 | // 调用服务层分配角色和权限 41 | if err := h.svc.AssignRoleToUser(c.Request.Context(), r.UserId, r.RoleIds, r.MenuIds, r.ApiIds); err != nil { 42 | h.l.Error("分配角色失败", zap.Error(err)) 43 | apiresponse.Error(c) 44 | return 45 | } 46 | 47 | apiresponse.Success(c) 48 | } 49 | 50 | // AssignUsersRole 批量为用户分配角色和权限 51 | func (h *PermissionHandler) AssignUsersRole(c *gin.Context) { 52 | var r req.AssignUsersRoleRequest 53 | // 绑定请求参数 54 | if err := c.ShouldBindJSON(&r); err != nil { 55 | h.l.Error("绑定请求参数失败", zap.Error(err)) 56 | apiresponse.Error(c) 57 | return 58 | } 59 | 60 | // 调用服务层批量分配角色和权限 61 | if err := h.svc.AssignRoleToUsers(c.Request.Context(), r.UserIds, r.RoleIds, r.MenuIds, r.ApiIds); err != nil { 62 | h.l.Error("分配角色失败", zap.Error(err)) 63 | apiresponse.Error(c) 64 | return 65 | } 66 | 67 | apiresponse.Success(c) 68 | } 69 | -------------------------------------------------------------------------------- /internal/api/plate.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/service" 7 | "github.com/GoSimplicity/LinkMe/middleware" 8 | "github.com/GoSimplicity/LinkMe/pkg/apiresponse" 9 | ijwt "github.com/GoSimplicity/LinkMe/utils/jwt" 10 | "github.com/casbin/casbin/v2" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type PlateHandler struct { 15 | svc service.PlateService 16 | ce *casbin.Enforcer 17 | } 18 | 19 | func NewPlateHandler(svc service.PlateService, ce *casbin.Enforcer) *PlateHandler { 20 | return &PlateHandler{ 21 | svc: svc, 22 | ce: ce, 23 | } 24 | } 25 | 26 | func (h *PlateHandler) RegisterRoutes(server *gin.Engine) { 27 | casbinMiddleware := middleware.NewCasbinMiddleware(h.ce) 28 | permissionGroup := server.Group("/api/plate") 29 | permissionGroup.Use(casbinMiddleware.CheckCasbin()) 30 | permissionGroup.POST("/create", h.CreatePlate) 31 | permissionGroup.POST("/update", h.UpdatePlate) 32 | permissionGroup.DELETE("/delete/:plateId", h.DeletePlate) 33 | permissionGroup.POST("/list", h.ListPlate) 34 | } 35 | 36 | func (h *PlateHandler) CreatePlate(ctx *gin.Context) { 37 | var req req.CreatePlateReq 38 | if err := ctx.ShouldBindJSON(&req); err != nil { 39 | apiresponse.ErrorWithMessage(ctx, "无效的请求参数") 40 | return 41 | } 42 | 43 | uc := ctx.MustGet("user").(ijwt.UserClaims) 44 | if err := h.svc.CreatePlate(ctx, domain.Plate{ 45 | Name: req.Name, 46 | Description: req.Description, 47 | Uid: uc.Uid, 48 | }); err != nil { 49 | apiresponse.ErrorWithMessage(ctx, err.Error()) 50 | return 51 | } 52 | apiresponse.Success(ctx) 53 | } 54 | 55 | func (h *PlateHandler) UpdatePlate(ctx *gin.Context) { 56 | var req req.UpdatePlateReq 57 | if err := ctx.ShouldBindJSON(&req); err != nil { 58 | apiresponse.ErrorWithMessage(ctx, "无效的请求参数") 59 | return 60 | } 61 | 62 | uc := ctx.MustGet("user").(ijwt.UserClaims) 63 | if err := h.svc.UpdatePlate(ctx, domain.Plate{ 64 | ID: req.ID, 65 | Name: req.Name, 66 | Description: req.Description, 67 | Uid: uc.Uid, 68 | }); err != nil { 69 | apiresponse.ErrorWithMessage(ctx, err.Error()) 70 | return 71 | } 72 | apiresponse.Success(ctx) 73 | } 74 | 75 | func (h *PlateHandler) DeletePlate(ctx *gin.Context) { 76 | var req req.DeletePlateReq 77 | if err := ctx.ShouldBindUri(&req); err != nil { 78 | apiresponse.ErrorWithMessage(ctx, "无效的请求参数") 79 | return 80 | } 81 | 82 | uc := ctx.MustGet("user").(ijwt.UserClaims) 83 | if err := h.svc.DeletePlate(ctx, req.PlateID, uc.Uid); err != nil { 84 | apiresponse.ErrorWithMessage(ctx, err.Error()) 85 | return 86 | } 87 | apiresponse.Success(ctx) 88 | } 89 | 90 | func (h *PlateHandler) ListPlate(ctx *gin.Context) { 91 | var req req.ListPlateReq 92 | if err := ctx.ShouldBindJSON(&req); err != nil { 93 | apiresponse.ErrorWithMessage(ctx, "无效的请求参数") 94 | return 95 | } 96 | 97 | plates, err := h.svc.ListPlate(ctx, domain.Pagination{ 98 | Page: req.Page, 99 | Size: req.Size, 100 | }) 101 | if err != nil { 102 | apiresponse.ErrorWithMessage(ctx, err.Error()) 103 | return 104 | } 105 | apiresponse.SuccessWithData(ctx, plates) 106 | } 107 | -------------------------------------------------------------------------------- /internal/api/ranking.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | . "github.com/GoSimplicity/LinkMe/internal/constants" 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/service" 8 | "github.com/GoSimplicity/LinkMe/pkg/apiresponse" 9 | . "github.com/GoSimplicity/LinkMe/pkg/ginp" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type RankingHandler struct { 14 | svc service.RankingService 15 | } 16 | 17 | func NewRakingHandler(svc service.RankingService) *RankingHandler { 18 | return &RankingHandler{ 19 | svc: svc, 20 | } 21 | } 22 | 23 | func (rh *RankingHandler) RegisterRoutes(server *gin.Engine) { 24 | postGroup := server.Group("/api/raking") 25 | 26 | postGroup.GET("/topN", rh.GetRanking) 27 | postGroup.GET("/config", rh.GetRankingConfig) 28 | postGroup.POST("/reset", WrapBody(rh.ResetRanking)) 29 | } 30 | 31 | // GetRanking 获取排行榜 32 | func (rh *RankingHandler) GetRanking(ctx *gin.Context) { 33 | dp, err := rh.svc.GetTopN(ctx) 34 | if err != nil { 35 | apiresponse.ErrorWithData(ctx, err) 36 | return 37 | } 38 | apiresponse.SuccessWithData(ctx, dp) 39 | } 40 | 41 | func (rh *RankingHandler) GetRankingConfig(ctx *gin.Context) { 42 | dp, err := rh.svc.GetRankingConfig(ctx) 43 | if err != nil { 44 | apiresponse.ErrorWithData(ctx, err) 45 | return 46 | } 47 | apiresponse.SuccessWithData(ctx, dp) 48 | } 49 | 50 | func (rh *RankingHandler) ResetRanking(ctx *gin.Context, req req.RankingParameterReq) (Result, error) { 51 | rankingParameters := domain.RankingParameter{ 52 | Alpha: req.Alpha, 53 | Beta: req.Beta, 54 | Gamma: req.Gamma, 55 | Lambda: req.Lambda, 56 | } 57 | err := rh.svc.ResetRankingConfig(ctx, rankingParameters) 58 | if err != nil { 59 | return Result{ 60 | Code: SetRankingErrorCode, 61 | Msg: SetRankingErrorMsg, 62 | }, err 63 | } 64 | return Result{ 65 | Code: RequestsOK, 66 | Msg: SetRankingSuccessMsg, 67 | }, nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/api/req/activity_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type GetRecentActivityReq struct { 4 | } 5 | -------------------------------------------------------------------------------- /internal/api/req/check_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | // SubmitCheckReq 定义了提交审核请求的结构体 4 | type SubmitCheckReq struct { 5 | PostID int64 `json:"postId" binding:"required"` // 帖子ID 6 | Content string `json:"content" binding:"required"` // 审核内容 7 | Title string `json:"title" binding:"required"` // 审核标题 8 | UserID int64 `json:"userId" binding:"required"` // 提交审核的用户ID 9 | } 10 | 11 | // ApproveCheckReq 定义了审核通过请求的结构体 12 | type ApproveCheckReq struct { 13 | CheckID int64 `json:"checkId" binding:"required"` // 审核ID 14 | Remark string `json:"remark"` // 审核通过备注 15 | } 16 | 17 | // RejectCheckReq 定义了审核拒绝请求的结构体 18 | type RejectCheckReq struct { 19 | CheckID int64 `json:"checkId" binding:"required"` // 审核ID 20 | Remark string `json:"remark" binding:"required"` // 审核拒绝原因 21 | } 22 | 23 | // ListCheckReq 定义了获取审核列表请求的结构体 24 | type ListCheckReq struct { 25 | Page int `form:"page" binding:"required"` // 页码 26 | Size *int64 `form:"size" binding:"required"` // 每页数量 27 | } 28 | 29 | // CheckDetailReq 定义了获取审核详情请求的结构体 30 | type CheckDetailReq struct { 31 | CheckID int64 `json:"checkId" binding:"required"` // 审核ID 32 | } 33 | 34 | type GetCheckCount struct { 35 | } 36 | -------------------------------------------------------------------------------- /internal/api/req/comment_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type CreateCommentReq struct { 4 | PostId int64 `json:"postId" binding:"required"` 5 | Content string `json:"content" binding:"required"` 6 | RootId *int64 `json:"rootId,omitempty"` // 根评论ID,顶层评论时为空 7 | PID *int64 `json:"pid,omitempty"` // 父评论ID,顶层评论时为空 8 | } 9 | 10 | type ListCommentsReq struct { 11 | PostId int64 `json:"postId"` 12 | MinId int64 `json:"minId"` 13 | Limit int64 `json:"limit"` 14 | } 15 | 16 | type DeleteCommentReq struct { 17 | CommentId int64 `uri:"commentId"` 18 | } 19 | 20 | type GetMoreCommentReplyReq struct { 21 | RootId int64 `json:"rootId"` 22 | MaxId int64 `json:"maxId"` 23 | Limit int64 `json:"limit"` 24 | } 25 | type GetTopCommentReplyReq struct { 26 | PostId int64 `json:"postId"` 27 | } 28 | -------------------------------------------------------------------------------- /internal/api/req/history_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type ListHistoryReq struct { 4 | Page int `json:"page,omitempty"` // 当前页码 5 | Size *int64 `json:"size,omitempty"` // 每页数据量 6 | } 7 | 8 | type DeleteHistoryReq struct { 9 | PostId uint `json:"postId,omitempty"` 10 | } 11 | 12 | type DeleteHistoryAllReq struct { 13 | IsDeleteAll bool `json:"isDeleteAll,omitempty"` 14 | } 15 | -------------------------------------------------------------------------------- /internal/api/req/lotteryDraw_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | // ListLotteryDrawsReq 定义获取所有抽奖活动的请求参数 4 | type ListLotteryDrawsReq struct { 5 | Page int `json:"page,omitempty"` // 当前页码 6 | Size *int64 `json:"size,omitempty"` // 每页数据量 7 | Status string `json:"status"` // 抽奖活动状态过滤 8 | } 9 | 10 | // CreateLotteryDrawReq 定义创建新的抽奖活动的请求参数 11 | type CreateLotteryDrawReq struct { 12 | Name string `json:"name"` // 抽奖活动名称 13 | Description string `json:"description"` // 抽奖活动描述 14 | StartTime int64 `json:"startTime"` // 活动开始时间,必须晚于当前时间 15 | EndTime int64 `json:"endTime"` // 活动结束时间,必须晚于开始时间 16 | } 17 | 18 | // GetLotteryDrawReq 定义获取指定ID抽奖活动的请求参数 19 | type GetLotteryDrawReq struct { 20 | ID int `uri:"id"` // 抽奖活动的唯一标识符 21 | } 22 | 23 | // ParticipateReq 定义参与抽奖活动的请求参数 24 | type ParticipateReq struct { 25 | ActivityID int `json:"activityId"` // 抽奖活动的唯一标识符 26 | } 27 | 28 | // GetAllSecondKillEventsReq 定义获取所有秒杀活动的请求参数 29 | type GetAllSecondKillEventsReq struct { 30 | Page int `json:"page,omitempty"` // 当前页码 31 | Size *int64 `json:"size,omitempty"` // 每页数据量 32 | Status string `json:"status"` // 秒杀活动状态过滤 33 | } 34 | 35 | // CreateSecondKillEventReq 定义创建新的秒杀活动的请求参数 36 | type CreateSecondKillEventReq struct { 37 | Name string `json:"name"` // 秒杀活动名称 38 | Description string `json:"description"` // 秒杀活动描述 39 | StartTime int64 `json:"startTime"` // 活动开始时间,必须晚于当前时间 40 | EndTime int64 `json:"endTime"` // 活动结束时间,必须晚于开始时间 41 | } 42 | 43 | // GetSecondKillEventReq 定义获取指定ID秒杀活动的请求参数 44 | type GetSecondKillEventReq struct { 45 | ID int `uri:"id"` // 秒杀活动的唯一标识符 46 | } 47 | -------------------------------------------------------------------------------- /internal/api/req/plate_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type CreatePlateReq struct { 4 | Name string `json:"name"` 5 | Description string `json:"description"` 6 | } 7 | 8 | type DeletePlateReq struct { 9 | PlateID int64 `uri:"plateId"` 10 | } 11 | 12 | type UpdatePlateReq struct { 13 | ID int64 `json:"plateId"` 14 | Name string `json:"name"` 15 | Description string `json:"description"` 16 | } 17 | type ListPlateReq struct { 18 | Page int `json:"page,omitempty"` // 当前页码 19 | Size *int64 `json:"size,omitempty"` // 每页数据量 20 | } 21 | -------------------------------------------------------------------------------- /internal/api/req/post_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type EditReq struct { 4 | PostId uint `json:"postId,omitempty"` 5 | Title string `json:"title,omitempty"` 6 | Content string `json:"content,omitempty"` 7 | PlateID int64 `json:"plateId,omitempty"` 8 | } 9 | 10 | type PublishReq struct { 11 | PostId uint `json:"postId,omitempty"` 12 | } 13 | 14 | type WithDrawReq struct { 15 | PostId uint `json:"postId,omitempty"` 16 | } 17 | 18 | type ListReq struct { 19 | Page int `json:"page,omitempty"` // 当前页码 20 | Size *int64 `json:"size,omitempty"` // 每页数据量 21 | } 22 | 23 | type DetailPostReq struct { 24 | PostId uint `uri:"postId"` 25 | } 26 | 27 | type UpdateReq struct { 28 | PostId uint `json:"postId,omitempty"` 29 | Title string `json:"title,omitempty"` 30 | Content string `json:"content,omitempty"` 31 | PlateID int64 `json:"plateId,omitempty"` 32 | } 33 | type DetailReq struct { 34 | PostId uint `uri:"postId"` 35 | } 36 | 37 | type DeleteReq struct { 38 | PostId uint `uri:"postId"` 39 | } 40 | 41 | type LikeReq struct { 42 | PostId uint `json:"postId,omitempty"` 43 | Liked bool `json:"liked,omitempty"` 44 | } 45 | 46 | type CollectReq struct { 47 | PostId uint `json:"postId,omitempty"` 48 | Collectd bool `json:"collectd,omitempty"` 49 | } 50 | 51 | // type InteractReq struct { 52 | // BizId []int64 `json:"bizId,omitempty"` 53 | // BizName string `json:"bizName,omitempty"` 54 | // } 55 | 56 | // type GetPostCountReq struct { 57 | // } 58 | 59 | type SearchByPlateReq struct { 60 | PlateId int64 `json:"plateId,omitempty"` 61 | Page int `json:"page,omitempty"` 62 | Size *int64 `json:"size,omitempty"` 63 | } 64 | -------------------------------------------------------------------------------- /internal/api/req/ranking_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type RankingParameterReq struct { 4 | ID uint `json:"id"` 5 | Alpha float64 `json:"alpha"` 6 | Beta float64 `json:"beta" ` 7 | Gamma float64 `json:"gamma"` 8 | Lambda float64 `json:"lambda"` 9 | } 10 | -------------------------------------------------------------------------------- /internal/api/req/relation_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type ListFollowerRelationsReq struct { 4 | FollowerID int64 `json:"followerId"` // 关注者 5 | Page int `json:"page,omitempty"` // 当前页码 6 | Size *int64 `json:"size,omitempty"` // 每页数据量 7 | } 8 | 9 | type ListFolloweeRelationsReq struct { 10 | FolloweeID int64 `json:"followeeId"` // 被关注者 11 | Page int `json:"page,omitempty"` // 当前页码 12 | Size *int64 `json:"size,omitempty"` // 每页数据量 13 | } 14 | 15 | type GetRelationInfoReq struct { 16 | FollowerID int64 `json:"followerId"` // 关注者 17 | FolloweeID int64 `json:"followeeId"` // 被关注者 18 | } 19 | 20 | type FollowUserReq struct { 21 | FollowerID int64 `json:"followerId"` // 关注者 22 | FolloweeID int64 `json:"followeeId"` // 被关注者 23 | } 24 | 25 | type CancelFollowUserReq struct { 26 | FollowerID int64 `json:"followerId"` // 关注者 27 | FolloweeID int64 `json:"followeeId"` // 被关注者 28 | } 29 | 30 | type GetFolloweeCountReq struct { 31 | UserID int64 `json:"userId"` 32 | } 33 | 34 | type GetFollowerCountReq struct { 35 | UserID int64 `json:"userId"` 36 | } 37 | -------------------------------------------------------------------------------- /internal/api/req/search_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type SearchReq struct { 4 | Expression string `json:"expression"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/api/req/user_req.go: -------------------------------------------------------------------------------- 1 | package req 2 | 3 | type SignUpReq struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | ConfirmPassword string `json:"confirmPassword"` 7 | } 8 | 9 | type LoginReq struct { 10 | Username string `json:"username"` 11 | Password string `json:"password"` 12 | } 13 | 14 | type SMSReq struct { 15 | Number string `json:"number"` 16 | } 17 | 18 | type ChangeReq struct { 19 | Username string `json:"username"` 20 | Password string `json:"password"` 21 | NewPassword string `json:"newPassword"` 22 | ConfirmPassword string `json:"confirmPassword"` 23 | } 24 | 25 | type UsernameReq struct { 26 | Username string `json:"username"` 27 | } 28 | 29 | type DeleteUserReq struct { 30 | Username string `json:"username"` 31 | Password string `json:"password"` 32 | } 33 | type UpdateProfileReq struct { 34 | RealName string `json:"realName"` // 真实姓名 35 | Avatar string `json:"avatar"` // 头像URL 36 | About string `json:"about"` // 个人简介 37 | Birthday string `json:"birthday"` // 生日 38 | Phone string `json:"phone"` 39 | } 40 | 41 | type UpdateProfileAdminReq struct { 42 | UserID int64 `json:"userId"` // 用户ID 43 | RealName string `json:"realName"` // 真实姓名 44 | Avatar string `json:"avatar"` // 头像URL 45 | About string `json:"about"` // 个人简介 46 | Birthday string `json:"birthday"` // 生日 47 | Phone string `json:"phone"` // 手机号 48 | } 49 | 50 | type LoginSMSReq struct { 51 | Code string `json:"code"` 52 | } 53 | 54 | type ListUserReq struct { 55 | Page int `json:"page,omitempty"` // 当前页码 56 | Size *int64 `json:"size,omitempty"` // 每页数据量 57 | } 58 | 59 | type GetUserCountReq struct { 60 | } 61 | 62 | type LogoutReq struct { 63 | } 64 | 65 | type RefreshTokenReq struct { 66 | RefreshToken string `json:"refreshToken"` 67 | } 68 | 69 | type GetProfileReq struct { 70 | } 71 | -------------------------------------------------------------------------------- /internal/api/search.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api/req" 5 | . "github.com/GoSimplicity/LinkMe/internal/constants" 6 | "github.com/GoSimplicity/LinkMe/internal/service" 7 | . "github.com/GoSimplicity/LinkMe/pkg/ginp" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type SearchHandler struct { 12 | svc service.SearchService 13 | } 14 | 15 | func NewSearchHandler(svc service.SearchService) *SearchHandler { 16 | return &SearchHandler{ 17 | svc: svc, 18 | } 19 | } 20 | 21 | func (s *SearchHandler) RegisterRoutes(server *gin.Engine) { 22 | permissionGroup := server.Group("/api/search") 23 | permissionGroup.POST("/search_user", WrapBody(s.SearchUser)) 24 | permissionGroup.POST("/search_post", WrapBody(s.SearchPost)) 25 | permissionGroup.POST("/search_comment", WrapBody(s.SearchComment)) 26 | } 27 | 28 | func (s *SearchHandler) SearchUser(ctx *gin.Context, req req.SearchReq) (Result, error) { 29 | users, err := s.svc.SearchUsers(ctx, req.Expression) 30 | if err != nil { 31 | return Result{ 32 | Code: SearchUserERRORCode, 33 | Msg: SearchUserERROR, 34 | }, nil 35 | } 36 | return Result{ 37 | Code: RequestsOK, 38 | Msg: SearchUserSuccess, 39 | Data: users, 40 | }, nil 41 | } 42 | 43 | func (s *SearchHandler) SearchPost(ctx *gin.Context, req req.SearchReq) (Result, error) { 44 | posts, err := s.svc.SearchPosts(ctx, req.Expression) 45 | if err != nil { 46 | return Result{ 47 | Code: SearchPostERRORCode, 48 | Msg: SearchPostERROR, 49 | }, nil 50 | } 51 | return Result{ 52 | Code: RequestsOK, 53 | Msg: SearchPostSuccess, 54 | Data: posts, 55 | }, nil 56 | } 57 | func (s *SearchHandler) SearchComment(ctx *gin.Context, req req.SearchReq) (Result, error) { 58 | comments, err := s.svc.SearchComments(ctx, req.Expression) 59 | if err != nil { 60 | return Result{ 61 | Code: SearchCommentERRORCode, 62 | Msg: SearchCommentERROR, 63 | }, nil 64 | } 65 | return Result{ 66 | Code: RequestsOK, 67 | Msg: SearchCommentSuccess, 68 | Data: comments, 69 | }, nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/constants/activity.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | GetRecentActivityERRORCode = 405001 5 | GetRecentActivityERROR = "Get recent activity failed" 6 | GetRecentActivitySuccess = "Get recent activity success" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/check.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | GetCheckERRORCode = 403001 5 | GetCheckERROR = "Get check failed" 6 | GetCheckSuccess = "Get check success" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/comment.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | // 错误代码 5 | CreateCommentErrorCode = 406001 6 | DeleteCommentErrorCode = 406002 7 | ListCommentErrorCode = 406003 8 | GetMoreCommentReplyErrorCode = 406004 9 | GetTopCommentReplyErrorCode = 406005 10 | 11 | // 错误信息 12 | CreateCommentErrorMsg = "Failed to create comment" 13 | DeleteCommentErrorMsg = "Failed to delete comment" 14 | ListCommentErrorMsg = "Failed to list comments" 15 | GetMoreCommentReplyErrorMsg = "Failed to get more comment replies" 16 | GetTopCommentReplyErrorMsg = "Failed to get top comment replies" 17 | 18 | // 成功信息 19 | CreateCommentSuccessMsg = "Comment created successfully" 20 | DeleteCommentSuccessMsg = "Comment deleted successfully" 21 | ListCommentSuccessMsg = "Comments listed successfully" 22 | GetMoreCommentReplySuccessMsg = "More comment replies retrieved successfully" 23 | GetTopCommentReplySuccessMsg = "Top comment replies retrieved successfully" 24 | ) 25 | -------------------------------------------------------------------------------- /internal/constants/general.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | RequestsOK = 200 // 请求成功 5 | RequestsERROR = 401 // 通用请求错误 6 | ServerRequestError = 402 7 | ServerERROR = 500 // 服务器内部错误 8 | ) 9 | -------------------------------------------------------------------------------- /internal/constants/history.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | HistoryListSuccess = "History query success" 5 | HistoryListError = "History query failed" 6 | HistoryDeleteError = "History delete failed" 7 | HistoryDeleteSuccess = "History delete success" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/constants/lotteryDraw.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | ListLotteryDrawsError = "list lottery draw failed" 5 | ListLotteryDrawsSuccess = "list lottery draw successful" 6 | CreateLotteryDrawError = "create lottery draw failed" 7 | CreateLotteryDrawSuccess = "create lottery draw successful" 8 | GetLotteryDrawError = "get lottery draw failed" 9 | GetLotteryDrawSuccess = "get lottery draw successful" 10 | ParticipateLotteryDrawError = "participate lottery draw failed" 11 | ParticipateLotteryDrawSuccess = "participate lottery draw successful" 12 | 13 | ListSecondKillEventsError = "list second kill events failed" 14 | ListSecondKillEventsSuccess = "list second kill events successful" 15 | CreateSecondKillEventError = "create second kill event failed" 16 | CreateSecondKillEventSuccess = "create second kill event successful" 17 | GetSecondKillEventError = "get second kill event failed" 18 | GetSecondKillEventSuccess = "get second kill event successful" 19 | ParticipateSecondKillError = "participate second kill failed" 20 | ParticipateSecondKillSuccess = "participate second kill successful" 21 | ) 22 | -------------------------------------------------------------------------------- /internal/constants/plate.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | PlateCreateSuccess = "Plate create success" 5 | PlateCreateError = "Plate create error" 6 | PlateUpdateSuccess = "Plate update success" 7 | PlateUpdateError = "Plate update error" 8 | PlateDeleteSuccess = "Plate delete success" 9 | PlateDeleteError = "Plate delete error" 10 | PlateListSuccess = "Plate list success" 11 | PlateListError = "Plate list error" 12 | ) 13 | -------------------------------------------------------------------------------- /internal/constants/post.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | PostServerERRORCode = 502001 // 系统错误 5 | PostEditERRORCode = 402002 // 帖子编辑错误码 6 | PostUpdateERRORCode = 402003 // 帖子更新错误码 7 | PostPublishERRORCode = 402004 // 帖子发布错误码 8 | PostWithdrawERRORCode = 402005 // 帖子撤销错误码 9 | PostListPubERRORCode = 402006 // 公开帖子列表查询错误码 10 | PostListERRORCode = 402007 // 个人帖子列表查询错误码 11 | PostDeleteERRORCode = 402008 // 帖子删除错误码 12 | PostGetInteractiveERRORCode = 402009 // 帖子获取互动信息错误码 13 | PostLikedERRORCode = 402010 // 帖子点赞错误码 14 | PostCollectERRORCode = 402011 // 帖子收藏错误码 15 | PostGetDetailERRORCode = 402012 // 帖子获取个人详情错误码 16 | PostGetPubDetailERRORCode = 402013 // 帖子获取公开详情错误码 17 | PostGetLikedERRORCode = 402014 // 帖子获取点赞信息错误码 18 | PostGetCollectERRORCode = 402015 // 帖子获取收藏信息错误码 19 | PostGetCountERRORCode = 402016 20 | PostGetCountSuccess = "获取帖子数量成功" 21 | PostGetCountERROR = "获取帖子数量失败" 22 | PostEditSuccess = "帖子编辑成功" // 帖子编辑成功 23 | PostUpdateSuccess = "帖子更新成功" // 帖子更新成功 24 | PostPublishSuccess = "帖子发布成功" // 帖子发布成功 25 | PostWithdrawSuccess = "帖子撤销成功" // 帖子撤销成功 26 | PostListPubSuccess = "公开帖子查询成功" // 公开帖子查询成功 27 | PostListSuccess = "个人帖子查询成功" // 个人帖子查询成功 28 | PostDeleteSuccess = "帖子删除成功" // 帖子删除成功 29 | PostGetInteractiveSuccess = "互动信息获取成功" // 互动信息获取成功 30 | PostLikedSuccess = "帖子点赞成功" // 帖子点赞成功 31 | PostCollectSuccess = "帖子收藏成功" // 帖子收藏成功 32 | PostGetDetailSuccess = "获取帖子详情成功" // 获取帖子详情成功 33 | PostGetPubDetailSuccess = "获取公开帖子详情成功" // 获取公开帖子详情成功 34 | PostEditERROR = "帖子编辑失败" // 帖子编辑失败 35 | PostUpdateERROR = "帖子更新失败" // 帖子更新失败 36 | PostPublishERROR = "帖子发布失败" // 帖子发布失败 37 | PostWithdrawERROR = "帖子撤销失败" // 帖子撤销失败 38 | PostListPubERROR = "公开帖子查询失败" // 公开帖子查询失败 39 | PostListERROR = "个人帖子查询失败" // 个人帖子查询失败 40 | PostDeleteERROR = "帖子删除失败" // 帖子删除失败 41 | PostGetInteractiveERROR = "互动信息获取失败" // 互动信息获取失败 42 | PostLikedERROR = "帖子点赞失败" // 帖子点赞失败 43 | PostCollectERROR = "帖子收藏失败" // 帖子收藏失败 44 | PostServerERROR = "系统错误" // 系统错误 45 | PostGetDetailERROR = "获取帖子详情失败" // 获取帖子详情失败 46 | PostGetPubDetailERROR = "获取公开帖子详情失败" // 获取公开帖子详情失败 47 | PostGetLikedERROR = "获取帖子点赞信息失败" // 获取帖子点赞信息失败 48 | PostGetCollectERROR = "获取帖子收藏信息失败" // 获取帖子收藏信息失败 49 | PostGetPostERROR = "获取帖子失败" // 获取帖子失败 50 | ) 51 | -------------------------------------------------------------------------------- /internal/constants/ranking.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | SetRankingErrorCode = 10001 5 | SetRankingErrorMsg = "重置榜单配置失败" 6 | SetRankingSuccessMsg = "重置榜单配置成功" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/constants/relation.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | CreateRelationERRORCode = 407001 5 | GetRelationERRORCode = 407002 6 | FollowUserERRORCode = 407003 7 | CancelFollowUserERRORCode = 407004 8 | GetFolloweeCountErrorCode = 407005 9 | GetFollowerCountErrorCode = 407006 10 | GetFolloweeCountSuccessMsg = "Followee count retrieved successfully" 11 | GetFollowerCountSuccessMsg = "Follower count retrieved successfully" 12 | GetFolloweeCountERRORMsg = "Failed to get followee count" 13 | GetFollowerCountERRORMsg = "Failed to get follower count" 14 | CreateRelationERRORMsg = "Failed to create relation" 15 | GetRelationERRORMsg = "Failed to get relation" 16 | FollowUserERRORMsg = "Failed to follow user" 17 | CancelFollowUserERRORMsg = "Failed to cancel follow user" 18 | CreateRelationSuccessMsg = "Relation created successfully" 19 | GetRelationSuccessMsg = "Relation retrieved successfully" 20 | FollowUserSuccessMsg = "User followed successfully" 21 | CancelFollowUserSuccessMsg = "User unfollowed successfully" 22 | ) 23 | -------------------------------------------------------------------------------- /internal/constants/search.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | SearchUserERRORCode = 407001 5 | SearchPostERRORCode = 407002 6 | SearchCommentERRORCode = 407003 7 | SearchUserERROR = "Search user failed" 8 | SearchPostERROR = "Search post failed" 9 | SearchCommentERROR = "Search comment failed" 10 | SearchUserSuccess = "Search user success" 11 | SearchPostSuccess = "Search post success" 12 | SearchCommentSuccess = "Search cpmment success" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/constants/sms.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | SMSNumberErr = 404001 5 | InvalidNumber = "电话号码无效" 6 | 7 | SMSInternalCode = 504001 8 | SMSInternalError = "Server error" 9 | ) 10 | -------------------------------------------------------------------------------- /internal/constants/user.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | UserInvalidInputCode = 401001 // 用户输入错误 5 | UserInvalidOrPasswordCode = 401002 // 用户名或密码错误 6 | UserInvalidOrProfileErrorCode = 401003 // 用户资料无效 7 | UserEmailFormatErrorCode = 401004 // 邮箱格式错误 8 | UserPasswordMismatchErrorCode = 401005 // 两次输入的密码不一致 9 | UserPasswordFormatErrorCode = 401006 // 密码格式错误 10 | UserEmailConflictErrorCode = 401007 // 邮箱冲突 11 | UserListErrorCode = 401008 // 用户获取失败 12 | UserServerErrorCode = 500001 // 用户服务内部错误 13 | UserGetCountErrorCode = 401009 // 获取用户数量失败 14 | UserSignUpSuccess = "用户注册成功" 15 | UserGetCountError = "获取用户数量失败" 16 | UserGetCountSuccess = "获取用户数量成功" 17 | UserListError = "用户获取失败" 18 | UserListSuccess = "用户获取成功" 19 | UserSignUpFailure = "用户注册失败" 20 | UserLoginSuccess = "用户登录成功" 21 | UserLoginFailure = "用户登录失败" 22 | UserLogoutSuccess = "用户登出成功" 23 | UserLogoutFailure = "用户登出失败" 24 | UserRefreshTokenSuccess = "令牌刷新成功" 25 | UserRefreshTokenFailure = "令牌刷新失败" 26 | UserProfileGetSuccess = "获取用户资料成功" 27 | UserProfileGetFailure = "获取用户资料失败" 28 | UserProfileUpdateSuccess = "更新用户资料成功" 29 | UserProfileUpdateFailure = "更新用户资料失败" 30 | UserPasswordChangeSuccess = "密码修改成功" 31 | UserPasswordChangeFailure = "密码修改失败" 32 | UserDeletedSuccess = "用户删除成功" 33 | UserDeletedFailure = "用户删除失败" 34 | UserSendSMSCodeSuccess = "短信验证码发送成功" 35 | UserSendEmailCodeSuccess = "邮箱验证码发送成功" 36 | UserEmailFormatError = "邮箱格式错误,请检查" 37 | UserPasswordMismatchError = "两次输入的密码不一致,请重新输入" 38 | UserPasswordFormatError = "密码必须包含字母、数字和特殊字符,且长度不少于8位" 39 | UserEmailConflictError = "该邮箱已被注册" 40 | ) 41 | -------------------------------------------------------------------------------- /internal/domain/activity.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type RecentActivity struct { 4 | ID int64 5 | UserID int64 6 | Description string 7 | Time string 8 | } 9 | -------------------------------------------------------------------------------- /internal/domain/all.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Pagination struct { 4 | Page int // 当前页码 5 | Size *int64 // 每页数据 6 | Uid int64 7 | // 以下字段通常在服务端内部使用,不需要客户端传递 8 | Offset *int64 // 数据偏移量 9 | Total *int64 // 总数据量 10 | } 11 | -------------------------------------------------------------------------------- /internal/domain/check.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | const ( 4 | UnderReview uint8 = iota 5 | Approved 6 | UnApproved 7 | ) 8 | 9 | type Check struct { 10 | ID int64 // 审核ID 11 | PostID uint // 帖子ID 12 | Content string // 审核内容 13 | Title string // 审核标签 14 | Uid int64 // 提交审核的用户ID 15 | PlateID int64 // 板块id 16 | Status uint8 // 审核状态 17 | Remark string // 审核备注 18 | CreatedAt int64 // 创建时间 19 | UpdatedAt int64 // 更新时间 20 | BizId int64 // 审核类型(帖子或者评论) 21 | } 22 | -------------------------------------------------------------------------------- /internal/domain/comment.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Comment struct { 4 | Id int64 5 | UserId int64 6 | Biz string 7 | BizId int64 8 | PostId int64 9 | Content string 10 | RootComment *Comment // 根节点 11 | ParentComment *Comment // 父节点 12 | Children []Comment // 子节点 13 | CreatedAt int64 14 | UpdatedAt int64 15 | Status uint8 // 评论的审核状态 16 | } 17 | -------------------------------------------------------------------------------- /internal/domain/events/canal/kafka_connector搭建: -------------------------------------------------------------------------------- 1 | #拉取镜像 2 | docker pull quay.io/debezium/connect 3 | 4 | #创建容器 5 | docker run -it --rm --name linkme-connect -p 8083:8083 \ 6 | -e GROUP_ID=1 \ 7 | -e CONFIG_STORAGE_TOPIC=my_connect_configs \ 8 | -e OFFSET_STORAGE_TOPIC=my_connect_offsets \ 9 | -e STATUS_STORAGE_TOPIC=my_connect_statuses \ 10 | -e BOOTSTRAP_SERVERS=192.168.84.130:9092 \ 11 | --link linkme-kafka:linkme-kafka --link linkme-mysql:linkme-mysql \ 12 | --network linkme_default \ 13 | quay.io/debezium/connect 14 | 15 | #创建connector 16 | 17 | curl -i -X POST -H "Accept:application/json" -H "Content-Type:application/json" localhost:8083/connectors/ -d \ 18 | '{ 19 | "name": "linkme-connector", 20 | "config": { 21 | "connector.class": "io.debezium.connector.mysql.MySqlConnector", 22 | "tasks.max": "1", 23 | "database.hostname": "linkme-mysql", 24 | "database.port": "3306", 25 | "database.user": "root", 26 | "database.password": "root", 27 | "database.server.id": "184054", 28 | "database.server.name": "linkme", 29 | "database.include.list": "linkme", 30 | "schema.history.internal.kafka.bootstrap.servers": "192.168.84.130:9092", 31 | "schema.history.internal.kafka.topic": "schema-changes.linkme", 32 | "topic.prefix":"oracle" 33 | } 34 | }' 35 | -------------------------------------------------------------------------------- /internal/domain/events/check/producer.go: -------------------------------------------------------------------------------- 1 | package check 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | const TopicCheckEvent = "check_events" 10 | 11 | type Producer interface { 12 | ProduceCheckEvent(evt CheckEvent) error 13 | } 14 | 15 | type CheckEvent struct { 16 | BizId int64 17 | PostId uint 18 | Uid int64 19 | Title string 20 | Content string 21 | PlateID int64 22 | } 23 | 24 | type SaramaCheckProducer struct { 25 | producer sarama.SyncProducer 26 | } 27 | 28 | func NewSaramaCheckProducer(producer sarama.SyncProducer) Producer { 29 | return &SaramaCheckProducer{ 30 | producer: producer, 31 | } 32 | } 33 | 34 | func (s *SaramaCheckProducer) ProduceCheckEvent(evt CheckEvent) error { 35 | val, err := json.Marshal(evt) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | _, _, err = s.producer.SendMessage(&sarama.ProducerMessage{ 41 | Topic: TopicCheckEvent, 42 | Value: sarama.StringEncoder(val), 43 | }) 44 | 45 | return err 46 | } 47 | -------------------------------------------------------------------------------- /internal/domain/events/comment/producer.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | const TopicCommentEvent = "comment_events" 10 | 11 | type Producer interface { 12 | ProduceCommentEvent(evt CommentEvent) error 13 | } 14 | 15 | type CommentEvent struct { 16 | BizId int64 17 | PostId uint 18 | Uid int64 19 | Title string 20 | Content string 21 | PlateID int64 22 | Status uint8 23 | } 24 | 25 | type SaramaCommentProducer struct { 26 | producer sarama.SyncProducer 27 | } 28 | 29 | func NewSaramaCommentProducer(producer sarama.SyncProducer) Producer { 30 | return &SaramaCommentProducer{ 31 | producer: producer, 32 | } 33 | } 34 | 35 | func (s *SaramaCommentProducer) ProduceCommentEvent(evt CommentEvent) error { 36 | val, err := json.Marshal(evt) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | _, _, err = s.producer.SendMessage(&sarama.ProducerMessage{ 42 | Topic: TopicCommentEvent, 43 | Value: sarama.StringEncoder(val), 44 | }) 45 | 46 | return err 47 | } 48 | -------------------------------------------------------------------------------- /internal/domain/events/email/consumer.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "time" 8 | 9 | "github.com/GoSimplicity/LinkMe/pkg/samarap" 10 | "github.com/IBM/sarama" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type EmailConsumer struct { 15 | repo repository.EmailRepository 16 | client sarama.Client 17 | l *zap.Logger 18 | } 19 | 20 | func NewEmailConsumer(repo repository.EmailRepository, client sarama.Client, l *zap.Logger) *EmailConsumer { 21 | return &EmailConsumer{repo: repo, client: client, l: l} 22 | } 23 | 24 | func (e *EmailConsumer) Start(ctx context.Context) error { 25 | cg, err := sarama.NewConsumerGroupFromClient("email_consumer_group", e.client) 26 | if err != nil { 27 | return err 28 | } 29 | go func() { 30 | attempts := 0 31 | maxRetries := 3 32 | e.l.Info("emailConsumer 开始消费") 33 | for attempts < maxRetries { 34 | er := cg.Consume(ctx, []string{TopicEmail}, samarap.NewHandler(e.l, e.HandleMessage)) 35 | if er != nil { 36 | e.l.Error("消费错误", zap.Error(er), zap.Int("重试次数", attempts+1)) 37 | attempts++ 38 | time.Sleep(time.Second * time.Duration(attempts)) 39 | continue 40 | } 41 | break 42 | } 43 | if attempts >= maxRetries { 44 | e.l.Error("达到最大重试次数,退出消费") 45 | } 46 | }() 47 | return nil 48 | } 49 | 50 | func (e *EmailConsumer) HandleMessage(msg *sarama.ConsumerMessage, emailEvent EmailEvent) error { 51 | err := json.Unmarshal(msg.Value, &emailEvent) 52 | if err != nil { 53 | e.l.Error("json.Unmarshal 失败", zap.Any("msg", msg)) 54 | return err 55 | } 56 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 57 | defer cancel() 58 | return e.repo.SendCode(ctx, emailEvent.Email) 59 | } 60 | -------------------------------------------------------------------------------- /internal/domain/events/email/producer.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/IBM/sarama" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | const TopicEmail = "email_events" 11 | 12 | type Producer interface { 13 | ProduceEmail(ctx context.Context, evt EmailEvent) error 14 | } 15 | 16 | // EmailEvent 代表单个短信验证码事件 17 | type EmailEvent struct { 18 | Email string 19 | } 20 | 21 | // SaramaSyncProducer 实现Producer接口的结构体 22 | type SaramaSyncProducer struct { 23 | producer sarama.SyncProducer 24 | logger *zap.Logger 25 | } 26 | 27 | // NewSaramaSyncProducer 创建一个新的SaramaSyncProducer实例 28 | func NewSaramaSyncProducer(producer sarama.SyncProducer, logger *zap.Logger) Producer { 29 | return &SaramaSyncProducer{ 30 | producer: producer, 31 | logger: logger, 32 | } 33 | } 34 | 35 | // ProduceEmail 发送邮箱验证码事件到Kafka 36 | func (s *SaramaSyncProducer) ProduceEmail(ctx context.Context, evt EmailEvent) error { 37 | // 序列化事件 38 | data, err := json.Marshal(evt) 39 | if err != nil { 40 | s.logger.Error("序列化事件失败", zap.Error(err)) 41 | return err 42 | } 43 | // 发送消息到Kafka 44 | partition, offset, err := s.producer.SendMessage(&sarama.ProducerMessage{ 45 | Topic: TopicEmail, 46 | Value: sarama.StringEncoder(data), 47 | }) 48 | if err != nil { 49 | s.logger.Error("发送信息到Kafka失败", zap.Error(err)) 50 | return err 51 | } 52 | s.logger.Info("成功发送消息到Kafka", zap.String("topic", TopicEmail), zap.Int32("partition", partition), zap.Int64("offset", offset)) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /internal/domain/events/es/consumer_test.go: -------------------------------------------------------------------------------- 1 | package es 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 8 | "github.com/IBM/sarama" 9 | "github.com/elastic/go-elasticsearch/v8" 10 | "go.uber.org/zap" 11 | "os" 12 | "os/signal" 13 | "syscall" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func initKafka() sarama.Client { 19 | scfg := sarama.NewConfig() 20 | scfg.Consumer.Offsets.Initial = sarama.OffsetOldest //从头消费 21 | client, _ := sarama.NewClient([]string{"192.168.84.130:9092"}, scfg) 22 | return client 23 | } 24 | 25 | func initLogger() *zap.Logger { 26 | cfg := zap.NewDevelopmentConfig() 27 | logger, _ := cfg.Build() 28 | return logger 29 | } 30 | 31 | func initEsClient() *elasticsearch.TypedClient { 32 | client, err := elasticsearch.NewTypedClient(elasticsearch.Config{ 33 | Addresses: []string{"http://localhost:9200"}, 34 | Username: "elastic", 35 | Password: "gNWFCTYAobqKRl09LiPj", 36 | }) 37 | if err != nil { 38 | panic(err) 39 | } 40 | return client 41 | } 42 | 43 | // 通过修改或添加数据库中的数据,判断是否数据同步到es中 44 | func TestEsConsumer(t *testing.T) { 45 | logger := initLogger() 46 | es := initEsClient() 47 | searchDao := dao.NewSearchDAO(es, logger) 48 | 49 | esConsumer := NewEsConsumer(initKafka(), logger, repository.NewSearchRepository(searchDao)) 50 | go func() { 51 | err := esConsumer.Start(context.Background()) 52 | if err != nil { 53 | panic(err) 54 | } 55 | }() 56 | 57 | time.Sleep(2 * time.Second) 58 | ctx, cancel := context.WithCancel(context.Background()) 59 | defer cancel() 60 | 61 | users, err := searchDao.SearchUsers(ctx, []string{"bob"}) 62 | if err != nil { 63 | panic(err) 64 | } 65 | for _, user := range users { 66 | fmt.Println("已成功找到用户:") 67 | fmt.Println(user) 68 | } 69 | 70 | posts, err := searchDao.SearchPosts(ctx, []string{"HisLife"}) 71 | if err != nil { 72 | panic(err) 73 | } 74 | fmt.Println("已成功找到文章:") 75 | fmt.Println(posts) 76 | 77 | // 优雅退出 78 | sigchan := make(chan os.Signal, 1) 79 | signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM) 80 | select { 81 | case <-sigchan: 82 | fmt.Printf("consumer test terminated") 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /internal/domain/events/post/producer.go: -------------------------------------------------------------------------------- 1 | package post 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | const ( 10 | TopicReadEvent = "read_events" 11 | ) 12 | 13 | type Producer interface { 14 | ProduceReadEvent(evt ReadEvent) error 15 | } 16 | 17 | type ReadEvent struct { 18 | PostId uint 19 | Uid int64 20 | Title string 21 | Content string 22 | PlateID int64 23 | } 24 | 25 | type SaramaSyncProducer struct { 26 | producer sarama.SyncProducer 27 | } 28 | 29 | func NewSaramaSyncProducer(producer sarama.SyncProducer) Producer { 30 | return &SaramaSyncProducer{ 31 | producer: producer, 32 | } 33 | } 34 | 35 | func (s *SaramaSyncProducer) ProduceReadEvent(evt ReadEvent) error { 36 | val, err := json.Marshal(evt) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | // 创建消息 42 | _, _, err = s.producer.SendMessage(&sarama.ProducerMessage{ 43 | Topic: TopicReadEvent, // 订阅主题 44 | Value: sarama.StringEncoder(val), // 消息内容 45 | }) 46 | 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /internal/domain/events/publish/producer.go: -------------------------------------------------------------------------------- 1 | package publish 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/IBM/sarama" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | const TopicPublishEvent = "publish_events" 11 | 12 | type Producer interface { 13 | ProducePublishEvent(evt PublishEvent) error 14 | } 15 | 16 | type PublishEvent struct { 17 | PostId uint `json:"post_id"` 18 | Uid int64 `json:"uid"` 19 | Status uint8 `json:"status"` 20 | BizId int64 `json:"biz_id"` 21 | } 22 | 23 | type SaramaSyncProducer struct { 24 | producer sarama.SyncProducer 25 | l *zap.Logger 26 | } 27 | 28 | func NewSaramaSyncProducer(producer sarama.SyncProducer, l *zap.Logger) Producer { 29 | return &SaramaSyncProducer{ 30 | producer: producer, 31 | l: l, 32 | } 33 | } 34 | 35 | func (s *SaramaSyncProducer) ProducePublishEvent(evt PublishEvent) error { 36 | val, err := json.Marshal(evt) 37 | if err != nil { 38 | s.l.Error("Failed to marshal publish event", zap.Error(err)) 39 | return err 40 | } 41 | 42 | msg := &sarama.ProducerMessage{ 43 | Topic: TopicPublishEvent, 44 | Value: sarama.StringEncoder(val), 45 | } 46 | 47 | partition, offset, err := s.producer.SendMessage(msg) 48 | if err != nil { 49 | s.l.Error("Failed to send publish event message", zap.Error(err)) 50 | return err 51 | } 52 | 53 | s.l.Info("Publish event message sent", zap.Int32("partition", partition), zap.Int64("offset", offset)) 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/domain/events/sms/consumer.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "time" 8 | 9 | //"LinkMe/internal/repository" 10 | "github.com/GoSimplicity/LinkMe/internal/repository/cache" 11 | "github.com/GoSimplicity/LinkMe/pkg/samarap" 12 | "github.com/IBM/sarama" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | type SMSConsumer struct { 17 | repo repository.SmsRepository 18 | client sarama.Client 19 | l *zap.Logger 20 | rdb cache.SMSCache 21 | } 22 | 23 | func NewSMSConsumer(repo repository.SmsRepository, client sarama.Client, l *zap.Logger, rdb cache.SMSCache) *SMSConsumer { 24 | return &SMSConsumer{repo: repo, client: client, l: l, rdb: rdb} 25 | } 26 | 27 | func (s *SMSConsumer) Start(ctx context.Context) error { 28 | cg, err := sarama.NewConsumerGroupFromClient("sms_consumer_group", s.client) 29 | 30 | s.l.Info("SMSConsumer 开始消费") 31 | 32 | if err != nil { 33 | return err 34 | } 35 | 36 | go func() { 37 | attempts := 0 38 | maxRetries := 3 39 | for attempts < maxRetries { 40 | er := cg.Consume(ctx, []string{TopicSMS}, samarap.NewHandler(s.l, s.HandleMessage)) 41 | if er != nil { 42 | s.l.Error("消费错误", zap.Error(er), zap.Int("重试次数", attempts+1)) 43 | attempts++ 44 | time.Sleep(time.Second * time.Duration(attempts)) 45 | continue 46 | } 47 | break 48 | } 49 | if attempts >= maxRetries { 50 | s.l.Error("达到最大重试次数,退出消费") 51 | } 52 | }() 53 | 54 | return nil 55 | } 56 | 57 | func (s *SMSConsumer) HandleMessage(msg *sarama.ConsumerMessage, smsEvent SMSCodeEvent) error { 58 | err := json.Unmarshal(msg.Value, &smsEvent) 59 | if err != nil { 60 | s.l.Error("json.Unmarshal 失败", zap.Any("msg", msg)) 61 | return err 62 | } 63 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 64 | defer cancel() 65 | return s.repo.SendCode(ctx, smsEvent.Number) 66 | } 67 | -------------------------------------------------------------------------------- /internal/domain/events/sms/producer.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/IBM/sarama" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | const TopicSMS = "linkme_sms_events" 12 | 13 | type Producer interface { 14 | ProduceSMSCode(ctx context.Context, evt SMSCodeEvent) error 15 | } 16 | 17 | // SMSCodeEvent 代表单个短信验证码事件 18 | type SMSCodeEvent struct { 19 | Number string 20 | } 21 | 22 | // SaramaSyncProducer 实现Producer接口的结构体 23 | type SaramaSyncProducer struct { 24 | producer sarama.SyncProducer 25 | logger *zap.Logger 26 | } 27 | 28 | func NewSaramaSyncProducer(producer sarama.SyncProducer, logger *zap.Logger) Producer { 29 | return &SaramaSyncProducer{ 30 | producer: producer, 31 | logger: logger, 32 | } 33 | } 34 | 35 | // ProduceSMSCode 发送短信验证码事件到Kafka 36 | func (s *SaramaSyncProducer) ProduceSMSCode(ctx context.Context, evt SMSCodeEvent) error { 37 | // 序列化事件 38 | data, err := json.Marshal(evt) 39 | if err != nil { 40 | s.logger.Error("序列化事件失败", zap.Error(err)) 41 | return err 42 | } 43 | // 发送消息到Kafka 44 | partition, offset, err := s.producer.SendMessage(&sarama.ProducerMessage{ 45 | Topic: TopicSMS, 46 | Value: sarama.StringEncoder(data), 47 | }) 48 | if err != nil { 49 | s.logger.Error("发送信息到Kafka失败", zap.Error(err)) 50 | return err 51 | } 52 | 53 | s.logger.Info("成功发送消息到Kafka", zap.String("topic", TopicSMS), zap.Int32("partition", partition), zap.Int64("offset", offset)) 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/domain/events/types.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | import "context" 4 | 5 | type Consumer interface { 6 | Start(ctx context.Context) error 7 | } 8 | -------------------------------------------------------------------------------- /internal/domain/history.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type History struct { 4 | PostID uint 5 | Title string 6 | Content string 7 | Uid int64 8 | Tags string 9 | } 10 | -------------------------------------------------------------------------------- /internal/domain/log.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type ReadEvent struct { 4 | Timestamp int64 `json:"timestamp"` 5 | Level string `json:"level"` 6 | Message string `json:"message"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/domain/lotteryDraw.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "errors" 4 | 5 | var ErrNotFound = errors.New("record not found") 6 | 7 | const ( 8 | LotteryStatusPending string = "pending" // 待开始 9 | LotteryStatusActive string = "active" // 进行中 10 | LotteryStatusCompleted string = "completed" // 已完成 11 | ) 12 | 13 | const ( 14 | SecondKillStatusPending string = "pending" // 待开始 15 | SecondKillStatusActive string = "active" // 进行中 16 | SecondKillStatusCompleted string = "completed" // 已完成 17 | ) 18 | 19 | // Participant 表示参与者的记录,适用于抽奖和秒杀活动 20 | type Participant struct { 21 | ID string // 参与记录的唯一标识符 22 | LotteryID *int // 关联的活动ID(可以是抽奖或秒杀活动) 23 | SecondKillID *int 24 | ActivityType string 25 | UserID int64 // 参与者的用户ID 26 | ParticipatedAt int64 // UNIX 时间戳,表示参与时间 27 | } 28 | 29 | // LotteryDraw 表示一个抽奖活动 30 | type LotteryDraw struct { 31 | ID int // 抽奖活动的唯一标识符 32 | Name string // 抽奖活动名称 33 | Description string // 抽奖活动描述 34 | StartTime int64 // UNIX 时间戳,表示活动开始时间 35 | EndTime int64 // UNIX 时间戳,表示活动结束时间 36 | Status string // 抽奖活动状态 37 | Participants []Participant // 参与者列表 38 | } 39 | 40 | // SecondKillEvent 表示一个秒杀活动 41 | type SecondKillEvent struct { 42 | ID int // 秒杀活动的唯一标识符 43 | Name string // 秒杀活动名称 44 | Description string // 秒杀活动描述 45 | StartTime int64 // UNIX 时间戳,表示活动开始时间 46 | EndTime int64 // UNIX 时间戳,表示活动结束时间 47 | Status string // 秒杀活动状态 48 | Participants []Participant // 参与者列表 49 | } 50 | -------------------------------------------------------------------------------- /internal/domain/mysql_job.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/robfig/cron/v3" 5 | "time" 6 | ) 7 | 8 | type Job struct { 9 | Id int64 // 任务的唯一标识符 10 | Name string // 任务名称 11 | Expression string // Cron 表达式,用于定义任务的调度时间 12 | Executor string // 执行任务的执行器名称 13 | Cfg string // 任务配置,可以是任意字符串 14 | CancelFunc func() // 用于取消任务的函数 15 | } 16 | 17 | // NextTime 计算任务的下次执行时间 18 | func (j *Job) NextTime() (time.Time, error) { 19 | // 创建新的 Cron 表达式解析器 20 | c := cron.NewParser(cron.Second | cron.Minute | cron.Hour | 21 | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) 22 | // 解析 Cron 表达式 23 | schedule, err := c.Parse(j.Expression) 24 | if err != nil { 25 | return time.Time{}, err // 返回解析错误 26 | } 27 | // 计算并返回下次执行时间 28 | return schedule.Next(time.Now()), nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/domain/plate.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Plate struct { 4 | ID int64 `json:"id"` 5 | Name string `json:"name"` 6 | Description string `json:"description"` 7 | Uid int64 `json:"uid"` 8 | CreatedAt int64 `json:"created_at"` 9 | UpdatedAt int64 `json:"updated_at"` 10 | DeletedAt int64 `json:"deleted_at"` 11 | Deleted bool `json:"deleted"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/domain/post.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "database/sql" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | const ( 10 | Draft uint8 = iota // 0: 草稿状态 11 | Published // 1: 发布状态 12 | Withdrawn // 2: 撤回状态 13 | Deleted // 3: 删除状态 14 | 15 | ) 16 | 17 | type Post struct { 18 | ID uint `json:"id"` 19 | Title string `json:"title"` 20 | Content string `json:"content"` 21 | CreatedAt time.Time `json:"created_at"` 22 | UpdatedAt time.Time `json:"updated_at"` 23 | DeletedAt sql.NullTime `json:"deleted_at"` 24 | ReadCount int64 `json:"read_count"` 25 | LikeCount int64 `json:"like_count"` 26 | CollectCount int64 `json:"collect_count"` 27 | Uid int64 `json:"uid"` 28 | Status uint8 `json:"status"` 29 | PlateID int64 `json:"plate_id"` 30 | Slug string `json:"slug"` 31 | CategoryID int64 `json:"category_id"` 32 | Tags string `json:"tags"` 33 | CommentCount int64 `json:"comment_count"` 34 | IsSubmit bool `json:"is_submit"` 35 | Total int64 `json:"total"` 36 | } 37 | 38 | type Interactive struct { 39 | BizID uint `json:"biz_id"` 40 | ReadCount int64 `json:"read_count"` 41 | LikeCount int64 `json:"like_count"` 42 | CollectCount int64 `json:"collect_count"` 43 | Liked bool `json:"liked"` 44 | Collected bool `json:"collected"` 45 | } 46 | 47 | func (i *Interactive) IncrementReadCount() { 48 | atomic.AddInt64(&i.ReadCount, 1) 49 | } 50 | 51 | func (i *Interactive) IncrementLikeCount() { 52 | atomic.AddInt64(&i.LikeCount, 1) 53 | } 54 | 55 | func (i *Interactive) IncrementCollectCount() { 56 | atomic.AddInt64(&i.CollectCount, 1) 57 | } 58 | 59 | func (p *Post) Abstract() string { 60 | // 将Content转换为一个rune切片 61 | str := []rune(p.Content) 62 | if len(str) > 128 { 63 | // 只保留前128个字符作为摘要 64 | str = str[:128] 65 | } 66 | return string(str) 67 | } 68 | -------------------------------------------------------------------------------- /internal/domain/rankingparameters.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type RankingParameter struct { 4 | ID uint 5 | Alpha float64 `json:"alpha"` // 点赞权重 6 | Beta float64 `json:"beta"` // 收藏权重 7 | Gamma float64 `json:"gamma"` // 阅读权重 8 | Lambda float64 `json:"lambda"` // 时间权重 9 | } 10 | -------------------------------------------------------------------------------- /internal/domain/relation.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type Relation struct { 4 | FolloweeId int64 5 | FollowerId int64 6 | } 7 | 8 | type RelationStats struct { 9 | FollowerCount int64 10 | FolloweeCount int64 11 | } 12 | -------------------------------------------------------------------------------- /internal/domain/role.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | // Menu 菜单 4 | type Menu struct { 5 | ID int `json:"id"` // 菜单ID 6 | Name string `json:"name"` // 菜单显示名称 7 | ParentID int `json:"parent_id"` // 上级菜单ID,0表示顶级菜单 8 | Path string `json:"path"` // 前端路由访问路径 9 | Component string `json:"component"` // 前端组件文件路径 10 | Icon string `json:"icon"` // 菜单显示图标 11 | SortOrder int `json:"sort_order"` // 菜单显示顺序,数值越小越靠前 12 | RouteName string `json:"route_name"` // 前端路由名称,需唯一 13 | Hidden int `json:"hidden"` // 菜单是否隐藏(0:显示 1:隐藏) 14 | CreateTime int64 `json:"create_time"` // 记录创建时间戳 15 | UpdateTime int64 `json:"update_time"` // 记录最后更新时间戳 16 | IsDeleted int `json:"is_deleted"` // 逻辑删除标记(0:未删除 1:已删除) 17 | Children []*Menu `json:"children"` // 子菜单列表 18 | } 19 | 20 | // Api API接口 21 | type Api struct { 22 | ID int `json:"id"` // 主键ID 23 | Name string `json:"name"` // API名称 24 | Path string `json:"path"` // API路径 25 | Method int `json:"method"` // HTTP请求方法(1:GET,2:POST,3:PUT,4:DELETE) 26 | Description string `json:"description"` // API描述 27 | Version string `json:"version"` // API版本 28 | Category int `json:"category"` // API分类(1:系统,2:业务) 29 | IsPublic int `json:"is_public"` // 是否公开(0:否,1:是) 30 | CreateTime int64 `json:"create_time"` // 创建时间 31 | UpdateTime int64 `json:"update_time"` // 更新时间 32 | IsDeleted int `json:"is_deleted"` // 是否删除(0:否,1:是) 33 | } 34 | 35 | // Role 角色 36 | type Role struct { 37 | ID int `json:"id"` // 主键ID 38 | Name string `json:"name"` // 角色名称 39 | Description string `json:"description"` // 角色描述 40 | RoleType int `json:"role_type"` // 角色类型(1:系统角色,2:自定义角色) 41 | IsDefault int `json:"is_default"` // 是否为默认角色(0:否,1:是) 42 | CreateTime int64 `json:"create_time"` // 创建时间 43 | UpdateTime int64 `json:"update_time"` // 更新时间 44 | IsDeleted int `json:"is_deleted"` // 是否删除(0:否,1:是) 45 | Menus []*Menu `json:"menus"` // 菜单列表 46 | Apis []*Api `json:"apis"` // API列表 47 | } 48 | -------------------------------------------------------------------------------- /internal/domain/search.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "time" 4 | 5 | type PostSearch struct { 6 | Id uint 7 | Title string 8 | AuthorId int64 9 | Status uint8 10 | Content string 11 | Tags string 12 | } 13 | 14 | type UserSearch struct { 15 | Id int64 16 | Nickname string 17 | Birthday time.Time 18 | Email string 19 | Phone string 20 | About string 21 | } 22 | 23 | type CommentSearch struct { 24 | Id uint // 评论ID 25 | AuthorId int64 // 评论者ID 26 | Status uint8 // 评论状态 27 | Content string // 评论内容 28 | } 29 | -------------------------------------------------------------------------------- /internal/domain/sms.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type AsyncSms struct { 4 | Id int64 5 | TplId string 6 | Args []string 7 | Numbers []string 8 | RetryMax int 9 | } 10 | -------------------------------------------------------------------------------- /internal/job/interfaces/ranking.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Post struct { 9 | ID uint 10 | Title string 11 | Content string 12 | UpdatedAt time.Time 13 | } 14 | 15 | type RankingService interface { 16 | TopN(ctx context.Context) error 17 | } 18 | -------------------------------------------------------------------------------- /internal/job/routes.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import "github.com/hibiken/asynq" 4 | 5 | type Routes struct { 6 | RefreshCache *RefreshCacheTask 7 | TimedTask *TimedTask 8 | } 9 | 10 | func NewRoutes(refreshCache *RefreshCacheTask, timedTask *TimedTask) *Routes { 11 | return &Routes{ 12 | RefreshCache: refreshCache, 13 | TimedTask: timedTask, 14 | } 15 | } 16 | 17 | func (r *Routes) RegisterHandlers() *asynq.ServeMux { 18 | mux := asynq.NewServeMux() 19 | 20 | mux.HandleFunc(RefreshPostCache, r.RefreshCache.ProcessTask) 21 | mux.HandleFunc(DeferTimedTask, r.TimedTask.ProcessTask) 22 | 23 | return mux 24 | } 25 | -------------------------------------------------------------------------------- /internal/job/scheduler.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/hibiken/asynq" 8 | ) 9 | 10 | const ( 11 | GetRankingTask = "get_ranking" 12 | ) 13 | 14 | type TimedScheduler struct { 15 | scheduler *asynq.Scheduler 16 | } 17 | 18 | func NewTimedScheduler(scheduler *asynq.Scheduler) *TimedScheduler { 19 | return &TimedScheduler{ 20 | scheduler: scheduler, 21 | } 22 | } 23 | 24 | func (s *TimedScheduler) RegisterTimedTasks() error { 25 | // 热榜刷新任务 - 每小时 26 | if err := s.registerTask( 27 | GetRankingTask, 28 | "@every 1h", 29 | ); err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (s *TimedScheduler) registerTask(taskName, cronSpec string) error { 37 | payload := TimedPayload{ 38 | TaskName: taskName, 39 | LastRunTime: time.Now(), 40 | } 41 | 42 | payloadBytes, err := json.Marshal(payload) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | task := asynq.NewTask(DeferTimedTask, payloadBytes) 48 | _, err = s.scheduler.Register(cronSpec, task) 49 | return err 50 | } 51 | 52 | func (s *TimedScheduler) Run() error { 53 | return s.scheduler.Run() 54 | } 55 | 56 | func (s *TimedScheduler) Stop() { 57 | s.scheduler.Shutdown() 58 | } 59 | -------------------------------------------------------------------------------- /internal/job/timed_task.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/GoSimplicity/LinkMe/internal/job/interfaces" 10 | "github.com/hibiken/asynq" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type TimedTask struct { 15 | l *zap.Logger 16 | svc interfaces.RankingService 17 | } 18 | 19 | type TimedPayload struct { 20 | TaskName string `json:"task_name"` 21 | LastRunTime time.Time `json:"last_run_time"` 22 | } 23 | 24 | func NewTimedTask(l *zap.Logger, svc interfaces.RankingService) *TimedTask { 25 | return &TimedTask{ 26 | l: l, 27 | svc: svc, 28 | } 29 | } 30 | 31 | func (t *TimedTask) ProcessTask(ctx context.Context, task *asynq.Task) error { 32 | var payload TimedPayload 33 | 34 | if err := json.Unmarshal(task.Payload(), &payload); err != nil { 35 | return fmt.Errorf("解析任务载荷失败: %v: %w", err, asynq.SkipRetry) 36 | } 37 | 38 | t.l.Info("开始处理定时任务", 39 | zap.String("task_name", payload.TaskName), 40 | zap.Time("last_run_time", payload.LastRunTime)) 41 | 42 | taskCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 43 | defer cancel() 44 | 45 | // 定义任务处理映射 46 | taskHandlers := map[string]func(context.Context) error{ 47 | GetRankingTask: t.svc.TopN, 48 | } 49 | 50 | // 获取对应的处理函数 51 | handler, exists := taskHandlers[payload.TaskName] 52 | if !exists { 53 | return fmt.Errorf("未知的任务类型: %s", payload.TaskName) 54 | } 55 | 56 | // 执行任务处理 57 | if err := handler(taskCtx); err != nil { 58 | t.l.Error("任务执行失败", 59 | zap.String("task_name", payload.TaskName), 60 | zap.Error(err)) 61 | return fmt.Errorf("%s: %w", payload.TaskName, err) 62 | } 63 | 64 | t.l.Info("成功完成任务", zap.String("task_name", payload.TaskName)) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /internal/job/types.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | const RefreshPostCache = "refresh_post_cache" 4 | const DeferTimedTask = "linkme:timed:task" 5 | -------------------------------------------------------------------------------- /internal/repository/activity.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 8 | ) 9 | 10 | type ActivityRepository interface { 11 | GetRecentActivity(ctx context.Context) ([]domain.RecentActivity, error) 12 | SetRecentActivity(ctx context.Context, dr domain.RecentActivity) error 13 | } 14 | 15 | type activityRepository struct { 16 | dao dao.ActivityDAO 17 | } 18 | 19 | func NewActivityRepository(dao dao.ActivityDAO) ActivityRepository { 20 | return &activityRepository{ 21 | dao: dao, 22 | } 23 | } 24 | 25 | func (a *activityRepository) GetRecentActivity(ctx context.Context) ([]domain.RecentActivity, error) { 26 | activity, err := a.dao.GetRecentActivity(ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return toDomainActivities(activity), nil 31 | } 32 | 33 | func (a *activityRepository) SetRecentActivity(ctx context.Context, dr domain.RecentActivity) error { 34 | err := a.dao.SetRecentActivity(ctx, fromDomainActivity(dr)) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | } 40 | 41 | // 将领域层对象转为dao层对象 42 | func fromDomainActivity(dr domain.RecentActivity) dao.RecentActivity { 43 | return dao.RecentActivity{ 44 | ID: dr.ID, 45 | Description: dr.Description, 46 | Time: dr.Time, 47 | UserID: dr.UserID, 48 | } 49 | } 50 | 51 | // 将dao层对象转为领域层对象 52 | func toDomainActivities(mrList []dao.RecentActivity) []domain.RecentActivity { 53 | domainList := make([]domain.RecentActivity, len(mrList)) 54 | for i, mr := range mrList { 55 | domainList[i] = domain.RecentActivity{ 56 | ID: mr.ID, 57 | Description: mr.Description, 58 | Time: mr.Time, 59 | UserID: mr.UserID, 60 | } 61 | } 62 | return domainList 63 | } 64 | -------------------------------------------------------------------------------- /internal/repository/cache/comment.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/GoSimplicity/LinkMe/internal/domain" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type CommentCache interface { 14 | Get(ctx context.Context, postId int64) (domain.Comment, error) 15 | Set(ctx context.Context, du domain.Comment) error 16 | } 17 | 18 | type commentCache struct { 19 | cmd redis.Cmdable 20 | expiration time.Duration 21 | } 22 | 23 | func NewCommentCache(cmd redis.Cmdable) CommentCache { 24 | return &commentCache{ 25 | cmd: cmd, 26 | expiration: time.Minute * 15, 27 | } 28 | } 29 | 30 | // Get 从redis中获取数据并反序列化 31 | func (u *commentCache) Get(ctx context.Context, postId int64) (domain.Comment, error) { 32 | var dc domain.Comment 33 | key := fmt.Sprintf("linkme:comment:%d", postId) 34 | // 从redis中读取数据 35 | data, err := u.cmd.Get(ctx, key).Result() 36 | if err != nil { 37 | if err == redis.Nil { 38 | return domain.Comment{}, err 39 | } 40 | return domain.Comment{}, err 41 | } 42 | if err = json.Unmarshal([]byte(data), &dc); err != nil { 43 | return domain.Comment{}, fmt.Errorf("反序列化评论数据失败: %v", err) 44 | } 45 | return dc, nil 46 | } 47 | 48 | // Set 将传入的du结构体序列化存入redis中 49 | func (u *commentCache) Set(ctx context.Context, dc domain.Comment) error { 50 | key := fmt.Sprintf("linkme:comment:%d", dc.PostId) 51 | data, err := json.Marshal(dc) 52 | if err != nil { 53 | return fmt.Errorf("序列化评论数据失败: %v", err) 54 | } 55 | 56 | if err := u.cmd.Set(ctx, key, data, u.expiration).Err(); err != nil { 57 | return fmt.Errorf("缓存评论数据失败: %v", err) 58 | } 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /internal/repository/cache/email.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/redis/go-redis/v9" 7 | "time" 8 | ) 9 | 10 | type EmailCache interface { 11 | GetVCode(ctx context.Context, email string) (string, error) 12 | StoreVCode(ctx context.Context, email, vCode string) error 13 | } 14 | 15 | type emailCache struct { 16 | client redis.Cmdable 17 | } 18 | 19 | func NewEmailCache(client redis.Cmdable) EmailCache { 20 | return &emailCache{ 21 | client: client, 22 | } 23 | } 24 | 25 | func (e emailCache) GetVCode(ctx context.Context, email string) (string, error) { 26 | return e.client.Get(ctx, genEmailKey(email)).Result() 27 | } 28 | 29 | func (e emailCache) StoreVCode(ctx context.Context, email, vCode string) error { 30 | return e.client.Set(ctx, genEmailKey(email), vCode, time.Duration(10)*time.Minute).Err() 31 | } 32 | 33 | func genEmailKey(email string) string { 34 | return fmt.Sprintf("linkme:email:%s", email) 35 | } 36 | -------------------------------------------------------------------------------- /internal/repository/cache/local.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/GoSimplicity/LinkMe/internal/domain" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | var ( 14 | ErrCacheEmpty = errors.New("本地缓存为空") 15 | ErrCacheExpired = errors.New("本地缓存已过期") 16 | ) 17 | 18 | type RankingLocalCache interface { 19 | Set(ctx context.Context, posts []domain.Post) error 20 | Get(ctx context.Context) ([]domain.Post, error) 21 | ForceGet(ctx context.Context) ([]domain.Post, error) 22 | } 23 | 24 | type rankingLocalCache struct { 25 | posts []domain.Post 26 | expireAt time.Time 27 | ttl time.Duration 28 | mu sync.RWMutex 29 | logger *zap.Logger 30 | } 31 | 32 | func NewRankingLocalCache(logger *zap.Logger) RankingLocalCache { 33 | return &rankingLocalCache{ 34 | ttl: 10 * time.Minute, 35 | logger: logger, 36 | } 37 | } 38 | 39 | func (r *rankingLocalCache) Set(ctx context.Context, posts []domain.Post) error { 40 | if len(posts) == 0 { 41 | r.logger.Warn("试图设置空缓存") 42 | return ErrCacheEmpty 43 | } 44 | 45 | r.mu.Lock() 46 | defer r.mu.Unlock() 47 | 48 | r.posts = make([]domain.Post, len(posts)) 49 | copy(r.posts, posts) 50 | r.expireAt = time.Now().Add(r.ttl) 51 | 52 | r.logger.Info("本地缓存已更新", 53 | zap.Int("post_count", len(posts)), 54 | zap.Time("expire_at", r.expireAt)) 55 | return nil 56 | } 57 | 58 | func (r *rankingLocalCache) Get(ctx context.Context) ([]domain.Post, error) { 59 | r.mu.RLock() 60 | defer r.mu.RUnlock() 61 | 62 | if err := r.validateCache(); err != nil { 63 | return nil, err 64 | } 65 | 66 | return r.copyPosts(), nil 67 | } 68 | 69 | func (r *rankingLocalCache) ForceGet(ctx context.Context) ([]domain.Post, error) { 70 | r.mu.RLock() 71 | defer r.mu.RUnlock() 72 | 73 | if len(r.posts) == 0 { 74 | r.logger.Warn("强制获取:本地缓存为空") 75 | return nil, ErrCacheEmpty 76 | } 77 | 78 | result := r.copyPosts() 79 | r.logger.Debug("强制获取本地缓存成功", 80 | zap.Int("post_count", len(result)), 81 | zap.Time("expire_at", r.expireAt)) 82 | return result, nil 83 | } 84 | 85 | func (r *rankingLocalCache) validateCache() error { 86 | if len(r.posts) == 0 { 87 | r.logger.Warn("本地缓存为空") 88 | return ErrCacheEmpty 89 | } 90 | 91 | if time.Now().After(r.expireAt) { 92 | r.logger.Warn("本地缓存已过期", 93 | zap.Time("expire_at", r.expireAt)) 94 | return ErrCacheExpired 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (r *rankingLocalCache) copyPosts() []domain.Post { 101 | result := make([]domain.Post, len(r.posts)) 102 | copy(result, r.posts) 103 | r.logger.Debug("本地缓存命中", 104 | zap.Int("post_count", len(result))) 105 | return result 106 | } 107 | -------------------------------------------------------------------------------- /internal/repository/cache/ranking.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "time" 8 | 9 | "github.com/GoSimplicity/LinkMe/internal/domain" 10 | "github.com/redis/go-redis/v9" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | var ( 15 | ErrMarshalPost = errors.New("序列化帖子失败") 16 | ErrUnmarshalPost = errors.New("反序列化帖子失败") 17 | ErrSetCache = errors.New("设置缓存失败") 18 | ErrGetCache = errors.New("获取缓存失败") 19 | ) 20 | 21 | type RankingRedisCache interface { 22 | Set(ctx context.Context, posts []domain.Post) error 23 | Get(ctx context.Context) ([]domain.Post, error) 24 | } 25 | 26 | type rankingCache struct { 27 | client redis.Cmdable 28 | key string 29 | expiration time.Duration 30 | logger *zap.Logger 31 | } 32 | 33 | func NewRankingRedisCache(client redis.Cmdable, logger *zap.Logger) RankingRedisCache { 34 | return &rankingCache{ 35 | client: client, 36 | key: "ranking:top_n", 37 | expiration: 3 * time.Minute, 38 | logger: logger, 39 | } 40 | } 41 | 42 | func (r *rankingCache) Set(ctx context.Context, posts []domain.Post) error { 43 | // 预处理帖子内容 44 | for i := range posts { 45 | posts[i].Content = posts[i].Abstract() 46 | } 47 | 48 | // 序列化帖子数据 49 | val, err := json.Marshal(posts) 50 | if err != nil { 51 | r.logger.Error("序列化帖子失败", zap.Error(err)) 52 | return ErrMarshalPost 53 | } 54 | 55 | // 设置缓存 56 | if err := r.client.Set(ctx, r.key, val, r.expiration).Err(); err != nil { 57 | r.logger.Error("设置缓存失败", 58 | zap.String("key", r.key), 59 | zap.Error(err)) 60 | return ErrSetCache 61 | } 62 | 63 | r.logger.Info("缓存设置成功", 64 | zap.String("key", r.key), 65 | zap.Int("post_count", len(posts))) 66 | return nil 67 | } 68 | 69 | func (r *rankingCache) Get(ctx context.Context) ([]domain.Post, error) { 70 | // 获取缓存数据 71 | val, err := r.client.Get(ctx, r.key).Bytes() 72 | if err != nil { 73 | if errors.Is(err, redis.Nil) { 74 | r.logger.Info("缓存未命中", zap.String("key", r.key)) 75 | return nil, nil 76 | } 77 | r.logger.Error("获取缓存失败", 78 | zap.String("key", r.key), 79 | zap.Error(err)) 80 | return nil, ErrGetCache 81 | } 82 | 83 | // 反序列化帖子数据 84 | var posts []domain.Post 85 | if err := json.Unmarshal(val, &posts); err != nil { 86 | r.logger.Error("反序列化帖子失败", zap.Error(err)) 87 | return nil, ErrUnmarshalPost 88 | } 89 | 90 | r.logger.Info("缓存获取成功", 91 | zap.String("key", r.key), 92 | zap.Int("post_count", len(posts))) 93 | 94 | return posts, nil 95 | } 96 | -------------------------------------------------------------------------------- /internal/repository/cache/redis_cmd.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "github.com/redis/go-redis/v9" 8 | "os" 9 | ) 10 | 11 | const ( 12 | SCRIPTPATH = "./script/commit.lua" 13 | ) 14 | 15 | var ( 16 | hash string 17 | script string 18 | ) 19 | 20 | type RedisEXCmd interface { 21 | AddCommand(args ...string) //添加redis命令 22 | Exec() error //执行具有修改性的redis命令,失败自动回滚 23 | Rollback() //可调用完成回滚 24 | } 25 | 26 | type redisEXCmd struct { 27 | cmd redis.Cmdable 28 | commandsTab []interface{} //存储命令 29 | rollbackTab []interface{} //存储回滚命令 30 | commandsCnt int //记录命令数量 31 | rollbackCnt int //记录回滚命令数量 32 | } 33 | 34 | func init() { 35 | //加载脚本 36 | if script == "" { 37 | t, _ := os.ReadFile(SCRIPTPATH) 38 | script = string(t) 39 | } 40 | } 41 | func NewRedisCmd(cmd redis.Cmdable) RedisEXCmd { 42 | 43 | return &redisEXCmd{ 44 | cmd: cmd, 45 | } 46 | } 47 | 48 | func (rc *redisEXCmd) AddCommand(args ...string) { 49 | var v []interface{} 50 | for _, arg := range args { 51 | v = append(v, arg) 52 | } 53 | rc.commandsTab = append(rc.commandsTab, v) 54 | rc.commandsCnt++ 55 | } 56 | 57 | // Exec 执行脚本 58 | func (rc *redisEXCmd) Exec() error { 59 | //fmt.Println(rc.commandsTab[:rc.commandsCnt]) 60 | cmd, _ := json.Marshal(rc.commandsTab[:rc.commandsCnt]) 61 | //标记清除存储的命令 62 | rc.commandsCnt = 0 63 | 64 | ctx := context.Background() 65 | //先检测脚本是否缓存 66 | if exist, _ := rc.cmd.ScriptExists(ctx, hash).Result(); !exist[0] { 67 | //缓存脚本 68 | hash = rc.cmd.ScriptLoad(ctx, script).Val() 69 | } 70 | //执行缓存脚本 71 | res, err := rc.cmd.EvalSha(ctx, hash, []string{}, cmd).Result() 72 | if err != nil { 73 | //日志 74 | 75 | return err 76 | } 77 | //redis命令执行失败 78 | if res.([]interface{})[0] != "OK" { 79 | return errors.New(res.([]interface{})[0].(string)) 80 | } 81 | //解析可能执行的回滚命令 82 | cmds := res.([]interface{})[1].([]interface{}) 83 | rc.rollbackCnt = len(cmds) 84 | for _, v := range cmds { 85 | rc.rollbackTab = append(rc.rollbackTab, v.([]interface{})) 86 | } 87 | //todo 超时取消 88 | 89 | return nil 90 | } 91 | 92 | func (rc *redisEXCmd) Rollback() { 93 | //fmt.Println(rc.rollbackTab) 94 | luaScript := ` 95 | local cmds = cjson.decode(ARGV[1]) 96 | for _, cmd in ipairs(cmds) do 97 | redis.call(unpack(cmd)) 98 | end 99 | ` 100 | 101 | cmd, _ := json.Marshal(rc.rollbackTab[:rc.rollbackCnt]) 102 | rc.rollbackCnt = 0 103 | 104 | rc.cmd.Eval(context.Background(), luaScript, []string{}, cmd) 105 | 106 | } 107 | -------------------------------------------------------------------------------- /internal/repository/cache/user.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/GoSimplicity/LinkMe/internal/domain" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type UserCache interface { 14 | Get(ctx context.Context, uid int64) (domain.User, error) 15 | Set(ctx context.Context, du domain.User) error 16 | } 17 | 18 | type userCache struct { 19 | cmd redis.Cmdable 20 | expiration time.Duration 21 | } 22 | 23 | func NewUserCache(cmd redis.Cmdable) UserCache { 24 | return &userCache{ 25 | cmd: cmd, 26 | expiration: time.Minute * 15, 27 | } 28 | } 29 | 30 | // Get 从redis中获取数据并反序列化 31 | func (u *userCache) Get(ctx context.Context, uid int64) (domain.User, error) { 32 | if uid <= 0 { 33 | return domain.User{}, fmt.Errorf("无效的用户ID: %d", uid) 34 | } 35 | 36 | var du domain.User 37 | key := fmt.Sprintf("linkeme:user:%d", uid) 38 | 39 | // 从redis中读取数据 40 | data, err := u.cmd.Get(ctx, key).Result() 41 | if err != nil { 42 | if err == redis.Nil { 43 | return domain.User{}, fmt.Errorf("缓存中未找到用户 %d", uid) 44 | } 45 | return domain.User{}, fmt.Errorf("从缓存获取用户失败: %v", err) 46 | } 47 | 48 | if err = json.Unmarshal([]byte(data), &du); err != nil { 49 | return domain.User{}, fmt.Errorf("反序列化用户数据失败: %v", err) 50 | } 51 | 52 | // 如果用户已被删除,则不返回数据 53 | if du.Deleted { 54 | return domain.User{}, fmt.Errorf("用户 %d 已被删除", uid) 55 | } 56 | 57 | return du, nil 58 | } 59 | 60 | // Set 将传入的du结构体序列化存入redis中 61 | func (u *userCache) Set(ctx context.Context, du domain.User) error { 62 | if du.ID <= 0 { 63 | return fmt.Errorf("无效的用户ID: %d", du.ID) 64 | } 65 | 66 | key := fmt.Sprintf("linkme:user:%d", du.ID) 67 | data, err := json.Marshal(du) 68 | if err != nil { 69 | return fmt.Errorf("序列化用户数据失败: %v", err) 70 | } 71 | 72 | // 向redis中插入数据,使用pipeline优化性能 73 | pipe := u.cmd.Pipeline() 74 | pipe.Set(ctx, key, data, u.expiration) 75 | _, err = pipe.Exec(ctx) 76 | if err != nil { 77 | return fmt.Errorf("缓存用户数据失败: %v", err) 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/repository/dao/activity.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/zap" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type ActivityDAO interface { 11 | GetRecentActivity(ctx context.Context) ([]RecentActivity, error) 12 | SetRecentActivity(ctx context.Context, mr RecentActivity) error 13 | } 14 | 15 | type activityDAO struct { 16 | db *gorm.DB 17 | l *zap.Logger 18 | } 19 | 20 | type RecentActivity struct { 21 | ID int64 `gorm:"primaryKey;autoIncrement"` 22 | UserID int64 `gorm:"column:user_id;not null" json:"user_id"` 23 | Description string `gorm:"type:varchar(255);not null"` 24 | Time string `gorm:"type:varchar(255);not null"` 25 | } 26 | 27 | func NewActivityDAO(db *gorm.DB, l *zap.Logger) ActivityDAO { 28 | return &activityDAO{ 29 | db: db, 30 | l: l, 31 | } 32 | } 33 | 34 | func (a *activityDAO) GetRecentActivity(ctx context.Context) ([]RecentActivity, error) { 35 | var mr []RecentActivity 36 | // 使用事务和上下文 37 | tx := a.db.WithContext(ctx).Model(&RecentActivity{}) 38 | // 执行查询 39 | if err := tx.Find(&mr).Error; err != nil { 40 | a.l.Error("failed to get recent activity", 41 | zap.Error(err), 42 | zap.String("method", "GetRecentActivity"), 43 | zap.Any("context", ctx)) 44 | return nil, err 45 | } 46 | return mr, nil 47 | } 48 | 49 | func (a *activityDAO) SetRecentActivity(ctx context.Context, mr RecentActivity) error { 50 | if err := a.db.WithContext(ctx).Create(&mr).Error; err != nil { 51 | a.l.Error("set recent activity failed", zap.Error(err)) 52 | return err 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/repository/dao/init.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // InitTables 初始化数据库表 8 | func InitTables(db *gorm.DB) error { 9 | return db.AutoMigrate( 10 | &User{}, 11 | &Profile{}, 12 | &Post{}, 13 | &PubPost{}, 14 | &Menu{}, 15 | &Api{}, 16 | &Role{}, 17 | &Interactive{}, 18 | &UserCollectionBiz{}, 19 | &UserLikeBiz{}, 20 | &VCodeSmsLog{}, 21 | &Check{}, 22 | &Plate{}, 23 | &RecentActivity{}, 24 | &Comment{}, 25 | &Relation{}, 26 | &RelationCount{}, 27 | &LotteryDraw{}, 28 | &SecondKillEvent{}, 29 | &Participant{}, 30 | &RankingParameter{}, 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /internal/repository/dao/rankingparameters.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | "gorm.io/gorm" 7 | "time" 8 | ) 9 | 10 | type RankingParameterDAO interface { 11 | Insert(ctx context.Context, rankingParameter RankingParameter) (uint, error) // Note:为了保证可以看到历史参数的内容,只以追加的形式插入,不进行更新 12 | FindLastParameter(ctx context.Context) (RankingParameter, error) // Note: 查找最后一次插入的参数 13 | //Update(ctx context.Context, rankingParameter RankingParameter) error 14 | //GetById(ctx context.Context, id uint) (RankingParameter, error) 15 | } 16 | type rankingParameterDAO struct { 17 | l *zap.Logger 18 | db *gorm.DB 19 | } 20 | 21 | type RankingParameter struct { 22 | gorm.Model 23 | ID uint `gorm:"primaryKey" json:"id"` // 主键 ID 24 | Alpha float64 `gorm:"not null;default:1.0" json:"alpha"` // 默认值 1.0 25 | Beta float64 `gorm:"not null;default:10.0" json:"beta"` // 默认值 10.0 26 | Gamma float64 `gorm:"not null;default:20.0" json:"gamma"` // 默认值 20.0 27 | Lambda float64 `gorm:"not null;default:1.2" json:"lambda"` // 默认值 1.2 28 | // CreatedAt 和 UpdatedAt 可根据需要自动填充 29 | CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` 30 | } 31 | 32 | func NewRankingParameterDAO(db *gorm.DB, l *zap.Logger) RankingParameterDAO { 33 | return &rankingParameterDAO{ 34 | l: l, 35 | db: db, 36 | } 37 | } 38 | func (r *rankingParameterDAO) Insert(ctx context.Context, rankingParameter RankingParameter) (uint, error) { 39 | if err := r.db.WithContext(ctx).Create(&rankingParameter).Error; err != nil { 40 | r.l.Error("插入RankingParameter失败", zap.Error(err)) 41 | return 0, err 42 | } 43 | return rankingParameter.ID, nil 44 | } 45 | func (r *rankingParameterDAO) FindLastParameter(ctx context.Context) (RankingParameter, error) { 46 | var rankingParameter RankingParameter 47 | if err := r.db.WithContext(ctx).Last(&rankingParameter).Error; err != nil { 48 | r.l.Error("查找RankingParameter失败", zap.Error(err)) 49 | return RankingParameter{}, err 50 | } 51 | return rankingParameter, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/repository/dao/sms.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | "gorm.io/gorm" 7 | "time" 8 | ) 9 | 10 | type SmsDAO interface { 11 | Insert(ctx context.Context, log VCodeSmsLog) error 12 | FindFailedLogs(ctx context.Context) ([]VCodeSmsLog, error) //查找当前时刻以前,发送失败的logs,后续需要重新发送 13 | Update(ctx context.Context, log VCodeSmsLog) error 14 | } 15 | 16 | type smsDao struct { 17 | db *gorm.DB 18 | l *zap.Logger 19 | } 20 | 21 | // VCodeSmsLog 表示用户认证操作的日志记录 22 | type VCodeSmsLog struct { 23 | Id int64 `gorm:"column:id;primaryKey;autoIncrement"` // 自增ID 24 | SmsId int64 `gorm:"column:sms_id"` // 短信类型ID 25 | SmsType string `gorm:"column:sms_type"` // 短信类型 26 | Mobile string `gorm:"column:mobile"` // 手机号 27 | VCode string `gorm:"column:v_code"` // 验证码 28 | Driver string `gorm:"column:driver"` // 服务商类型 29 | Status int64 `gorm:"column:status"` // 发送状态,1为成功,0为失败 30 | StatusCode string `gorm:"column:status_code"` // 状态码 31 | CreateTime int64 `gorm:"column:created_at;type:bigint;not null"` // 创建时间 32 | UpdatedTime int64 `gorm:"column:updated_at;type:bigint;not null;index"` // 更新时间 33 | DeletedTime int64 `gorm:"column:deleted_at;type:bigint;index"` // 删除时间 34 | } 35 | 36 | func NewSmsDAO(db *gorm.DB, l *zap.Logger) SmsDAO { 37 | return &smsDao{ 38 | db: db, 39 | l: l, 40 | } 41 | } 42 | 43 | func (s *smsDao) Insert(ctx context.Context, log VCodeSmsLog) error { 44 | return s.db.WithContext(ctx).Create(&log).Error 45 | } 46 | 47 | func (s *smsDao) FindFailedLogs(ctx context.Context) ([]VCodeSmsLog, error) { 48 | var logs []VCodeSmsLog 49 | now := time.Now().Unix() 50 | err := s.db.WithContext(ctx). 51 | Where("status = ? AND CreateTime < ?", 0, now). 52 | Find(&logs).Error 53 | if err != nil { 54 | return nil, err 55 | } //如果status 为0 或者创建时间比现在要早,则发送错误信息 56 | return logs, nil 57 | } 58 | 59 | func (s *smsDao) Update(ctx context.Context, log VCodeSmsLog) error { 60 | log.UpdatedTime = time.Now().Unix() //更新时初始化时间戳 61 | return s.db.WithContext(ctx).Save(&log).Error 62 | } 63 | -------------------------------------------------------------------------------- /internal/repository/email.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/GoSimplicity/LinkMe/internal/repository/cache" 7 | qqEmail "github.com/GoSimplicity/LinkMe/pkg/email" 8 | "github.com/GoSimplicity/LinkMe/utils" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type EmailRepository interface { 13 | SendCode(ctx context.Context, email string) error 14 | CheckCode(ctx context.Context, email, vCode string) (bool, error) 15 | } 16 | 17 | // emailRepository 实现了 EmailRepository 接口 18 | type emailRepository struct { 19 | cache cache.EmailCache 20 | l *zap.Logger 21 | } 22 | 23 | // NewEmailRepository 创建并返回一个新的 smsRepository 实例 24 | func NewEmailRepository(cache cache.EmailCache, l *zap.Logger) EmailRepository { 25 | return &emailRepository{ 26 | cache: cache, 27 | l: l, 28 | } 29 | } 30 | 31 | func (e emailRepository) SendCode(ctx context.Context, email string) error { 32 | e.l.Info("[emailRepository.SendCode]", zap.String("email", email)) 33 | vCode := utils.GenRandomCode(6) 34 | e.l.Info("[emailRepository.SendCode]", zap.String("vCode", vCode)) 35 | if err := e.cache.StoreVCode(ctx, email, vCode); err != nil { 36 | e.l.Error("[emailRepository.SendCode] StoreVCode失败", zap.Error(err)) 37 | return err 38 | } 39 | body := fmt.Sprintf("您的验证码是:%s", vCode) 40 | return qqEmail.SendEmail(email, "【LinkMe】密码重置", body) 41 | } 42 | 43 | func (e emailRepository) CheckCode(ctx context.Context, email, vCode string) (bool, error) { 44 | storedCode, err := e.cache.GetVCode(ctx, email) 45 | return storedCode == vCode, err 46 | } 47 | -------------------------------------------------------------------------------- /internal/repository/history.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/repository/cache" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type HistoryRepository interface { 12 | GetHistory(ctx context.Context, pagination domain.Pagination) ([]domain.History, error) 13 | SetHistory(ctx context.Context, post domain.Post) error 14 | DeleteOneHistory(ctx context.Context, postId uint, uid int64) error 15 | DeleteAllHistory(ctx context.Context, uid int64) error 16 | } 17 | 18 | type historyRepository struct { 19 | l *zap.Logger 20 | cache cache.HistoryCache 21 | } 22 | 23 | func NewHistoryRepository(l *zap.Logger, cache cache.HistoryCache) HistoryRepository { 24 | return &historyRepository{ 25 | l: l, 26 | cache: cache, 27 | } 28 | } 29 | 30 | // GetHistory 获取历史记录 31 | func (h *historyRepository) GetHistory(ctx context.Context, pagination domain.Pagination) ([]domain.History, error) { 32 | histories, err := h.cache.GetCache(ctx, pagination) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return histories, nil 38 | } 39 | 40 | // SetHistory 设置历史记录 41 | func (h *historyRepository) SetHistory(ctx context.Context, post domain.Post) error { 42 | history := toDomainHistory(post) 43 | 44 | return h.cache.SetCache(ctx, history) 45 | } 46 | 47 | // DeleteOneHistory 删除一条历史记录 48 | func (h *historyRepository) DeleteOneHistory(ctx context.Context, postId uint, uid int64) error { 49 | return h.cache.DeleteOneCache(ctx, postId, uid) 50 | } 51 | 52 | func (h *historyRepository) DeleteAllHistory(ctx context.Context, uid int64) error { 53 | return h.cache.DeleteAllHistory(ctx, uid) 54 | } 55 | 56 | // createContentSummary 创建内容摘要,限制为28个汉字 57 | func createContentSummary(content string) string { 58 | const limit = 28 59 | 60 | runes := []rune(content) 61 | if len(runes) <= limit { 62 | return content 63 | } 64 | 65 | return string(runes[:limit]) 66 | } 67 | 68 | // toDomainHistory 将帖子转换为历史记录 69 | func toDomainHistory(post domain.Post) domain.History { 70 | return domain.History{ 71 | Content: createContentSummary(post.Content), 72 | Uid: post.Uid, 73 | Tags: post.Tags, 74 | PostID: post.ID, 75 | Title: post.Title, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/repository/permission.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type PermissionRepository interface { 11 | AssignRole(ctx context.Context, roleId int, menuIds []int, apiIds []int) error 12 | AssignRoleToUser(ctx context.Context, userId int, roleIds []int, menuIds []int, apiIds []int) error 13 | AssignRoleToUsers(ctx context.Context, userIds []int, roleIds []int, menuIds []int, apiIds []int) error 14 | RemoveUserPermissions(ctx context.Context, userId int) error 15 | RemoveRolePermissions(ctx context.Context, roleId int) error 16 | RemoveUsersPermissions(ctx context.Context, userIds []int) error 17 | } 18 | 19 | type permissionRepository struct { 20 | l *zap.Logger 21 | dao dao.PermissionDAO 22 | } 23 | 24 | func NewPermissionRepository(l *zap.Logger, dao dao.PermissionDAO) PermissionRepository { 25 | return &permissionRepository{ 26 | l: l, 27 | dao: dao, 28 | } 29 | } 30 | 31 | // AssignRole 实现角色权限分配 32 | func (p *permissionRepository) AssignRole(ctx context.Context, roleId int, menuIds []int, apiIds []int) error { 33 | return p.dao.AssignRole(ctx, roleId, menuIds, apiIds) 34 | } 35 | 36 | // AssignRoleToUser 实现用户角色权限分配 37 | func (p *permissionRepository) AssignRoleToUser(ctx context.Context, userId int, roleIds []int, menuIds []int, apiIds []int) error { 38 | return p.dao.AssignRoleToUser(ctx, userId, roleIds, menuIds, apiIds) 39 | } 40 | 41 | // RemoveRolePermissions 实现角色权限移除 42 | func (p *permissionRepository) RemoveRolePermissions(ctx context.Context, roleId int) error { 43 | return p.dao.RemoveRolePermissions(ctx, roleId) 44 | } 45 | 46 | // RemoveUserPermissions 实现用户权限移除 47 | func (p *permissionRepository) RemoveUserPermissions(ctx context.Context, userId int) error { 48 | return p.dao.RemoveUserPermissions(ctx, userId) 49 | } 50 | 51 | // AssignRoleToUsers 实现批量用户角色权限分配 52 | func (p *permissionRepository) AssignRoleToUsers(ctx context.Context, userIds []int, roleIds []int, menuIds []int, apiIds []int) error { 53 | return p.dao.AssignRoleToUsers(ctx, userIds, roleIds, menuIds, apiIds) 54 | } 55 | 56 | // RemoveUsersPermissions 实现批量用户权限移除 57 | func (p *permissionRepository) RemoveUsersPermissions(ctx context.Context, userIds []int) error { 58 | return p.dao.RemoveUsersPermissions(ctx, userIds) 59 | } 60 | -------------------------------------------------------------------------------- /internal/repository/plate.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type PlateRepository interface { 12 | CreatePlate(ctx context.Context, plate domain.Plate) error 13 | ListPlate(ctx context.Context, pagination domain.Pagination) ([]domain.Plate, error) 14 | UpdatePlate(ctx context.Context, plate domain.Plate) error 15 | DeletePlate(ctx context.Context, plateId int64, uid int64) error 16 | } 17 | 18 | type plateRepository struct { 19 | l *zap.Logger 20 | dao dao.PlateDAO 21 | } 22 | 23 | func NewPlateRepository(l *zap.Logger, dao dao.PlateDAO) PlateRepository { 24 | return &plateRepository{ 25 | l: l, 26 | dao: dao, 27 | } 28 | } 29 | 30 | func (p *plateRepository) CreatePlate(ctx context.Context, plate domain.Plate) error { 31 | return p.dao.CreatePlate(ctx, plate) 32 | } 33 | 34 | func (p *plateRepository) ListPlate(ctx context.Context, pagination domain.Pagination) ([]domain.Plate, error) { 35 | plates, err := p.dao.ListPlate(ctx, pagination) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return fromDomainSlicePlate(plates), err 40 | } 41 | 42 | func (p *plateRepository) UpdatePlate(ctx context.Context, plate domain.Plate) error { 43 | return p.dao.UpdatePlate(ctx, plate) 44 | } 45 | 46 | func (p *plateRepository) DeletePlate(ctx context.Context, plateId int64, uid int64) error { 47 | return p.dao.DeletePlate(ctx, plateId, uid) 48 | } 49 | 50 | // 将dao层对象转为领域层对象 51 | func fromDomainSlicePlate(post []dao.Plate) []domain.Plate { 52 | domainPlate := make([]domain.Plate, len(post)) 53 | for i, repoPlate := range post { 54 | domainPlate[i] = domain.Plate{ 55 | ID: repoPlate.ID, 56 | Name: repoPlate.Name, 57 | Uid: repoPlate.Uid, 58 | Description: repoPlate.Description, 59 | CreatedAt: repoPlate.CreateTime, 60 | UpdatedAt: repoPlate.UpdatedTime, 61 | DeletedAt: repoPlate.DeletedTime, 62 | Deleted: repoPlate.Deleted, 63 | } 64 | } 65 | return domainPlate 66 | } 67 | -------------------------------------------------------------------------------- /internal/repository/ranking.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/GoSimplicity/LinkMe/internal/domain" 8 | "github.com/GoSimplicity/LinkMe/internal/repository/cache" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type RankingRepository interface { 13 | ReplaceTopN(ctx context.Context, posts []domain.Post) error 14 | GetTopN(ctx context.Context) ([]domain.Post, error) 15 | } 16 | 17 | type rankingRepository struct { 18 | redisCache cache.RankingRedisCache 19 | localCache cache.RankingLocalCache 20 | l *zap.Logger 21 | } 22 | 23 | func NewRankingRepository(redisCache cache.RankingRedisCache, localCache cache.RankingLocalCache, l *zap.Logger) RankingRepository { 24 | return &rankingRepository{ 25 | redisCache: redisCache, 26 | localCache: localCache, 27 | l: l, 28 | } 29 | } 30 | 31 | // GetTopN 从缓存中获取排名前 N 的帖子 32 | func (rc *rankingRepository) GetTopN(ctx context.Context) ([]domain.Post, error) { 33 | // 优先从本地缓存获取 34 | if posts, err := rc.localCache.Get(ctx); err == nil { 35 | rc.l.Debug("本地缓存命中") 36 | return posts, nil 37 | } 38 | 39 | // 从 Redis 获取 40 | posts, err := rc.redisCache.Get(ctx) 41 | if err != nil { 42 | rc.l.Warn("Redis 缓存未命中", zap.Error(err)) 43 | // Redis 未命中时强制从本地缓存获取 44 | return rc.localCache.ForceGet(ctx) 45 | } 46 | 47 | rc.l.Debug("Redis 缓存命中") 48 | 49 | // 异步更新本地缓存 50 | go func() { 51 | if err := rc.localCache.Set(context.Background(), posts); err != nil { 52 | rc.l.Error("更新本地缓存失败", zap.Error(err)) 53 | } 54 | }() 55 | 56 | return posts, nil 57 | } 58 | 59 | // ReplaceTopN 替换缓存中的排名前 N 的帖子 60 | func (rc *rankingRepository) ReplaceTopN(ctx context.Context, posts []domain.Post) error { 61 | errChan := make(chan error, 2) 62 | 63 | // 并发更新缓存 64 | go func() { 65 | errChan <- rc.localCache.Set(ctx, posts) 66 | }() 67 | 68 | go func() { 69 | errChan <- rc.redisCache.Set(ctx, posts) 70 | }() 71 | 72 | // 等待两个更新操作完成 73 | var errs []error 74 | for i := 0; i < 2; i++ { 75 | if err := <-errChan; err != nil { 76 | errs = append(errs, err) 77 | } 78 | } 79 | 80 | if len(errs) > 0 { 81 | return fmt.Errorf("替换缓存失败: %v", errs) 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /internal/repository/rankingparameters.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type RankingParameterRepository interface { 11 | Insert(ctx context.Context, rankingParameter domain.RankingParameter) (uint, error) // Note:为了保证可以看到历史参数的内容,只以追加的形式插入,不进行更新 12 | FindLastParameter(ctx context.Context) (domain.RankingParameter, error) // Note: 查找最后一次插入的参数 13 | } 14 | type rankingParameterRepository struct { 15 | dao dao.RankingParameterDAO 16 | l *zap.Logger 17 | } 18 | 19 | func NewRankingParameterRepository(dao dao.RankingParameterDAO, l *zap.Logger) RankingParameterRepository { 20 | return &rankingParameterRepository{ 21 | dao: dao, 22 | l: l, 23 | } 24 | } 25 | 26 | func (r *rankingParameterRepository) Insert(ctx context.Context, rankingParameter domain.RankingParameter) (uint, error) { 27 | rankingParameterID, err := r.dao.Insert(ctx, toDaoRankingParameter(rankingParameter)) 28 | if err != nil { 29 | r.l.Error("插入RankingParameter失败", zap.Error(err)) 30 | return 0, err 31 | } 32 | return rankingParameterID, nil 33 | } 34 | func (r *rankingParameterRepository) FindLastParameter(ctx context.Context) (domain.RankingParameter, error) { 35 | rankingParameterVal, err := r.dao.FindLastParameter(ctx) 36 | if err != nil { 37 | r.l.Error("查找RankingParameter失败", zap.Error(err)) 38 | return domain.RankingParameter{}, err 39 | } 40 | return toDomainRankingParameter(rankingParameterVal), nil 41 | } 42 | func toDomainRankingParameter(rankingParameter dao.RankingParameter) domain.RankingParameter { 43 | return domain.RankingParameter{ 44 | ID: rankingParameter.ID, 45 | Alpha: rankingParameter.Alpha, 46 | Beta: rankingParameter.Beta, 47 | Gamma: rankingParameter.Gamma, 48 | Lambda: rankingParameter.Lambda, 49 | } 50 | } 51 | func toDaoRankingParameter(rankingParameter domain.RankingParameter) dao.RankingParameter { 52 | return dao.RankingParameter{ 53 | ID: rankingParameter.ID, 54 | Alpha: rankingParameter.Alpha, 55 | Beta: rankingParameter.Beta, 56 | Gamma: rankingParameter.Gamma, 57 | Lambda: rankingParameter.Lambda, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/repository/search_test.go: -------------------------------------------------------------------------------- 1 | package repository_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 7 | "github.com/elastic/go-elasticsearch/v8" 8 | "go.uber.org/zap" 9 | "gorm.io/gorm" 10 | "testing" 11 | ) 12 | 13 | // 辅助函数,用于创建一个模拟的Elasticsearch客户端,便于测试 14 | func createMockElasticsearchClient() *elasticsearch.TypedClient { 15 | client, err := elasticsearch.NewTypedClient(elasticsearch.Config{ 16 | Addresses: []string{"http://localhost:9200"}, 17 | Username: "elastic", 18 | Password: "07X-o2S2eD7TrGodvKJw", 19 | }) 20 | if err != nil { 21 | panic(err) 22 | } 23 | return client 24 | } 25 | 26 | // 辅助函数,用于创建一个模拟的GORM数据库实例,这里简单返回nil,根据实际情况可进一步完善模拟逻辑 27 | func createMockGormDB() *gorm.DB { 28 | return nil 29 | } 30 | 31 | // 辅助函数,用于创建一个模拟的Zap日志记录器,这里简单返回一个空的实现,根据实际情况可替换为真实日志记录器或更完善的模拟 32 | func createMockLogger() *zap.Logger { 33 | return zap.NewNop() 34 | } 35 | 36 | // 测试CreatePostIndex函数 37 | func TestCreatePostIndex(t *testing.T) { 38 | searchDAO := dao.NewSearchDAO(createMockElasticsearchClient(), createMockLogger()) 39 | err := searchDAO.CreatePostIndex(context.Background()) 40 | if err != nil { 41 | t.Errorf("CreatePostIndex failed: %v", err) 42 | } 43 | } 44 | 45 | // 测试SearchPosts函数 46 | func TestSearchPosts(t *testing.T) { 47 | searchDAO := dao.NewSearchDAO(createMockElasticsearchClient(), createMockLogger()) 48 | keywords := []string{"test", "post"} 49 | posts, err := searchDAO.SearchPosts(context.Background(), keywords) 50 | if err != nil { 51 | t.Errorf("SearchPosts failed: %v", err) 52 | return 53 | } 54 | if len(posts) == 0 { 55 | t.Log("No posts found, but test passed as long as there's no error") 56 | return 57 | } 58 | // 可以进一步验证返回的帖子数据结构是否符合预期 59 | for _, post := range posts { 60 | postJSON, _ := json.Marshal(post) 61 | t.Logf("Retrieved post: %s", postJSON) 62 | } 63 | } 64 | 65 | // 测试SearchUsers函数 66 | func TestSearchUsers(t *testing.T) { 67 | searchDAO := dao.NewSearchDAO(createMockElasticsearchClient(), createMockLogger()) 68 | keywords := []string{"test", "user"} 69 | users, err := searchDAO.SearchUsers(context.Background(), keywords) 70 | if err != nil { 71 | t.Errorf("SearchUsers failed: %v", err) 72 | } 73 | if len(users) == 0 { 74 | t.Log("No users found, but test passed as long as there's no error") 75 | } 76 | // 可以进一步验证返回的用户数据结构是否符合预期 77 | for _, user := range users { 78 | userJSON, _ := json.Marshal(user) 79 | t.Logf("Retrieved user: %s", userJSON) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/repository/sms_test.go: -------------------------------------------------------------------------------- 1 | package repository_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/GoSimplicity/LinkMe/internal/repository" 9 | "github.com/GoSimplicity/LinkMe/internal/repository/cache" 10 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 11 | "github.com/GoSimplicity/LinkMe/ioc" 12 | "github.com/spf13/pflag" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | func TestSendCode(t *testing.T) { 17 | configFile := pflag.String("config", "../../config/config.yaml", "配置文件路径") 18 | pflag.Parse() 19 | viper.SetConfigFile(*configFile) 20 | err := viper.ReadInConfig() 21 | if err != nil { 22 | panic(err) 23 | } 24 | logger := ioc.InitLogger() 25 | d := dao.NewSmsDAO(ioc.InitDB(), logger) 26 | c := cache.NewSMSCache(ioc.InitRedis()) 27 | client := ioc.InitSms() 28 | repo := repository.NewSmsRepository(d, c, logger, client) 29 | if er := repo.SendCode(context.Background(), "xxx"); er != nil { 30 | fmt.Println(er) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/repository/sql_job.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 7 | "time" 8 | ) 9 | 10 | // CronJobRepository 定义了定时任务仓库接口 11 | type CronJobRepository interface { 12 | Preempt(ctx context.Context) (domain.Job, error) 13 | Release(ctx context.Context, jobId int64) error 14 | UpdateTime(ctx context.Context, id int64) error 15 | UpdateNextTime(ctx context.Context, id int64, nextTime time.Time) error 16 | } 17 | 18 | // cronJobRepository 实现了 CronJobRepository 接口 19 | type cronJobRepository struct { 20 | dao dao.JobDAO 21 | } 22 | 23 | // NewCronJobRepository 创建并初始化 cronJobRepository 实例 24 | func NewCronJobRepository(dao dao.JobDAO) CronJobRepository { 25 | return &cronJobRepository{ 26 | dao: dao, 27 | } 28 | } 29 | 30 | // Preempt 从数据库中抢占一个任务 31 | func (c *cronJobRepository) Preempt(ctx context.Context) (domain.Job, error) { 32 | j, err := c.dao.Preempt(ctx) 33 | if err != nil { 34 | return domain.Job{}, err 35 | } 36 | return domain.Job{ 37 | Id: j.Id, 38 | Expression: j.Expression, 39 | Executor: j.Executor, 40 | Name: j.Name, 41 | }, nil 42 | } 43 | 44 | // Release 释放一个任务 45 | func (c *cronJobRepository) Release(ctx context.Context, jobId int64) error { 46 | return c.dao.Release(ctx, jobId) 47 | } 48 | 49 | // UpdateTime 更新任务的时间 50 | func (c *cronJobRepository) UpdateTime(ctx context.Context, id int64) error { 51 | return c.dao.UpdateTime(ctx, id) 52 | } 53 | 54 | // UpdateNextTime 更新任务的下次执行时间 55 | func (c *cronJobRepository) UpdateNextTime(ctx context.Context, id int64, nextTime time.Time) error { 56 | return c.dao.UpdateNextTime(ctx, id, nextTime) 57 | } 58 | -------------------------------------------------------------------------------- /internal/service/activity.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/repository" 8 | ) 9 | 10 | type ActivityService interface { 11 | GetRecentActivity(ctx context.Context) ([]domain.RecentActivity, error) 12 | } 13 | 14 | type activityService struct { 15 | repo repository.ActivityRepository 16 | } 17 | 18 | func NewActivityService(repo repository.ActivityRepository) ActivityService { 19 | return &activityService{ 20 | repo: repo, 21 | } 22 | } 23 | 24 | func (a *activityService) GetRecentActivity(ctx context.Context) ([]domain.RecentActivity, error) { 25 | activity, err := a.repo.GetRecentActivity(ctx) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return activity, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/service/api.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/GoSimplicity/LinkMe/internal/domain" 8 | "github.com/GoSimplicity/LinkMe/internal/repository" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type ApiService interface { 13 | CreateApi(ctx context.Context, api *domain.Api) error 14 | GetApiById(ctx context.Context, id int) (*domain.Api, error) 15 | UpdateApi(ctx context.Context, api *domain.Api) error 16 | DeleteApi(ctx context.Context, id int) error 17 | ListApis(ctx context.Context, page, pageSize int) ([]*domain.Api, int, error) 18 | } 19 | 20 | type apiService struct { 21 | l *zap.Logger 22 | repo repository.ApiRepository 23 | } 24 | 25 | func NewApiService(l *zap.Logger, repo repository.ApiRepository) ApiService { 26 | return &apiService{ 27 | l: l, 28 | repo: repo, 29 | } 30 | } 31 | 32 | // CreateApi 创建新的API 33 | func (a *apiService) CreateApi(ctx context.Context, api *domain.Api) error { 34 | if api == nil { 35 | a.l.Warn("API不能为空") 36 | return errors.New("api不能为空") 37 | } 38 | 39 | return a.repo.CreateApi(ctx, api) 40 | } 41 | 42 | // GetApiById 根据ID获取API 43 | func (a *apiService) GetApiById(ctx context.Context, id int) (*domain.Api, error) { 44 | if id <= 0 { 45 | a.l.Warn("API ID无效", zap.Int("ID", id)) 46 | return nil, errors.New("api id无效") 47 | } 48 | 49 | return a.repo.GetApiById(ctx, id) 50 | } 51 | 52 | // UpdateApi 更新API信息 53 | func (a *apiService) UpdateApi(ctx context.Context, api *domain.Api) error { 54 | if api == nil { 55 | a.l.Warn("API不能为空") 56 | return errors.New("api不能为空") 57 | } 58 | 59 | return a.repo.UpdateApi(ctx, api) 60 | } 61 | 62 | // DeleteApi 删除指定ID的API 63 | func (a *apiService) DeleteApi(ctx context.Context, id int) error { 64 | if id <= 0 { 65 | a.l.Warn("API ID无效", zap.Int("ID", id)) 66 | return errors.New("api id无效") 67 | } 68 | 69 | return a.repo.DeleteApi(ctx, id) 70 | } 71 | 72 | // ListApis 分页获取API列表 73 | func (a *apiService) ListApis(ctx context.Context, page, pageSize int) ([]*domain.Api, int, error) { 74 | if page < 1 || pageSize < 1 { 75 | a.l.Warn("分页参数无效", zap.Int("页码", page), zap.Int("每页数量", pageSize)) 76 | return nil, 0, errors.New("分页参数无效") 77 | } 78 | 79 | return a.repo.ListApis(ctx, page, pageSize) 80 | } 81 | -------------------------------------------------------------------------------- /internal/service/history.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/domain" 7 | "github.com/GoSimplicity/LinkMe/internal/repository" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type HistoryService interface { 12 | GetHistory(ctx context.Context, pagination domain.Pagination) ([]domain.History, error) 13 | DeleteOneHistory(ctx context.Context, postId uint, uid int64) error 14 | DeleteAllHistory(ctx context.Context, uid int64) error 15 | } 16 | 17 | type historyService struct { 18 | repo repository.HistoryRepository 19 | l *zap.Logger 20 | } 21 | 22 | func NewHistoryService(repo repository.HistoryRepository, l *zap.Logger) HistoryService { 23 | return &historyService{ 24 | repo: repo, 25 | l: l, 26 | } 27 | } 28 | 29 | // GetHistory 获取历史记录 30 | func (h *historyService) GetHistory(ctx context.Context, pagination domain.Pagination) ([]domain.History, error) { 31 | // 计算偏移量 32 | offset := int64(pagination.Page-1) * *pagination.Size 33 | pagination.Offset = &offset 34 | 35 | history, err := h.repo.GetHistory(ctx, pagination) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return history, nil 41 | } 42 | 43 | // DeleteOneHistory 删除一条历史记录 44 | func (h *historyService) DeleteOneHistory(ctx context.Context, postId uint, uid int64) error { 45 | if err := h.repo.DeleteOneHistory(ctx, postId, uid); err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // DeleteAllHistory 删除所有历史记录 53 | func (h *historyService) DeleteAllHistory(ctx context.Context, uid int64) error { 54 | if err := h.repo.DeleteAllHistory(ctx, uid); err != nil { 55 | return err 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/service/im.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | /** 4 | * @Author: Bamboo 5 | * @Author: 13664854532@163.com 6 | * @Date: 2024/9/9 18:27 7 | * @Desc: 8 | */ 9 | -------------------------------------------------------------------------------- /internal/service/menu.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/GoSimplicity/LinkMe/internal/domain" 8 | "github.com/GoSimplicity/LinkMe/internal/repository" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type MenuService interface { 13 | GetMenus(ctx context.Context, pageNum, pageSize int, isTree bool) ([]*domain.Menu, int, error) 14 | CreateMenu(ctx context.Context, menu *domain.Menu) error 15 | GetMenuById(ctx context.Context, id int) (*domain.Menu, error) 16 | UpdateMenu(ctx context.Context, menu *domain.Menu) error 17 | DeleteMenu(ctx context.Context, id int) error 18 | GetMenuTree(ctx context.Context) ([]*domain.Menu, error) 19 | } 20 | 21 | type menuService struct { 22 | l *zap.Logger 23 | repo repository.MenuRepository 24 | } 25 | 26 | func NewMenuService(l *zap.Logger, repo repository.MenuRepository) MenuService { 27 | return &menuService{ 28 | l: l, 29 | repo: repo, 30 | } 31 | } 32 | 33 | // GetMenus 获取菜单列表,支持分页和树形结构 34 | func (m *menuService) GetMenus(ctx context.Context, pageNum, pageSize int, isTree bool) ([]*domain.Menu, int, error) { 35 | if pageNum < 1 || pageSize < 1 { 36 | m.l.Warn("分页参数无效", zap.Int("页码", pageNum), zap.Int("每页数量", pageSize)) 37 | return nil, 0, errors.New("分页参数无效") 38 | } 39 | 40 | // 如果需要树形结构,则调用GetMenuTree 41 | if isTree { 42 | menus, err := m.repo.GetMenuTree(ctx) 43 | if err != nil { 44 | m.l.Error("获取菜单树失败", zap.Error(err)) 45 | return nil, 0, err 46 | } 47 | return menus, len(menus), nil 48 | } 49 | 50 | return m.repo.ListMenus(ctx, pageNum, pageSize) 51 | } 52 | 53 | // CreateMenu 创建新菜单 54 | func (m *menuService) CreateMenu(ctx context.Context, menu *domain.Menu) error { 55 | if menu == nil { 56 | m.l.Warn("菜单不能为空") 57 | return errors.New("菜单不能为空") 58 | } 59 | 60 | return m.repo.CreateMenu(ctx, menu) 61 | } 62 | 63 | // GetMenuById 根据ID获取菜单 64 | func (m *menuService) GetMenuById(ctx context.Context, id int) (*domain.Menu, error) { 65 | if id <= 0 { 66 | m.l.Warn("菜单ID无效", zap.Int("ID", id)) 67 | return nil, errors.New("菜单ID无效") 68 | } 69 | 70 | return m.repo.GetMenuById(ctx, id) 71 | } 72 | 73 | // UpdateMenu 更新菜单信息 74 | func (m *menuService) UpdateMenu(ctx context.Context, menu *domain.Menu) error { 75 | if menu == nil { 76 | m.l.Warn("菜单不能为空") 77 | return errors.New("菜单不能为空") 78 | } 79 | 80 | return m.repo.UpdateMenu(ctx, menu) 81 | } 82 | 83 | // DeleteMenu 删除指定ID的菜单 84 | func (m *menuService) DeleteMenu(ctx context.Context, id int) error { 85 | if id <= 0 { 86 | m.l.Warn("菜单ID无效", zap.Int("ID", id)) 87 | return errors.New("菜单ID无效") 88 | } 89 | 90 | return m.repo.DeleteMenu(ctx, id) 91 | } 92 | 93 | // GetMenuTree 获取菜单树形结构 94 | func (m *menuService) GetMenuTree(ctx context.Context) ([]*domain.Menu, error) { 95 | return m.repo.GetMenuTree(ctx) 96 | } 97 | -------------------------------------------------------------------------------- /internal/service/mocks/sms.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/service/sms.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | 6 | // 7 | // mockgen -source=internal/service/sms.go -destination=internal/service/mocks/sms.mock.go -package=mocks 8 | // 9 | package mocks 10 | 11 | import ( 12 | context "context" 13 | reflect "reflect" 14 | 15 | gomock "github.com/golang/mock/gomock" 16 | ) 17 | 18 | // MockSmsService is a mock of SmsService interface. 19 | type MockSmsService struct { 20 | ctrl *gomock.Controller 21 | recorder *MockSmsServiceMockRecorder 22 | } 23 | 24 | // MockSmsServiceMockRecorder is the mock recorder for MockSmsService. 25 | type MockSmsServiceMockRecorder struct { 26 | mock *MockSmsService 27 | } 28 | 29 | // NewMockSmsService creates a new mock instance. 30 | func NewMockSmsService(ctrl *gomock.Controller) *MockSmsService { 31 | mock := &MockSmsService{ctrl: ctrl} 32 | mock.recorder = &MockSmsServiceMockRecorder{mock} 33 | return mock 34 | } 35 | 36 | // EXPECT returns an object that allows the caller to indicate expected use. 37 | func (m *MockSmsService) EXPECT() *MockSmsServiceMockRecorder { 38 | return m.recorder 39 | } 40 | 41 | // CheckCode mocks base method. 42 | func (m *MockSmsService) CheckCode(ctx context.Context, smsID, mobile, vCode string) (bool, error) { 43 | m.ctrl.T.Helper() 44 | ret := m.ctrl.Call(m, "CheckCode", ctx, smsID, mobile, vCode) 45 | ret0, _ := ret[0].(bool) 46 | ret1, _ := ret[1].(error) 47 | return ret0, ret1 48 | } 49 | 50 | // CheckCode indicates an expected call of CheckCode. 51 | func (mr *MockSmsServiceMockRecorder) CheckCode(ctx, smsID, mobile, vCode interface{}) *gomock.Call { 52 | mr.mock.ctrl.T.Helper() 53 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckCode", reflect.TypeOf((*MockSmsService)(nil).CheckCode), ctx, smsID, mobile, vCode) 54 | } 55 | 56 | // SendCode mocks base method. 57 | func (m *MockSmsService) SendCode(ctx context.Context, number string) error { 58 | m.ctrl.T.Helper() 59 | ret := m.ctrl.Call(m, "SendCode", ctx, number) 60 | ret0, _ := ret[0].(error) 61 | return ret0 62 | } 63 | 64 | // SendCode indicates an expected call of SendCode. 65 | func (mr *MockSmsServiceMockRecorder) SendCode(ctx, number interface{}) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendCode", reflect.TypeOf((*MockSmsService)(nil).SendCode), ctx, number) 68 | } 69 | -------------------------------------------------------------------------------- /internal/service/mocks/user.mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: service/user.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=internal/service/user.go -destination=internal/service/mocks/user.mock.go -package=mocks 7 | // 8 | 9 | // Package mocks is a generated GoMock package. 10 | package mocks 11 | 12 | import ( 13 | domain "github.com/GoSimplicity/LinkMe/internal/domain" 14 | context "context" 15 | reflect "reflect" 16 | 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockUserService is a mock of UserService interface. 21 | type MockUserService struct { 22 | ctrl *gomock.Controller 23 | recorder *MockUserServiceMockRecorder 24 | } 25 | 26 | func (m *MockUserService) ChangePassword(ctx context.Context, email string, password string, newPassword string, confirmPassword string) error { 27 | //TODO implement me 28 | panic("implement me") 29 | } 30 | 31 | func (m *MockUserService) DeleteUser(ctx context.Context, email string, password string, uid int64) error { 32 | //TODO implement me 33 | panic("implement me") 34 | } 35 | 36 | func (m *MockUserService) UpdateProfile(ctx context.Context, profile domain.Profile) (err error) { 37 | //TODO implement me 38 | panic("implement me") 39 | } 40 | 41 | func (m *MockUserService) GetProfileByUserID(ctx context.Context, UserID int64) (profile domain.Profile, err error) { 42 | //TODO implement me 43 | panic("implement me") 44 | } 45 | 46 | // MockUserServiceMockRecorder is the mock recorder for MockUserService. 47 | type MockUserServiceMockRecorder struct { 48 | mock *MockUserService 49 | } 50 | 51 | // NewMockUserService creates a new mock instance. 52 | func NewMockUserService(ctrl *gomock.Controller) *MockUserService { 53 | mock := &MockUserService{ctrl: ctrl} 54 | mock.recorder = &MockUserServiceMockRecorder{mock} 55 | return mock 56 | } 57 | 58 | // EXPECT returns an object that allows the caller to indicate expected use. 59 | func (m *MockUserService) EXPECT() *MockUserServiceMockRecorder { 60 | return m.recorder 61 | } 62 | 63 | // Login mocks base method. 64 | func (m *MockUserService) Login(ctx context.Context, email, password string) (domain.User, error) { 65 | m.ctrl.T.Helper() 66 | ret := m.ctrl.Call(m, "Login", ctx, email, password) 67 | ret0, _ := ret[0].(domain.User) 68 | ret1, _ := ret[1].(error) 69 | return ret0, ret1 70 | } 71 | 72 | // Login indicates an expected call of Login. 73 | func (mr *MockUserServiceMockRecorder) Login(ctx, email, password any) *gomock.Call { 74 | mr.mock.ctrl.T.Helper() 75 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockUserService)(nil).Login), ctx, email, password) 76 | } 77 | 78 | // SignUp mocks base method. 79 | func (m *MockUserService) SignUp(ctx context.Context, u domain.User) error { 80 | m.ctrl.T.Helper() 81 | ret := m.ctrl.Call(m, "SignUp", ctx, u) 82 | ret0, _ := ret[0].(error) 83 | return ret0 84 | } 85 | 86 | // SignUp indicates an expected call of SignUp. 87 | func (mr *MockUserServiceMockRecorder) SignUp(ctx, u any) *gomock.Call { 88 | mr.mock.ctrl.T.Helper() 89 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignUp", reflect.TypeOf((*MockUserService)(nil).SignUp), ctx, u) 90 | } 91 | -------------------------------------------------------------------------------- /internal/service/permission.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type PermissionService interface { 11 | AssignRole(ctx context.Context, roleId int, menuIds []int, apiIds []int) error 12 | AssignRoleToUser(ctx context.Context, userId int, roleIds []int, menuIds []int, apiIds []int) error 13 | AssignRoleToUsers(ctx context.Context, userIds []int, roleIds []int, menuIds []int, apiIds []int) error 14 | } 15 | 16 | type permissionService struct { 17 | l *zap.Logger 18 | repo repository.PermissionRepository 19 | } 20 | 21 | func NewPermissionService(l *zap.Logger, repo repository.PermissionRepository) PermissionService { 22 | return &permissionService{ 23 | l: l, 24 | repo: repo, 25 | } 26 | } 27 | 28 | // TODO: 下述接口遗留问题 29 | // 1.每次角色分配权限,都会先移除旧权限,再重新分配新权限,这样会导致角色被赋予新权限后,用户没有旧权限,导致无法访问 30 | // 解决办法: 在前端处理,打开前端的角色分配权限页面,先获取当前角色的权限,再进行分配,这样不会出现上述问题 31 | // 2.每次移除旧的权限都是对数据库的一次写操作,但是每次分配新权限,都会对数据库进行一次读操作,这样会导致性能问题,需要优化 32 | // 3.由于casbin_rule表id主键为自增的,所以每次写入都会导致id自增,导致性能问题,需要优化 33 | 34 | // AssignRole 为角色分配权限 35 | func (p *permissionService) AssignRole(ctx context.Context, roleId int, menuIds []int, apiIds []int) error { 36 | // 参数校验 37 | if roleId <= 0 { 38 | p.l.Warn("角色ID无效", zap.Int("roleId", roleId)) 39 | return nil 40 | } 41 | 42 | // 先移除旧权限 43 | if err := p.repo.RemoveRolePermissions(ctx, roleId); err != nil { 44 | p.l.Error("移除角色API权限失败", zap.Error(err)) 45 | return err 46 | } 47 | 48 | // 分配新权限 49 | return p.repo.AssignRole(ctx, roleId, menuIds, apiIds) 50 | } 51 | 52 | // AssignRoleToUser 为用户分配角色和权限 53 | func (p *permissionService) AssignRoleToUser(ctx context.Context, userId int, roleIds []int, menuIds []int, apiIds []int) error { 54 | // 参数校验 55 | if userId <= 0 { 56 | p.l.Warn("用户ID无效", zap.Int("userId", userId)) 57 | return nil 58 | } 59 | 60 | // 先移除旧角色和权限 61 | if err := p.repo.RemoveUserPermissions(ctx, userId); err != nil { 62 | p.l.Error("移除用户角色失败", zap.Error(err)) 63 | return err 64 | } 65 | 66 | // 分配新角色和权限 67 | return p.repo.AssignRoleToUser(ctx, userId, roleIds, menuIds, apiIds) 68 | } 69 | 70 | // AssignRoleToUsers 为多个用户批量分配角色和权限 71 | func (p *permissionService) AssignRoleToUsers(ctx context.Context, userIds []int, roleIds []int, menuIds []int, apiIds []int) error { 72 | // 参数校验 73 | if len(userIds) == 0 { 74 | p.l.Warn("用户ID列表不能为空") 75 | return nil 76 | } 77 | 78 | // 先移除旧角色和权限 79 | if err := p.repo.RemoveUsersPermissions(ctx, userIds); err != nil { 80 | p.l.Error("移除用户角色失败", zap.Error(err)) 81 | return err 82 | } 83 | 84 | // 批量分配新角色和权限 85 | return p.repo.AssignRoleToUsers(ctx, userIds, roleIds, menuIds, apiIds) 86 | } 87 | -------------------------------------------------------------------------------- /internal/service/plate.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type PlateService interface { 11 | CreatePlate(ctx context.Context, plate domain.Plate) error 12 | ListPlate(ctx context.Context, pagination domain.Pagination) ([]domain.Plate, error) 13 | UpdatePlate(ctx context.Context, plate domain.Plate) error 14 | DeletePlate(ctx context.Context, plateId int64, uid int64) error 15 | } 16 | 17 | type plateService struct { 18 | l *zap.Logger 19 | repo repository.PlateRepository 20 | } 21 | 22 | func NewPlateService(l *zap.Logger, repo repository.PlateRepository) PlateService { 23 | return &plateService{ 24 | l: l, 25 | repo: repo, 26 | } 27 | } 28 | 29 | func (p *plateService) CreatePlate(ctx context.Context, plate domain.Plate) error { 30 | return p.repo.CreatePlate(ctx, plate) 31 | } 32 | 33 | func (p *plateService) ListPlate(ctx context.Context, pagination domain.Pagination) ([]domain.Plate, error) { 34 | offset := int64(pagination.Page-1) * *pagination.Size 35 | pagination.Offset = &offset 36 | plates, err := p.repo.ListPlate(ctx, pagination) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return plates, err 41 | } 42 | 43 | func (p *plateService) UpdatePlate(ctx context.Context, plate domain.Plate) error { 44 | return p.repo.UpdatePlate(ctx, plate) 45 | } 46 | 47 | func (p *plateService) DeletePlate(ctx context.Context, plateId int64, uid int64) error { 48 | return p.repo.DeletePlate(ctx, plateId, uid) 49 | } 50 | -------------------------------------------------------------------------------- /internal/service/relation.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | ) 8 | 9 | type RelationService interface { 10 | ListFollowerRelations(ctx context.Context, followerID int64, pagination domain.Pagination) ([]domain.Relation, error) 11 | ListFolloweeRelations(ctx context.Context, followeeID int64, pagination domain.Pagination) ([]domain.Relation, error) 12 | FollowUser(ctx context.Context, followerID, followeeID int64) error 13 | CancelFollowUser(ctx context.Context, followerID, followeeID int64) error 14 | GetFolloweeCount(ctx context.Context, UserID int64) (int64, error) 15 | GetFollowerCount(ctx context.Context, UserID int64) (int64, error) 16 | } 17 | 18 | type relationService struct { 19 | repo repository.RelationRepository 20 | } 21 | 22 | func NewRelationService(repo repository.RelationRepository) RelationService { 23 | return &relationService{ 24 | repo: repo, 25 | } 26 | } 27 | 28 | // ListFollowerRelations 列出所有关注关系 29 | func (r *relationService) ListFollowerRelations(ctx context.Context, followerID int64, pagination domain.Pagination) ([]domain.Relation, error) { 30 | // 计算偏移量 31 | offset := int64(pagination.Page-1) * *pagination.Size 32 | pagination.Offset = &offset 33 | return r.repo.ListFollowerRelations(ctx, followerID, pagination) 34 | } 35 | 36 | // ListFolloweeRelations 获取特定的关注关系信息 37 | func (r *relationService) ListFolloweeRelations(ctx context.Context, followeeID int64, pagination domain.Pagination) ([]domain.Relation, error) { 38 | // 计算偏移量 39 | offset := int64(pagination.Page-1) * *pagination.Size 40 | pagination.Offset = &offset 41 | return r.repo.ListFolloweeRelations(ctx, followeeID, pagination) 42 | } 43 | 44 | // FollowUser 关注用户 45 | func (r *relationService) FollowUser(ctx context.Context, followerID, followeeID int64) error { 46 | return r.repo.FollowUser(ctx, followerID, followeeID) 47 | } 48 | 49 | // CancelFollowUser 取消关注用户 50 | func (r *relationService) CancelFollowUser(ctx context.Context, followerID, followeeID int64) error { 51 | return r.repo.CancelFollowUser(ctx, followerID, followeeID) 52 | } 53 | 54 | func (r *relationService) GetFolloweeCount(ctx context.Context, UserID int64) (int64, error) { 55 | return r.repo.GetFolloweeCount(ctx, UserID) 56 | } 57 | 58 | func (r *relationService) GetFollowerCount(ctx context.Context, UserID int64) (int64, error) { 59 | return r.repo.GetFollowerCount(ctx, UserID) 60 | } 61 | -------------------------------------------------------------------------------- /internal/service/search.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "golang.org/x/sync/errgroup" 8 | "strings" 9 | ) 10 | 11 | type searchService struct { 12 | repo repository.SearchRepository 13 | } 14 | 15 | type SearchService interface { 16 | SearchPosts(ctx context.Context, expression string) ([]domain.PostSearch, error) 17 | SearchUsers(ctx context.Context, expression string) ([]domain.UserSearch, error) 18 | SearchComments(ctx context.Context, expression string) ([]domain.CommentSearch, error) 19 | } 20 | 21 | func NewSearchService(repo repository.SearchRepository) SearchService { 22 | return &searchService{ 23 | repo: repo, 24 | } 25 | } 26 | func (s *searchService) SearchComments(ctx context.Context, expression string) ([]domain.CommentSearch, error) { 27 | // 将表达式拆分为关键字数组 28 | keywords := strings.Split(expression, " ") 29 | var eg errgroup.Group 30 | var comments []domain.CommentSearch 31 | eg.Go(func() error { 32 | // 搜索评论 33 | foundComments, err := s.repo.SearchComments(ctx, keywords) 34 | if err != nil { 35 | return err 36 | } 37 | comments = foundComments 38 | return nil 39 | }) 40 | // 等待所有并发任务完成 41 | if err := eg.Wait(); err != nil { 42 | return nil, err 43 | } 44 | return comments, nil 45 | } 46 | func (s *searchService) SearchPosts(ctx context.Context, expression string) ([]domain.PostSearch, error) { 47 | // 将表达式拆分为关键字数组 48 | keywords := strings.Split(expression, " ") 49 | 50 | var eg errgroup.Group 51 | var posts []domain.PostSearch 52 | 53 | eg.Go(func() error { 54 | // 搜索帖子 55 | foundPosts, err := s.repo.SearchPosts(ctx, keywords) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | posts = foundPosts 61 | 62 | return nil 63 | }) 64 | 65 | // 等待所有并发任务完成 66 | if err := eg.Wait(); err != nil { 67 | return nil, err 68 | } 69 | 70 | return posts, nil 71 | } 72 | 73 | func (s *searchService) SearchUsers(ctx context.Context, expression string) ([]domain.UserSearch, error) { 74 | // 将表达式拆分为关键字数组 75 | keywords := strings.Split(expression, " ") 76 | 77 | var eg errgroup.Group 78 | var users []domain.UserSearch 79 | 80 | eg.Go(func() error { 81 | // 搜索用户 82 | foundUsers, err := s.repo.SearchUsers(ctx, keywords) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | users = foundUsers 88 | 89 | return nil 90 | }) 91 | 92 | if err := eg.Wait(); err != nil { 93 | return nil, err 94 | } 95 | 96 | return users, nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/service/sql_job.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "github.com/GoSimplicity/LinkMe/internal/domain" 6 | "github.com/GoSimplicity/LinkMe/internal/repository" 7 | "go.uber.org/zap" 8 | "time" 9 | ) 10 | 11 | type CronJobService interface { 12 | Preempt(ctx context.Context) (domain.Job, error) 13 | ResetNextTime(ctx context.Context, dj domain.Job) error 14 | } 15 | 16 | type cronJobService struct { 17 | repo repository.CronJobRepository 18 | l *zap.Logger 19 | refreshInterval time.Duration 20 | } 21 | 22 | func NewCronJobService(repo repository.CronJobRepository, l *zap.Logger) CronJobService { 23 | return &cronJobService{ 24 | repo: repo, 25 | l: l, 26 | refreshInterval: time.Minute, 27 | } 28 | } 29 | 30 | // Preempt 获取一个任务,并启动一个协程定期刷新任务的更新时间 31 | func (c *cronJobService) Preempt(ctx context.Context) (domain.Job, error) { 32 | j, err := c.repo.Preempt(ctx) 33 | if err != nil { 34 | return domain.Job{}, err 35 | } 36 | 37 | ticker := time.NewTicker(c.refreshInterval) 38 | go func() { 39 | for { 40 | select { 41 | case <-ticker.C: 42 | c.refresh(j.Id) 43 | case <-ctx.Done(): 44 | ticker.Stop() 45 | return 46 | } 47 | } 48 | }() 49 | j.CancelFunc = func() { 50 | ticker.Stop() 51 | ct, cancel := context.WithTimeout(context.Background(), time.Second) 52 | defer cancel() 53 | er := c.repo.Release(ct, j.Id) 54 | if er != nil { 55 | c.l.Error("Failed to release job", zap.Error(er)) 56 | } 57 | } 58 | return j, nil 59 | } 60 | 61 | // ResetNextTime 重置任务的下次执行时间 62 | func (c *cronJobService) ResetNextTime(ctx context.Context, dj domain.Job) error { 63 | nextTime, err := dj.NextTime() 64 | if err != nil { 65 | return err 66 | } 67 | return c.repo.UpdateNextTime(ctx, dj.Id, nextTime) 68 | } 69 | 70 | // refresh 更新任务的更新时间 71 | func (c *cronJobService) refresh(id int64) { 72 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 73 | defer cancel() 74 | err := c.repo.UpdateTime(ctx, id) 75 | if err != nil { 76 | c.l.Error("Failed to refresh job", zap.Error(err)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ioc/asynq.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/job/interfaces" 5 | "github.com/GoSimplicity/LinkMe/internal/service" 6 | "github.com/hibiken/asynq" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | /* 11 | * Copyright 2024 Bamboo 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | * 25 | * File: asynq.go 26 | * Description: 27 | */ 28 | 29 | func InitAsynqClient() *asynq.Client { 30 | return asynq.NewClient(asynq.RedisClientOpt{ 31 | Addr: viper.GetString("redis.addr"), 32 | Password: viper.GetString("redis.password"), 33 | }) 34 | } 35 | 36 | func InitAsynqServer() *asynq.Server { 37 | return asynq.NewServer( 38 | asynq.RedisClientOpt{ 39 | Addr: viper.GetString("redis.addr"), 40 | Password: viper.GetString("redis.password"), 41 | }, 42 | asynq.Config{ 43 | Concurrency: 10, // 设置并发数 44 | }, 45 | ) 46 | } 47 | 48 | func InitScheduler() *asynq.Scheduler { 49 | return asynq.NewScheduler(asynq.RedisClientOpt{ 50 | Addr: viper.GetString("redis.addr"), 51 | Password: viper.GetString("redis.password"), 52 | }, nil) 53 | } 54 | 55 | func InitRankingService(svc service.RankingService) interfaces.RankingService { 56 | return svc 57 | } 58 | -------------------------------------------------------------------------------- /ioc/casbin.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/casbin/casbin/v2" 5 | gormadapter "github.com/casbin/gorm-adapter/v3" 6 | "gorm.io/gorm" 7 | "log" 8 | ) 9 | 10 | // InitCasbin 初始化casbin权限管理器 11 | func InitCasbin(db *gorm.DB) *casbin.Enforcer { 12 | // 创建gorm适配器,用于将权限规则存储到数据库中 13 | adapter, err := gormadapter.NewAdapterByDB(db) 14 | if err != nil { 15 | log.Fatalf("创建适配器失败: %v", err) 16 | } 17 | 18 | // 创建enforcer实例,使用配置文件中的模型定义和数据库适配器 19 | enforcer, err := casbin.NewEnforcer("config/model.conf", adapter) 20 | if err != nil { 21 | log.Fatalf("创建enforcer失败: %v", err) 22 | } 23 | return enforcer 24 | } 25 | -------------------------------------------------------------------------------- /ioc/cmd.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/domain/events" 5 | "github.com/GoSimplicity/LinkMe/internal/job" 6 | "github.com/gin-gonic/gin" 7 | "github.com/hibiken/asynq" 8 | ) 9 | 10 | type Cmd struct { 11 | Server *gin.Engine 12 | Consumer []events.Consumer 13 | Routes *job.Routes 14 | Asynq *asynq.Server 15 | Scheduler *job.TimedScheduler 16 | } 17 | -------------------------------------------------------------------------------- /ioc/db.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/GoSimplicity/LinkMe/internal/repository/dao" 6 | prometheus3 "github.com/GoSimplicity/LinkMe/pkg/gormp/prometheus" 7 | prometheus2 "github.com/prometheus/client_golang/prometheus" 8 | "github.com/spf13/viper" 9 | "gorm.io/driver/mysql" 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/logger" 12 | "gorm.io/plugin/prometheus" 13 | "log" 14 | ) 15 | 16 | type config struct { 17 | DSN string `yaml:"dsn"` 18 | } 19 | 20 | // InitDB 初始化数据库 21 | func InitDB() *gorm.DB { 22 | var c config 23 | 24 | if err := viper.UnmarshalKey("db", &c); err != nil { 25 | panic(fmt.Errorf("init failed:%v", err)) 26 | } 27 | db, err := gorm.Open(mysql.Open(c.DSN), &gorm.Config{ 28 | Logger: logger.Default.LogMode(logger.Info), 29 | }) 30 | 31 | if err != nil { 32 | panic(err) 33 | } 34 | // 初始化表 35 | 36 | if err = dao.InitTables(db); err != nil { 37 | panic(err) 38 | } 39 | 40 | // 注册 Prometheus 插件 41 | if err = db.Use(prometheus.New(prometheus.Config{ 42 | DBName: "linkme", // Prometheus中标识数据库的名称 43 | RefreshInterval: 5, // 监控数据刷新间隔,单位为秒 44 | })); err != nil { 45 | log.Println("register prometheus plugin failed") 46 | } 47 | 48 | // 创建并注册自定义的 PrometheusCallbacks 插件,用于监控gorm操作执行时间 49 | prometheusPlugin := prometheus3.NewPrometheusCallbacks(prometheus2.SummaryOpts{ 50 | Namespace: "linkme", // 命名空间 51 | Subsystem: "gorm", // 子系统 52 | Name: "operation_duration_seconds", // 指标名称 53 | Help: "Duration of GORM database operations in seconds", // 指标帮助信息 54 | Objectives: map[float64]float64{ 55 | 0.5: 0.01, 56 | 0.75: 0.01, 57 | 0.9: 0.01, 58 | 0.99: 0.001, 59 | 0.999: 0.0001, 60 | }, 61 | }) 62 | 63 | if err = db.Use(prometheusPlugin); err != nil { 64 | log.Println("register custom prometheus callbacks plugin failed:", err) 65 | } 66 | 67 | return db 68 | } 69 | -------------------------------------------------------------------------------- /ioc/es.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/elastic/go-elasticsearch/v8" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | // InitES 初始化elasticsearch 9 | func InitES() *elasticsearch.TypedClient { 10 | addr := viper.GetString("es.addr") 11 | cfg := elasticsearch.Config{ 12 | Addresses: []string{ 13 | addr, 14 | }, 15 | } 16 | client, err := elasticsearch.NewTypedClient(cfg) 17 | if err != nil { 18 | panic(err) 19 | } 20 | return client 21 | } 22 | -------------------------------------------------------------------------------- /ioc/kafka.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/domain/events" 5 | "github.com/GoSimplicity/LinkMe/internal/domain/events/check" 6 | "github.com/GoSimplicity/LinkMe/internal/domain/events/comment" 7 | "github.com/GoSimplicity/LinkMe/internal/domain/events/email" 8 | "github.com/GoSimplicity/LinkMe/internal/domain/events/es" 9 | "github.com/GoSimplicity/LinkMe/internal/domain/events/post" 10 | "github.com/GoSimplicity/LinkMe/internal/domain/events/publish" 11 | "github.com/GoSimplicity/LinkMe/internal/domain/events/sms" 12 | "github.com/GoSimplicity/LinkMe/pkg/samarap/prometheus" 13 | "github.com/IBM/sarama" 14 | prometheus2 "github.com/prometheus/client_golang/prometheus" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | // InitSaramaClient 初始化Sarama客户端,用于连接到Kafka集群 19 | func InitSaramaClient() sarama.Client { 20 | type Config struct { 21 | Addr []string `yaml:"addr"` 22 | } 23 | 24 | var cfg Config 25 | 26 | err := viper.UnmarshalKey("kafka", &cfg) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | scfg := sarama.NewConfig() 32 | // 配置生产者需要返回确认成功的消息 33 | scfg.Producer.Return.Successes = true 34 | 35 | client, err := sarama.NewClient(cfg.Addr, scfg) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | return client 41 | } 42 | 43 | // InitSyncProducer 使用已有的Sarama客户端初始化同步生产者 44 | func InitSyncProducer(c sarama.Client) sarama.SyncProducer { 45 | // 根据现有的客户端实例创建同步生产者 46 | p, err := sarama.NewSyncProducerFromClient(c) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | // 创建并注册自定义的 KafkaMetricsHook 插件 52 | kafkaMetricsHook := prometheus.NewKafkaMetricsHook(prometheus2.SummaryOpts{ 53 | Namespace: "linkme", 54 | Subsystem: "kafka", 55 | Name: "operation_duration_seconds", 56 | Help: "Duration of Kafka operations in seconds", 57 | Objectives: map[float64]float64{ 58 | 0.5: 0.01, 59 | 0.75: 0.01, 60 | 0.9: 0.01, 61 | 0.99: 0.001, 62 | }, 63 | }) 64 | 65 | // 包装生产者 66 | return kafkaMetricsHook.WrapProducer(p) 67 | } 68 | 69 | // InitConsumers 初始化并返回一个事件消费者 70 | func InitConsumers( 71 | postConsumer *post.EventConsumer, 72 | smsConsumer *sms.SMSConsumer, 73 | commentConsumer *comment.PublishCommentEventConsumer, 74 | emailConsumer *email.EmailConsumer, 75 | publishConsumer *publish.PublishPostEventConsumer, 76 | esConsumer *es.EsConsumer, 77 | checkConsumer *check.CheckEventConsumer, 78 | postDLQConsumer *post.PostDeadLetterConsumer, 79 | publishDLQConsumer *publish.PublishDeadLetterConsumer, 80 | checkDLQConsumer *check.CheckDeadLetterConsumer, 81 | ) []events.Consumer { 82 | // 返回消费者切片 83 | return []events.Consumer{ 84 | postConsumer, 85 | smsConsumer, 86 | commentConsumer, 87 | emailConsumer, 88 | publishConsumer, 89 | esConsumer, 90 | checkConsumer, 91 | postDLQConsumer, 92 | publishDLQConsumer, 93 | checkDLQConsumer, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /ioc/liniter.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | . "github.com/GoSimplicity/LinkMe/pkg/limiterp" 5 | "time" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | func InitLimiter(redis redis.Cmdable) Limiter { 11 | return NewRedisSlidingWindowLimiter(redis, time.Second, 100) 12 | } 13 | -------------------------------------------------------------------------------- /ioc/logger.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "time" 7 | 8 | "github.com/spf13/viper" 9 | "go.uber.org/zap" 10 | "go.uber.org/zap/zapcore" 11 | "gopkg.in/natefinch/lumberjack.v2" 12 | ) 13 | 14 | func InitLogger() *zap.Logger { 15 | // 从配置获取日志目录 16 | logDir := viper.GetString("log.dir") 17 | logFile := filepath.Join(logDir, "linkme-"+time.Now().Format("2006-01-02")+"-json.log") 18 | 19 | // 日志轮转配置 20 | fileWriter := &lumberjack.Logger{ 21 | Filename: logFile, 22 | MaxSize: 10, // 每个日志文件最大10MB 23 | MaxBackups: 5, // 保留最近5个日志文件 24 | MaxAge: 30, // 日志文件最多保留30天 25 | Compress: true, // 压缩旧日志 26 | LocalTime: true, // 使用本地时间 27 | } 28 | 29 | // 多路输出配置 30 | writeSyncer := zapcore.NewMultiWriteSyncer( 31 | zapcore.AddSync(os.Stdout), 32 | zapcore.AddSync(fileWriter), 33 | ) 34 | 35 | // 编码器配置 36 | encoderConfig := zap.NewProductionEncoderConfig() 37 | encoderConfig.TimeKey = "time" 38 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 39 | encoderConfig.EncodeLevel = zapcore.LowercaseLevelEncoder 40 | encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 41 | 42 | // 创建核心配置 43 | core := zapcore.NewCore( 44 | zapcore.NewJSONEncoder(encoderConfig), 45 | writeSyncer, 46 | zap.NewAtomicLevelAt(zapcore.InfoLevel), 47 | ) 48 | 49 | // 创建并返回logger 50 | return zap.New( 51 | core, 52 | zap.AddCaller(), 53 | zap.AddStacktrace(zapcore.ErrorLevel), 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /ioc/middleware.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/GoSimplicity/LinkMe/middleware" 8 | "github.com/GoSimplicity/LinkMe/pkg/ginp/prometheus" 9 | ijwt "github.com/GoSimplicity/LinkMe/utils/jwt" 10 | "github.com/gin-contrib/cors" 11 | "github.com/gin-gonic/gin" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | // InitMiddlewares 初始化中间件 16 | func InitMiddlewares(ih ijwt.Handler, l *zap.Logger) []gin.HandlerFunc { 17 | prom := &prometheus.MetricsPlugin{ 18 | Namespace: "linkme", 19 | Subsystem: "api", 20 | InstanceID: "instance_1", 21 | } 22 | 23 | // 注册指标 24 | prom.RegisterMetrics() 25 | 26 | return []gin.HandlerFunc{ 27 | cors.New(cors.Config{ 28 | AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}, 29 | AllowCredentials: true, // 允许携带凭证 30 | AllowHeaders: []string{"Content-Type", "Authorization", "X-Refresh-Token"}, 31 | ExposeHeaders: []string{"x-jwt-token", "x-refresh-token"}, 32 | AllowOriginFunc: func(origin string) bool { 33 | if strings.HasPrefix(origin, "http://localhost") { 34 | return true 35 | } 36 | return strings.Contains(origin, "") 37 | }, 38 | MaxAge: 12 * time.Hour, 39 | }), 40 | // 统计响应时间 41 | prom.TrackActiveRequestsMiddleware(), 42 | // 统计活跃请求数 43 | prom.TrackResponseTimeMiddleware(), 44 | middleware.NewJWTMiddleware(ih).CheckLogin(), 45 | middleware.NewLogMiddleware(l).Log(), 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ioc/mongo.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "context" 5 | "github.com/spf13/viper" 6 | "go.mongodb.org/mongo-driver/mongo" 7 | "go.mongodb.org/mongo-driver/mongo/options" 8 | ) 9 | 10 | func InitMongoDB() *mongo.Client { 11 | addr := viper.GetString("mongodb.addr") 12 | client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(addr)) 13 | if err != nil { 14 | panic(err) 15 | } 16 | return client 17 | } 18 | -------------------------------------------------------------------------------- /ioc/redis.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | prometheus2 "github.com/GoSimplicity/LinkMe/pkg/cachep/prometheus" // 替换为实际路径 5 | prometheus "github.com/prometheus/client_golang/prometheus" 6 | "github.com/redis/go-redis/v9" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func InitRedis() redis.Cmdable { 11 | // 初始化 Redis 客户端 12 | client := redis.NewClient(&redis.Options{ 13 | Addr: viper.GetString("redis.addr"), 14 | Password: viper.GetString("redis.password"), 15 | }) 16 | 17 | // 创建并注册自定义的 RedisMetricsHook 插件 18 | prometheusHook := prometheus2.NewRedisMetricsHook(prometheus.SummaryOpts{ 19 | Namespace: "linkme", 20 | Subsystem: "redis", 21 | Name: "operation_duration_seconds", 22 | Help: "Duration of Redis operations in seconds", 23 | Objectives: map[float64]float64{ 24 | 0.5: 0.01, 25 | 0.75: 0.01, 26 | 0.9: 0.01, 27 | 0.99: 0.001, 28 | }, 29 | }) 30 | 31 | // 添加 Hook 插件到 Redis 客户端 32 | client.AddHook(prometheusHook) 33 | 34 | return client 35 | } 36 | -------------------------------------------------------------------------------- /ioc/sms.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/pkg/sms" 5 | "github.com/spf13/viper" 6 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 7 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" 8 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions" 9 | tencentsms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" 10 | ) 11 | 12 | // newClient 创建腾讯云短信客户端 13 | func newClient() *tencentsms.Client { 14 | secretId := viper.GetString("sms.tencent.secretId") 15 | secretKey := viper.GetString("sms.tencent.secretKey") 16 | endPoint := viper.GetString("sms.tencent.endPoint") 17 | credential := common.NewCredential( 18 | secretId, 19 | secretKey, 20 | ) 21 | // 创建客户端配置 22 | cpf := profile.NewClientProfile() 23 | // 设置腾讯云短信服务端点 24 | cpf.HttpProfile.Endpoint = endPoint 25 | // 创建腾讯云短信客户端 26 | client, _ := tencentsms.NewClient(credential, regions.Nanjing, cpf) 27 | return client 28 | } 29 | 30 | // InitSms 初始化腾讯云短信实例 31 | func InitSms() *sms.TencentSms { 32 | smsID := viper.GetString("sms.tencent.smsID") 33 | sign := viper.GetString("sms.tencent.sign") 34 | templateID := viper.GetString("sms.tencent.templateID") 35 | // 创建腾讯云短信实例 36 | return sms.NewTencentSms(newClient(), smsID, sign, templateID) 37 | } 38 | -------------------------------------------------------------------------------- /ioc/snowflake.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | sf "github.com/bwmarrin/snowflake" 5 | "time" 6 | ) 7 | 8 | // InitializeSnowflakeNode 初始化雪花节点 9 | func InitializeSnowflakeNode() *sf.Node { 10 | st := getTime() 11 | sf.Epoch = st.UnixNano() / 1000000 // 计算起始时间戳并赋值给sf.Epoch 12 | node, err := sf.NewNode(1) // 创建新的节点 13 | if err != nil { 14 | return nil 15 | } 16 | return node 17 | } 18 | 19 | func getTime() time.Time { 20 | year := 2024 21 | month := time.May 22 | day := 13 23 | hour := 10 24 | minute := 30 25 | second := 0 26 | nanosecond := 0 27 | // 加载北京时间 (CST) 时区 28 | location, err := time.LoadLocation("Asia/Shanghai") 29 | if err != nil { 30 | panic(err) 31 | } 32 | return time.Date(year, month, day, hour, minute, second, nanosecond, location) 33 | } 34 | -------------------------------------------------------------------------------- /ioc/web.go: -------------------------------------------------------------------------------- 1 | package ioc 2 | 3 | import ( 4 | "github.com/GoSimplicity/LinkMe/internal/api" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // InitWeb 初始化web服务 9 | func InitWeb(userHdl *api.UserHandler, 10 | postHdl *api.PostHandler, 11 | historyHdl *api.HistoryHandler, 12 | checkHdl *api.CheckHandler, 13 | m []gin.HandlerFunc, 14 | permHdl *api.PermissionHandler, 15 | rankingHdl *api.RankingHandler, 16 | plateHdl *api.PlateHandler, 17 | activityHdl *api.ActivityHandler, 18 | commentHdl *api.CommentHandler, 19 | searchHdl *api.SearchHandler, 20 | relationHdl *api.RelationHandler, 21 | lotteryDrawHdl *api.LotteryDrawHandler, 22 | roleHdl *api.RoleHandler, 23 | menuHdl *api.MenuHandler, 24 | apiHdl *api.ApiHandler, 25 | ) *gin.Engine { 26 | server := gin.Default() 27 | server.Use(m...) 28 | userHdl.RegisterRoutes(server) 29 | postHdl.RegisterRoutes(server) 30 | historyHdl.RegisterRoutes(server) 31 | checkHdl.RegisterRoutes(server) 32 | permHdl.RegisterRoutes(server) 33 | rankingHdl.RegisterRoutes(server) 34 | plateHdl.RegisterRoutes(server) 35 | activityHdl.RegisterRoutes(server) 36 | commentHdl.RegisterRoutes(server) 37 | searchHdl.RegisterRoutes(server) 38 | relationHdl.RegisterRoutes(server) 39 | lotteryDrawHdl.RegisterRoutes(server) 40 | roleHdl.RegisterRoutes(server) 41 | menuHdl.RegisterRoutes(server) 42 | apiHdl.RegisterRoutes(server) 43 | return server 44 | } 45 | -------------------------------------------------------------------------------- /middleware/log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "github.com/gin-gonic/gin" 6 | "go.uber.org/zap" 7 | "io/ioutil" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type AccessLog struct { 13 | Path string `json:"path"` // 请求路径 14 | Method string `json:"method"` // 请求方法 15 | ReqBody string `json:"reqBody"` // 请求体内容 16 | Status int `json:"status"` // 响应状态码 17 | RespBody string `json:"respBody"` // 响应体内容 18 | Duration time.Duration `json:"duration"` // 请求处理耗时 19 | } 20 | 21 | type LogMiddleware struct { 22 | l *zap.Logger 23 | } 24 | 25 | func NewLogMiddleware(l *zap.Logger) *LogMiddleware { 26 | return &LogMiddleware{ 27 | l: l, 28 | } 29 | } 30 | 31 | // Log 日志中间件 32 | func (lm *LogMiddleware) Log() gin.HandlerFunc { 33 | return func(c *gin.Context) { 34 | // 开始时间 35 | start := time.Now() 36 | // 请求路径 37 | path := c.Request.URL.Path 38 | // 请求方法 39 | method := c.Request.Method 40 | // 读取请求体 41 | bodyBytes, err := ioutil.ReadAll(c.Request.Body) 42 | if err != nil { 43 | lm.l.Error("请求体读取失败", zap.Error(err)) 44 | c.AbortWithStatus(http.StatusInternalServerError) 45 | return 46 | } 47 | // 由于读取请求体会消耗掉c.Request.Body,所以需要重新设置回上下文 48 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 49 | al := AccessLog{ 50 | Path: path, 51 | Method: method, 52 | ReqBody: string(bodyBytes), 53 | } 54 | c.Next() 55 | // 记录响应状态码和响应体 56 | al.Status = c.Writer.Status() 57 | al.RespBody = c.Writer.Header().Get("Content-Type") 58 | al.Duration = time.Since(start) 59 | lm.l.Info("请求日志", zap.Any("accessLog", al)) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /middleware/login.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | . "github.com/GoSimplicity/LinkMe/internal/constants" 5 | ijwt "github.com/GoSimplicity/LinkMe/utils/jwt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/golang-jwt/jwt/v5" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | type JWTMiddleware struct { 12 | ijwt.Handler 13 | } 14 | 15 | func NewJWTMiddleware(hdl ijwt.Handler) *JWTMiddleware { 16 | return &JWTMiddleware{ 17 | Handler: hdl, 18 | } 19 | } 20 | 21 | // CheckLogin 校验JWT 22 | func (m *JWTMiddleware) CheckLogin() gin.HandlerFunc { 23 | return func(ctx *gin.Context) { 24 | path := ctx.Request.URL.Path 25 | // 如果请求的路径是下述路径,则不进行token验证 26 | if path == "/api/user/signup" || 27 | path == "/api/user/login" || 28 | path == "/api/user/refresh_token" || 29 | path == "/api/user/change_password" || 30 | path == "/api/user/send_sms" || 31 | path == "/api/user/send_email" { 32 | return 33 | } 34 | // 从请求中提取token 35 | tokenStr := m.ExtractToken(ctx) 36 | var uc ijwt.UserClaims 37 | token, err := jwt.ParseWithClaims(tokenStr, &uc, func(token *jwt.Token) (interface{}, error) { 38 | return []byte(viper.GetString("jwt.auth_key")), nil 39 | }) 40 | if err != nil { 41 | // token 错误 42 | ctx.AbortWithStatus(RequestsERROR) 43 | return 44 | } 45 | if token == nil || !token.Valid { 46 | // token 非法或过期 47 | ctx.AbortWithStatus(RequestsERROR) 48 | return 49 | } 50 | // 检查是否携带ua头 51 | if uc.UserAgent == "" { 52 | ctx.AbortWithStatus(RequestsERROR) 53 | return 54 | } 55 | // 检查会话是否有效 56 | err = m.CheckSession(ctx, uc.Ssid) 57 | if err != nil { 58 | ctx.AbortWithStatus(RequestsERROR) 59 | return 60 | } 61 | ctx.Set("user", uc) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /middleware/role.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | ijwt "github.com/GoSimplicity/LinkMe/utils/jwt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/casbin/casbin/v2" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type CasbinMiddleware struct { 13 | enforcer *casbin.Enforcer 14 | } 15 | 16 | func NewCasbinMiddleware(enforcer *casbin.Enforcer) *CasbinMiddleware { 17 | return &CasbinMiddleware{ 18 | enforcer: enforcer, 19 | } 20 | } 21 | 22 | // CheckCasbin 创建一个 Casbin 中间件 23 | func (cm *CasbinMiddleware) CheckCasbin() gin.HandlerFunc { 24 | return func(c *gin.Context) { 25 | // 获取用户身份 26 | userClaims, exists := c.Get("user") 27 | if !exists { 28 | c.JSON(http.StatusUnauthorized, gin.H{"message": "User not authenticated"}) 29 | c.Abort() 30 | return 31 | } 32 | sub, ok := userClaims.(ijwt.UserClaims) 33 | if !ok { 34 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid user claims"}) 35 | c.Abort() 36 | return 37 | } 38 | if sub.Uid == 0 { 39 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid user ID"}) 40 | c.Abort() 41 | return 42 | } 43 | // 将用户ID转换为字符串 44 | userIDStr := strconv.FormatInt(sub.Uid, 10) 45 | // 获取请求的 URL 和请求方法 46 | obj := c.Request.URL.Path 47 | act := c.Request.Method 48 | // 使用 Casbin 检查权限 49 | ok, err := cm.enforcer.Enforce(userIDStr, obj, act) 50 | if err != nil { 51 | c.JSON(http.StatusInternalServerError, gin.H{"message": "Error occurred when enforcing policy"}) 52 | c.Abort() 53 | return 54 | } 55 | if !ok { 56 | c.JSON(http.StatusForbidden, gin.H{"message": "You don't have permission to access this resource"}) 57 | c.Abort() 58 | return 59 | } 60 | // 继续处理请求 61 | c.Next() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | #linkme 2 | **/*.go { 3 | prep: go build -o tmp/main -v cmd/main.go 4 | daemon +sigkill: ./tmp/main 5 | } 6 | -------------------------------------------------------------------------------- /pkg/cachep/local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "time" 8 | 9 | "github.com/patrickmn/go-cache" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | type CacheManager struct { 14 | localCache *cache.Cache // 本地缓存 15 | redisClient redis.Cmdable 16 | } 17 | 18 | func NewLocalCacheManager(redisClient redis.Cmdable) *CacheManager { 19 | return &CacheManager{ 20 | localCache: cache.New(5*time.Minute, 10*time.Minute), // 默认缓存 5 分钟 21 | redisClient: redisClient, 22 | } 23 | } 24 | 25 | // Set 缓存数据到本地缓存和 Redis 26 | func (cm *CacheManager) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { 27 | data, err := json.Marshal(value) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | // 并发设置本地缓存和Redis 33 | errChan := make(chan error, 2) 34 | 35 | go func() { 36 | cm.localCache.Set(key, value, expiration) 37 | errChan <- nil 38 | }() 39 | 40 | go func() { 41 | errChan <- cm.redisClient.Set(ctx, key, data, expiration).Err() 42 | }() 43 | 44 | // 等待两个操作完成 45 | for i := 0; i < 2; i++ { 46 | if err := <-errChan; err != nil { 47 | return err 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // Get 从缓存中获取数据,如果缓存未命中,则调用 loader 加载数据并缓存 55 | func (cm *CacheManager) Get(ctx context.Context, key string, loader func() (interface{}, error), result interface{}) error { 56 | // 尝试从本地缓存获取 57 | if value, found := cm.localCache.Get(key); found { 58 | return cm.unmarshalValue(value, result) 59 | } 60 | 61 | // 尝试从 Redis 获取 62 | data, err := cm.redisClient.Get(ctx, key).Bytes() 63 | if err == nil { 64 | if err := json.Unmarshal(data, result); err != nil { 65 | return err 66 | } 67 | // 同步到本地缓存 68 | cm.localCache.Set(key, result, cache.DefaultExpiration) 69 | return nil 70 | } 71 | 72 | if !errors.Is(err, redis.Nil) { 73 | return err 74 | } 75 | 76 | // 缓存未命中,从 loader 加载数据 77 | value, err := loader() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // 缓存加载的数据 83 | if err := cm.Set(ctx, key, value, cache.DefaultExpiration); err != nil { 84 | return err 85 | } 86 | 87 | return cm.unmarshalValue(value, result) 88 | } 89 | 90 | // Delete 从本地缓存和 Redis 中删除一个或多个键 91 | func (cm *CacheManager) Delete(ctx context.Context, keys ...string) error { 92 | // 并发删除本地缓存和Redis 93 | errChan := make(chan error, 2) 94 | 95 | go func() { 96 | for _, key := range keys { 97 | cm.localCache.Delete(key) 98 | } 99 | errChan <- nil 100 | }() 101 | 102 | go func() { 103 | errChan <- cm.redisClient.Del(ctx, keys...).Err() 104 | }() 105 | 106 | // 等待两个操作完成 107 | for i := 0; i < 2; i++ { 108 | if err := <-errChan; err != nil { 109 | return err 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | // SetEmptyCache 缓存空对象,防止缓存穿透 117 | func (cm *CacheManager) SetEmptyCache(ctx context.Context, key string, ttl time.Duration) error { 118 | emptyValue := struct{}{} 119 | return cm.Set(ctx, key, emptyValue, ttl) 120 | } 121 | 122 | // unmarshalValue 将缓存值反序列化为指定类型 123 | func (cm *CacheManager) unmarshalValue(value interface{}, result interface{}) error { 124 | data, err := json.Marshal(value) 125 | if err != nil { 126 | return err 127 | } 128 | return json.Unmarshal(data, result) 129 | } 130 | -------------------------------------------------------------------------------- /pkg/cachep/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "time" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | // RedisMetricsHook 实现了 redis.Hook 接口,用于监控 Redis 操作 13 | type RedisMetricsHook struct { 14 | operationMetrics *prometheus.SummaryVec 15 | } 16 | 17 | // NewRedisMetricsHook 初始化 RedisMetricsHook 实例,并注册 Prometheus 指标 18 | func NewRedisMetricsHook(opts prometheus.SummaryOpts) *RedisMetricsHook { 19 | operationMetrics := prometheus.NewSummaryVec(opts, []string{"operation"}) 20 | prometheus.MustRegister(operationMetrics) 21 | return &RedisMetricsHook{ 22 | operationMetrics: operationMetrics, 23 | } 24 | } 25 | 26 | // ProcessHook 实现了 redis.ProcessHook,用于监控单个命令的执行时间 27 | func (h *RedisMetricsHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 28 | return func(ctx context.Context, cmd redis.Cmder) error { 29 | // 在命令执行之前记录开始时间 30 | startTime := time.Now() 31 | 32 | // 调用下一个钩子或最终的命令执行 33 | err := next(ctx, cmd) 34 | 35 | // 计算命令执行的持续时间,并记录到 Prometheus 36 | duration := time.Since(startTime).Seconds() 37 | h.operationMetrics.WithLabelValues(cmd.Name()).Observe(duration) 38 | 39 | return err 40 | } 41 | } 42 | 43 | // DialHook 实现了 redis.DialHook,用于监控连接的执行时间 44 | func (h *RedisMetricsHook) DialHook(next redis.DialHook) redis.DialHook { 45 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 46 | // 在连接操作之前记录开始时间 47 | startTime := time.Now() 48 | 49 | // 调用下一个钩子或实际的连接操作 50 | conn, err := next(ctx, network, addr) 51 | 52 | // 计算连接操作的持续时间,并记录到 Prometheus 53 | duration := time.Since(startTime).Seconds() 54 | h.operationMetrics.WithLabelValues("dial").Observe(duration) 55 | 56 | return conn, err 57 | } 58 | } 59 | 60 | // ProcessPipelineHook 实现了 redis.ProcessPipelineHook,用于监控 Pipeline 操作的执行时间 61 | func (h *RedisMetricsHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 62 | return func(ctx context.Context, cmds []redis.Cmder) error { 63 | // 在 Pipeline 执行之前记录开始时间 64 | startTime := time.Now() 65 | 66 | // 调用下一个钩子或实际的 Pipeline 操作 67 | err := next(ctx, cmds) 68 | 69 | // 计算 Pipeline 操作的持续时间,并记录到 Prometheus 70 | duration := time.Since(startTime).Seconds() 71 | h.operationMetrics.WithLabelValues("pipeline").Observe(duration) 72 | 73 | return err 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/canalp/message.go: -------------------------------------------------------------------------------- 1 | package canalp 2 | 3 | type Message[T any] struct { 4 | Data []T `json:"data"` 5 | Database string `json:"database"` 6 | Table string `json:"table"` 7 | Type string `json:"type"` 8 | } 9 | -------------------------------------------------------------------------------- /pkg/email/qq.go: -------------------------------------------------------------------------------- 1 | package qqEmail 2 | 3 | import ( 4 | "gopkg.in/gomail.v2" 5 | ) 6 | 7 | func SendEmail(to string, subject string, body string) (err error) { 8 | // 创建邮件消息 9 | m := gomail.NewMessage() 10 | m.SetHeader("From", "xxx@qq.com") 11 | m.SetHeader("To", to) 12 | m.SetHeader("Subject", subject) 13 | m.SetBody("text/plain", body) 14 | 15 | // 使用QQ邮箱SMTP服务器 16 | d := gomail.NewDialer("smtp.qq.com", 587, "xxx@qq.com", "xxx") 17 | 18 | // 发送邮件 19 | return d.DialAndSend(m) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/email/qqEmail_test.go: -------------------------------------------------------------------------------- 1 | package qqEmail 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/GoSimplicity/LinkMe/utils" 7 | ) 8 | 9 | func TestQQEmail(t *testing.T) { 10 | type args struct { 11 | to string 12 | subject string 13 | body string 14 | } 15 | tests := []struct { 16 | name string 17 | args args 18 | wantErr bool 19 | }{ 20 | { 21 | name: "test-1", 22 | args: args{ 23 | to: "yansaitao@qq.com", 24 | subject: "test-1", 25 | body: "验证码为" + utils.GenRandomCode(6), 26 | }, 27 | wantErr: false, 28 | }, 29 | { 30 | name: "test-2", 31 | args: args{ 32 | to: "yansaitao@gmail.com", 33 | body: "验证码为" + utils.GenRandomCode(6), 34 | }, 35 | wantErr: false, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | if err := SendEmail(tt.args.to, tt.args.subject, tt.args.body); (err != nil) != tt.wantErr { 42 | t.Errorf("sendEmail() error = %v, wantErr %v", err, tt.wantErr) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/general/general.go: -------------------------------------------------------------------------------- 1 | package general 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | func WithAsyncCancel(ctx context.Context, cancel context.CancelFunc, fn func() error) func() { 8 | return func() { 9 | go func() { 10 | // 监听 context 取消信号 11 | done := make(chan struct{}) 12 | defer close(done) 13 | 14 | go func() { 15 | select { 16 | case <-ctx.Done(): 17 | cancel() 18 | case <-done: 19 | return 20 | } 21 | }() 22 | 23 | // 确保 goroutine 中的 panic 不会导致程序崩溃 24 | defer func() { 25 | if r := recover(); r != nil { 26 | cancel() // 发生 panic 时取消操作 27 | } 28 | }() 29 | 30 | // 执行目标函数 31 | if err := fn(); err != nil { 32 | cancel() // 发生错误时取消操作 33 | } 34 | }() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/ginp/result.go: -------------------------------------------------------------------------------- 1 | package ginp 2 | 3 | type Result struct { 4 | Code int `json:"code"` 5 | Msg string `json:"msg"` 6 | Data interface{} `json:"data"` 7 | } 8 | 9 | type TokenResult struct { 10 | Code int `json:"code"` 11 | Msg string `json:"msg"` 12 | JWTToken string `json:"jwt_token"` 13 | RefreshToken string `json:"refresh_token"` 14 | } 15 | -------------------------------------------------------------------------------- /pkg/ginp/wrapper.go: -------------------------------------------------------------------------------- 1 | package ginp 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | // WrapBody 是一个中间件,用于包裹业务逻辑函数,自动绑定请求体、处理响应并集中管理错误处理。 10 | func WrapBody[Req any](bizFn func(ctx *gin.Context, req Req) (Result, error)) gin.HandlerFunc { 11 | return func(ctx *gin.Context) { 12 | var req Req 13 | // 使用 ShouldBindJSON 替换 Bind,以便于更好地处理错误,避免直接 panic 14 | if err := ctx.ShouldBindJSON(&req); err != nil { 15 | // 当请求体解析失败时,返回适当的HTTP错误响应而非 panic 16 | ctx.AbortWithStatusJSON(http.StatusBadRequest, Result{ 17 | Msg: "无效的请求负载", 18 | }) 19 | return 20 | } 21 | 22 | res, err := bizFn(ctx, req) 23 | if err != nil { 24 | // 记录错误(在生产环境中建议使用结构化日志) 25 | log.Printf("执行业务逻辑时发生错误: %v", err) 26 | // 根据应用需求自定义错误响应 27 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误"}) 28 | return 29 | } 30 | 31 | // 成功处理,返回结果 32 | ctx.JSON(http.StatusOK, res) 33 | } 34 | } 35 | 36 | func WrapParam[Req any](bizFn func(ctx *gin.Context, req Req) (Result, error)) gin.HandlerFunc { 37 | return func(ctx *gin.Context) { 38 | var req Req 39 | // 使用 ShouldBindJSON 替换 Bind,以便于更好地处理错误,避免直接 panic 40 | if err := ctx.ShouldBindUri(&req); err != nil { 41 | // 当请求体解析失败时,返回适当的HTTP错误响应而非 panic 42 | ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "无效的请求负载"}) 43 | return 44 | } 45 | 46 | res, err := bizFn(ctx, req) 47 | if err != nil { 48 | // 记录错误(在生产环境中建议使用结构化日志) 49 | log.Printf("执行业务逻辑时发生错误: %v", err) 50 | // 根据应用需求自定义错误响应 51 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err}) 52 | return 53 | } 54 | // 成功处理,返回结果 55 | ctx.JSON(http.StatusOK, res) 56 | } 57 | } 58 | 59 | func WrapQuery[Req any](bizFn func(ctx *gin.Context, req Req) (Result, error)) gin.HandlerFunc { 60 | return func(ctx *gin.Context) { 61 | var req Req 62 | // 使用 ShouldBindQuery 绑定查询参数 63 | if err := ctx.ShouldBindQuery(&req); err != nil { 64 | // 当请求参数解析失败时,返回适当的HTTP错误响应而非 panic 65 | ctx.AbortWithStatusJSON(http.StatusBadRequest, Result{ 66 | Msg: "无效的请求参数", 67 | }) 68 | return 69 | } 70 | res, err := bizFn(ctx, req) 71 | if err != nil { 72 | // 记录错误(在生产环境中建议使用结构化日志) 73 | log.Printf("执行业务逻辑时发生错误: %v", err) 74 | // 根据应用需求自定义错误响应 75 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, Result{ 76 | Msg: "服务器内部错误", 77 | }) 78 | return 79 | } 80 | // 成功处理,返回结果 81 | ctx.JSON(http.StatusOK, res) 82 | } 83 | } 84 | 85 | // WrapNoParam 是一个中间件,用于包裹不需要请求参数的业务逻辑函数,处理响应并集中管理错误处理。 86 | func WrapNoParam(bizFn func(ctx *gin.Context) (Result, error)) gin.HandlerFunc { 87 | return func(ctx *gin.Context) { 88 | res, err := bizFn(ctx) 89 | if err != nil { 90 | // 记录错误(在生产环境中建议使用结构化日志) 91 | log.Printf("执行业务逻辑时发生错误: %v", err) 92 | // 根据应用需求自定义错误响应 93 | ctx.AbortWithStatusJSON(http.StatusInternalServerError, Result{ 94 | Msg: "服务器内部错误", 95 | }) 96 | return 97 | } 98 | // 成功处理,返回结果 99 | ctx.JSON(http.StatusOK, res) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pkg/limiterp/limit.go: -------------------------------------------------------------------------------- 1 | package limiterp 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | //go:embed limit_slide_window.lua 12 | var luaScript string 13 | 14 | type Limiter interface { 15 | // Limit true为触发,false为不触发 16 | Limit(ctx context.Context, key string) (bool, error) 17 | } 18 | 19 | type limiter struct { 20 | cmd redis.Cmdable 21 | // 时间窗口间隔 22 | interval time.Duration 23 | // 阈值 24 | rate int 25 | } 26 | 27 | func NewRedisSlidingWindowLimiter(cmd redis.Cmdable, interval time.Duration, rate int) Limiter { 28 | return &limiter{ 29 | cmd: cmd, 30 | interval: interval, 31 | rate: rate, 32 | } 33 | } 34 | 35 | func (l *limiter) Limit(ctx context.Context, key string) (bool, error) { 36 | return l.cmd.Eval(ctx, luaScript, []string{key}, 37 | l.interval.Milliseconds(), l.rate, time.Now().UnixMilli()).Bool() 38 | } 39 | -------------------------------------------------------------------------------- /pkg/limiterp/limit_slide_window.lua: -------------------------------------------------------------------------------- 1 | -- 限流对象 2 | local key = KEYS[1] 3 | -- 窗口大小 4 | local window = tonumber(ARGV[1]) 5 | -- 阈值 6 | local threshold = tonumber( ARGV[2]) 7 | local now = tonumber(ARGV[3]) 8 | -- 窗口的起始时间 9 | local min = now - window 10 | -- 移除窗口中过期的请求 11 | redis.call('ZREMRANGEBYSCORE', key, '-inf', min) 12 | -- 获取窗口中的请求数量 13 | local cnt = redis.call('ZCOUNT', key, '-inf', '+inf') 14 | if cnt >= threshold then 15 | return "true" 16 | else 17 | redis.call('ZADD', key, now, now) 18 | redis.call('PEXPIRE', key, window) 19 | return "false" 20 | end -------------------------------------------------------------------------------- /pkg/priorityqueue/priority_queue.go: -------------------------------------------------------------------------------- 1 | package priorityqueue 2 | 3 | import ( 4 | "container/heap" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrOutOfCapacity = errors.New("队列已满") 10 | ErrEmptyQueue = errors.New("队列为空") 11 | ) 12 | 13 | type PriorityQueue[T any] struct { 14 | items []T 15 | capacity int 16 | less func(a, b T) bool 17 | } 18 | 19 | func NewPriorityQueue[T any](capacity int, less func(a, b T) bool) *PriorityQueue[T] { 20 | if capacity <= 0 { 21 | capacity = 1 22 | } 23 | pq := &PriorityQueue[T]{ 24 | items: make([]T, 0, capacity), 25 | capacity: capacity, 26 | less: less, 27 | } 28 | heap.Init(pq) 29 | return pq 30 | } 31 | 32 | // Len 返回队列的长度 33 | func (pq *PriorityQueue[T]) Len() int { 34 | return len(pq.items) 35 | } 36 | 37 | // Less 比较两个元素的大小 38 | func (pq *PriorityQueue[T]) Less(i, j int) bool { 39 | if pq.less == nil { 40 | return false 41 | } 42 | return pq.less(pq.items[i], pq.items[j]) 43 | } 44 | 45 | // Swap 交换两个元素 46 | func (pq *PriorityQueue[T]) Swap(i, j int) { 47 | if i >= 0 && i < len(pq.items) && j >= 0 && j < len(pq.items) { 48 | pq.items[i], pq.items[j] = pq.items[j], pq.items[i] 49 | } 50 | } 51 | 52 | // Push 添加一个元素 53 | func (pq *PriorityQueue[T]) Push(x interface{}) { 54 | if item, ok := x.(T); ok { 55 | pq.items = append(pq.items, item) 56 | } 57 | } 58 | 59 | // Pop 移除并返回最小元素 60 | func (pq *PriorityQueue[T]) Pop() interface{} { 61 | if len(pq.items) == 0 { 62 | return nil 63 | } 64 | old := pq.items 65 | n := len(old) 66 | item := old[n-1] 67 | pq.items = old[0 : n-1] 68 | return item 69 | } 70 | 71 | // Enqueue 添加一个元素 72 | func (pq *PriorityQueue[T]) Enqueue(item T) error { 73 | if pq.Len() >= pq.capacity { 74 | return ErrOutOfCapacity 75 | } 76 | heap.Push(pq, item) 77 | return nil 78 | } 79 | 80 | // Dequeue 移除并返回最小元素 81 | func (pq *PriorityQueue[T]) Dequeue() (T, error) { 82 | if pq.Len() == 0 { 83 | var zero T 84 | return zero, ErrEmptyQueue 85 | } 86 | result := heap.Pop(pq) 87 | if result == nil { 88 | var zero T 89 | return zero, ErrEmptyQueue 90 | } 91 | return result.(T), nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/samarap/handler.go: -------------------------------------------------------------------------------- 1 | package samarap 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/IBM/sarama" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | // Handler 是一个通用的消息处理器 10 | type Handler[T any] struct { 11 | logger *zap.Logger 12 | handle func(msg *sarama.ConsumerMessage, event T) error 13 | } 14 | 15 | // NewHandler 创建一个新的 Handler 实例 16 | func NewHandler[T any](logger *zap.Logger, handle func(msg *sarama.ConsumerMessage, event T) error) *Handler[T] { 17 | return &Handler[T]{logger: logger, handle: handle} 18 | } 19 | 20 | // Setup 在消费组会话开始时调用 21 | func (h *Handler[T]) Setup(session sarama.ConsumerGroupSession) error { 22 | h.logger.Info("Consumer group session setup") 23 | return nil 24 | } 25 | 26 | // Cleanup 在消费组会话结束时调用 27 | func (h *Handler[T]) Cleanup(session sarama.ConsumerGroupSession) error { 28 | h.logger.Info("Consumer group session cleanup") 29 | return nil 30 | } 31 | 32 | // ConsumeClaim 处理消费组的消息 33 | func (h *Handler[T]) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { 34 | for msg := range claim.Messages() { 35 | var event T 36 | if err := json.Unmarshal(msg.Value, &event); err != nil { 37 | h.logger.Error("Failed to unmarshal message", zap.Error(err), zap.ByteString("value", msg.Value)) 38 | continue // 跳过无法反序列化的消息 39 | } 40 | if err := h.handle(msg, event); err != nil { 41 | h.logger.Error("Failed to process message", zap.Error(err), zap.ByteString("key", msg.Key), zap.ByteString("value", msg.Value)) 42 | // 你可以在这里引入重试逻辑,根据具体需求 43 | } else { 44 | session.MarkMessage(msg, "") 45 | } 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/samarap/handler_batch.go: -------------------------------------------------------------------------------- 1 | package samarap 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/IBM/sarama" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | type BatchHandler[T any] struct { 13 | fn func(msgs []*sarama.ConsumerMessage, ts []T) error 14 | l *zap.Logger 15 | } 16 | 17 | func NewBatchHandler[T any](l *zap.Logger, fn func(msgs []*sarama.ConsumerMessage, ts []T) error) *BatchHandler[T] { 18 | return &BatchHandler[T]{fn: fn, l: l} 19 | } 20 | 21 | func (b *BatchHandler[T]) Setup(session sarama.ConsumerGroupSession) error { 22 | return nil 23 | } 24 | 25 | func (b *BatchHandler[T]) Cleanup(session sarama.ConsumerGroupSession) error { 26 | return nil 27 | } 28 | 29 | func (b *BatchHandler[T]) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { 30 | msgs := claim.Messages() // 获取分配给当前消费者的消息通道 31 | const batchSize = 100 // 定义批处理的大小 32 | const timeout = time.Second * 10 33 | for { 34 | batch := make([]*sarama.ConsumerMessage, 0, batchSize) 35 | ts := make([]T, 0, batchSize) 36 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 37 | var done bool 38 | for i := 0; i < batchSize && !done; i++ { // 循环直到批次大小达到或者上下文超时 39 | select { 40 | case <-ctx.Done(): // 是否超时 41 | done = true // 标记为完成 42 | case msg, ok := <-msgs: // 从消息通道中接收消息 43 | if !ok { 44 | cancel() 45 | return nil 46 | } 47 | var t T 48 | err := json.Unmarshal(msg.Value, &t) 49 | if err != nil { 50 | b.l.Error("反序列消息体失败", zap.Error(err)) 51 | continue // 跳过当前消息,继续处理下一个 52 | } 53 | batch = append(batch, msg) 54 | ts = append(ts, t) 55 | } 56 | } 57 | cancel() 58 | if len(batch) == 0 { // 如果批次为空,跳过处理 59 | continue 60 | } 61 | err := b.fn(batch, ts) // 调用处理函数处理批次中的消息 62 | if err != nil { // 如果处理失败 63 | b.l.Error("处理消息失败", zap.Error(err)) 64 | continue 65 | } 66 | for _, msg := range batch { 67 | session.MarkMessage(msg, "") // 标记消息为已处理 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /pkg/samarap/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/prometheus/client_golang/prometheus" 6 | "time" 7 | ) 8 | 9 | type KafkaMetricsHook struct { 10 | operationMetrics *prometheus.SummaryVec 11 | } 12 | 13 | // NewKafkaMetricsHook 初始化 KafkaMetricsHook 实例,并注册 Prometheus 指标 14 | func NewKafkaMetricsHook(opts prometheus.SummaryOpts) *KafkaMetricsHook { 15 | operationMetrics := prometheus.NewSummaryVec(opts, []string{"operation", "topic"}) 16 | prometheus.MustRegister(operationMetrics) 17 | return &KafkaMetricsHook{ 18 | operationMetrics: operationMetrics, 19 | } 20 | } 21 | 22 | // WrapProducer 为 Sarama 同步生产者包装一个带有指标记录的生产者 23 | func (h *KafkaMetricsHook) WrapProducer(producer sarama.SyncProducer) sarama.SyncProducer { 24 | return &instrumentedProducer{ 25 | SyncProducer: producer, 26 | operationMetrics: h.operationMetrics, 27 | } 28 | } 29 | 30 | // instrumentedProducer 是一个包装了 Sarama SyncProducer 的结构体 31 | type instrumentedProducer struct { 32 | sarama.SyncProducer 33 | operationMetrics *prometheus.SummaryVec 34 | } 35 | 36 | // SendMessage 包装后的 SendMessage 方法,用于监控消息发送时间 37 | func (p *instrumentedProducer) SendMessage(msg *sarama.ProducerMessage) (partition int32, offset int64, err error) { 38 | startTime := time.Now() 39 | partition, offset, err = p.SyncProducer.SendMessage(msg) 40 | duration := time.Since(startTime).Seconds() 41 | p.operationMetrics.WithLabelValues("send", msg.Topic).Observe(duration) 42 | return partition, offset, err 43 | } 44 | -------------------------------------------------------------------------------- /pkg/slicetools/slice_tools.go: -------------------------------------------------------------------------------- 1 | package slicetools 2 | 3 | func Map[T any, U any](slice []T, mapper func(int, T) U) []U { 4 | result := make([]U, len(slice)) 5 | for i, v := range slice { 6 | result[i] = mapper(i, v) 7 | } 8 | return result 9 | } 10 | -------------------------------------------------------------------------------- /pkg/sms/tencentSms.go: -------------------------------------------------------------------------------- 1 | package sms 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 7 | tencent "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" 8 | ) 9 | 10 | // TencentSms 腾讯云SMS 11 | type TencentSms struct { 12 | client *tencent.Client 13 | smsID string 14 | signName string 15 | TemplateID string 16 | } 17 | 18 | func NewTencentSms(client *tencent.Client, smsID string, signName string, TemplateID string) *TencentSms { 19 | return &TencentSms{ 20 | client: client, 21 | smsID: smsID, 22 | signName: signName, 23 | TemplateID: TemplateID, 24 | } 25 | } 26 | 27 | // Send 发送短信 28 | func (s *TencentSms) Send(ctx context.Context, args []string, numbers ...string) (smsID string, driver string, err error) { 29 | request := tencent.NewSendSmsRequest() 30 | request.SetContext(ctx) 31 | request.SmsSdkAppId = common.StringPtr(s.smsID) 32 | request.SignName = common.StringPtr(s.signName) 33 | request.TemplateId = common.StringPtr(s.TemplateID) 34 | 35 | request.TemplateParamSet = common.StringPtrs(args) 36 | request.PhoneNumberSet = common.StringPtrs(numbers) 37 | 38 | response, err := s.client.SendSms(request) 39 | if err != nil { 40 | return s.smsID, "tencent", err 41 | } 42 | for _, statusPtr := range response.Response.SendStatusSet { 43 | if statusPtr == nil { 44 | continue 45 | } 46 | status := *statusPtr 47 | if status.Code == nil || *(status.Code) != "Ok" { 48 | // 发送失败 49 | return s.smsID, "tencent", fmt.Errorf("send sms messages failed,code: %s, msg: %s", *status.Code, *status.Message) 50 | } 51 | } 52 | return s.smsID, "tencent", nil 53 | } 54 | -------------------------------------------------------------------------------- /utils/AiCheck/doubao.go: -------------------------------------------------------------------------------- 1 | package AiCheck 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | "time" 9 | 10 | ark "github.com/sashabaranov/go-openai" 11 | "github.com/sony/gobreaker" 12 | "github.com/spf13/viper" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | var ( 17 | client *ark.Client 18 | clientOnce sync.Once 19 | breaker *gobreaker.CircuitBreaker 20 | ) 21 | 22 | func init() { 23 | breaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{ 24 | Name: "AI_Check", 25 | Timeout: 3 * time.Second, // 超时时间 26 | MaxRequests: 5, // 半开状态下允许的试探请求 27 | Interval: 1 * time.Minute, // 统计窗口间隔 28 | ReadyToTrip: func(counts gobreaker.Counts) bool { 29 | return counts.ConsecutiveFailures > 5 // 连续失败触发熔断 30 | }, 31 | OnStateChange: func(name string, from, to gobreaker.State) { 32 | zap.L().Info("熔断状态变更", zap.String("name", name), zap.Any("from", from), zap.Any("to", to)) 33 | }, 34 | }) 35 | } 36 | 37 | type config struct { 38 | KEY string `yaml:"dsn"` 39 | } 40 | 41 | // 获取单例 client 42 | func getClient() *ark.Client { 43 | clientOnce.Do(func() { 44 | var c config 45 | if err := viper.UnmarshalKey("ark_api", &c); err != nil { 46 | panic(fmt.Errorf("init failed:%v", err)) 47 | } 48 | ARK_API_KEY := c.KEY // 这个AIP是可以修改的 49 | config := ark.DefaultConfig(ARK_API_KEY) 50 | config.BaseURL = "https://ark.cn-beijing.volces.com/api/v3" 51 | client = ark.NewClientWithConfig(config) 52 | fmt.Println("AI 审核 client 初始化完成") 53 | }) 54 | return client 55 | } 56 | 57 | func CheckPostContent(title string, content string) (bool, error) { 58 | result, err := breaker.Execute(func() (interface{}, error) { 59 | client := getClient() 60 | checkLanguage := "你是一个负责评论审核的人工智能,请对输入的内容进行审查,判断其是否包含违规信息。返回结果如下: 如果是 1 说明内容包含违规信息, 如果是 0 说明内容不包含违规信息, 如果是 -1 说明无法判断或其他错误" 61 | checkContents := fmt.Sprintf("标题:%s\n内容:%s", title, content) 62 | resp, err := client.CreateChatCompletion( 63 | context.Background(), 64 | ark.ChatCompletionRequest{ 65 | Model: "ep-20250207162731-kvrzk", 66 | Messages: []ark.ChatCompletionMessage{ 67 | { 68 | Role: ark.ChatMessageRoleSystem, 69 | Content: checkLanguage, 70 | }, 71 | { 72 | Role: ark.ChatMessageRoleUser, 73 | Content: checkContents, 74 | }, 75 | }, 76 | }, 77 | ) 78 | if err != nil { 79 | fmt.Printf("ChatCompletion error: %v\n", err) 80 | return false, errors.New("AI 审核失败") 81 | } 82 | ans := true 83 | if resp.Choices[0].Message.Content == "1" || resp.Choices[0].Message.Content == "-1" { 84 | ans = false 85 | } 86 | return ans, nil 87 | }) 88 | 89 | if err != nil { 90 | return false, err 91 | } 92 | 93 | return result.(bool), nil 94 | } 95 | -------------------------------------------------------------------------------- /utils/AiCheck/doubao_test.go: -------------------------------------------------------------------------------- 1 | package AiCheck 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ark "github.com/sashabaranov/go-openai" 7 | "testing" 8 | ) 9 | 10 | func TestContent(t *testing.T) { 11 | ARK_API_KEY := "" 12 | config := ark.DefaultConfig(ARK_API_KEY) 13 | config.BaseURL = "https://ark.cn-beijing.volces.com/api/v3" 14 | client := ark.NewClientWithConfig(config) 15 | 16 | //fmt.Println("----- standard request -----") 17 | checkLanguage := "你是一个负责评论审核的人工智能,请判断输入内容是否包含违规信息,返回结果1或者0,其中1表示包含,0表示不包含。" 18 | //checkContents := "操你妈hhh" 19 | checkContents := "hhh" 20 | resp, err := client.CreateChatCompletion( 21 | context.Background(), 22 | ark.ChatCompletionRequest{ 23 | Model: "ep-20250207162731-kvrzk", 24 | Messages: []ark.ChatCompletionMessage{ 25 | { 26 | Role: ark.ChatMessageRoleSystem, 27 | Content: checkLanguage, 28 | }, 29 | { 30 | Role: ark.ChatMessageRoleUser, 31 | Content: checkContents, 32 | }, 33 | }, 34 | }, 35 | ) 36 | if err != nil { 37 | fmt.Printf("ChatCompletion error: %v\n", err) 38 | 39 | } 40 | fmt.Println(resp.Choices[0].Message.Content) 41 | } 42 | -------------------------------------------------------------------------------- /utils/contentfilter/sensitive-words.txt: -------------------------------------------------------------------------------- 1 | 赌博 2 | 嫖娼 3 | 吸毒 4 | 开票 5 | -------------------------------------------------------------------------------- /utils/sms.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "regexp" 6 | ) 7 | 8 | // GenRandomCode 生成随机数 9 | func GenRandomCode(length int) string { 10 | letters := []rune("123456789") 11 | code := make([]rune, length) 12 | for i := range code { 13 | code[i] = letters[rand.Intn(len(letters))] 14 | } 15 | return string(code) 16 | } 17 | 18 | // IsValidNumber 检查给定的字符串是否符合中国的手机号格式 19 | func IsValidNumber(number string) bool { 20 | // 中国的手机号通常是以1开头的11位数字 21 | // 这个正则表达式匹配以1开头,第二位是3、4、5、6、7、8、9中的一个,后面跟着9位数字的字符串 22 | pattern := `^1[3456789]\d{9}$` 23 | matched, _ := regexp.MatchString(pattern, number) 24 | return matched 25 | } 26 | -------------------------------------------------------------------------------- /utils/test/testMySQL.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "log" 7 | 8 | _ "github.com/go-sql-driver/mysql" 9 | ) 10 | 11 | func main() { 12 | dsn := "root:v6SxhWHyZC7S@tcp(localhost:33306)/linkme?charset=utf8mb4&parseTime=True&loc=Local" 13 | 14 | db, err := sql.Open("mysql", dsn) 15 | if err != nil { 16 | log.Fatalf("Failed to open DB: %v", err) 17 | } 18 | defer db.Close() 19 | 20 | // 测试连接 21 | err = db.Ping() 22 | if err != nil { 23 | log.Fatalf("Failed to connect to MySQL: %v", err) 24 | } 25 | 26 | fmt.Println("✅ Successfully connected to MySQL!") 27 | 28 | // 插入数据 29 | insertSQL := `INSERT INTO users (created_at, updated_at, deleted_at, username, password_hash, deleted, roles) 30 | VALUES (UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), NULL, ?, ?, ?, ?);` 31 | 32 | _, err = db.Exec(insertSQL, "admin2", "$2a$10$wH8sHZTflD5vKj5iHxD5reZ6eYPs1E4/RyTc7HbYJxU6sphGzHl7i", 0, `[{"role":"admin"}]`) 33 | if err != nil { 34 | log.Fatalf("Failed to insert user: %v", err) 35 | } 36 | 37 | fmt.Println("✅ User 'admin' inserted successfully!") 38 | } 39 | --------------------------------------------------------------------------------