Repository: Double-Jin/jin-microservices Branch: main Commit: 15c02c8d0bb6 Files: 264 Total size: 240.1 KB Directory structure: gitextract_8s8otvkg/ ├── .gitignore ├── LICENSE ├── README-CN.md ├── README.md ├── api-gateway/ │ ├── app/ │ │ ├── Constants/ │ │ │ ├── ErrorCode.php │ │ │ └── ResponseCode.php │ │ ├── Controller/ │ │ │ ├── AbstractController.php │ │ │ ├── AuthController.php │ │ │ ├── CircuitBreakerController.php │ │ │ ├── CommonController.php │ │ │ ├── ConfigController.php │ │ │ ├── FileController.php │ │ │ ├── OrderController.php │ │ │ ├── RateLimitController.php │ │ │ └── UserController.php │ │ ├── Exception/ │ │ │ ├── BusinessException.php │ │ │ ├── Handler/ │ │ │ │ ├── AppExceptionHandler.php │ │ │ │ ├── AppServiceExceptionHandler.php │ │ │ │ └── RateLimitExceptionHandler.php │ │ │ └── ServiceException.php │ │ ├── JsonRpc/ │ │ │ ├── FileRpcServiceInterface.php │ │ │ ├── OrderRpcServiceInterface.php │ │ │ └── UserRpcServiceInterface.php │ │ ├── Kernel/ │ │ │ └── Functions.php │ │ ├── Listener/ │ │ │ ├── DbQueryExecutedListener.php │ │ │ └── QueueHandleListener.php │ │ ├── Log.php │ │ ├── Middleware/ │ │ │ └── UserAuthMiddleware.php │ │ ├── Model/ │ │ │ └── Model.php │ │ ├── Process/ │ │ │ └── AsyncQueueConsumer.php │ │ └── Services/ │ │ ├── AuthService.php │ │ ├── FileService.php │ │ ├── OrderService.php │ │ └── UserService.php │ ├── bin/ │ │ └── hyperf.php │ ├── composer.json │ ├── config/ │ │ ├── autoload/ │ │ │ ├── annotations.php │ │ │ ├── aspects.php │ │ │ ├── async_queue.php │ │ │ ├── cache.php │ │ │ ├── commands.php │ │ │ ├── config_center.php │ │ │ ├── databases.php │ │ │ ├── dependencies.php │ │ │ ├── devtool.php │ │ │ ├── exceptions.php │ │ │ ├── listeners.php │ │ │ ├── logger.php │ │ │ ├── middlewares.php │ │ │ ├── nacos.php │ │ │ ├── opentracing.php │ │ │ ├── processes.php │ │ │ ├── rate_limit.php │ │ │ ├── redis.php │ │ │ ├── server.php │ │ │ ├── services.php │ │ │ ├── tracer.php │ │ │ └── watcher.php │ │ ├── config.php │ │ ├── container.php │ │ └── routes.php │ └── test/ │ ├── Cases/ │ │ └── ExampleTest.php │ ├── HttpTestCase.php │ └── bootstrap.php ├── doc/ │ ├── DEPLOY_ONE.md │ └── DEPLOY_TWO.md ├── docker-compose.yaml ├── file-srv/ │ ├── .github/ │ │ └── workflows/ │ │ ├── Dockerfile │ │ ├── build.yml │ │ └── release.yml │ ├── .gitignore │ ├── .watcher.php │ ├── app/ │ │ ├── Constants/ │ │ │ └── ErrorCode.php │ │ ├── Controller/ │ │ │ ├── AbstractController.php │ │ │ └── IndexController.php │ │ ├── Exception/ │ │ │ ├── BusinessException.php │ │ │ ├── Handler/ │ │ │ │ └── AppExceptionHandler.php │ │ │ └── JsonRpcException.php │ │ ├── JsonRpc/ │ │ │ ├── FileRpcService.php │ │ │ └── FileRpcServiceInterface.php │ │ ├── Listener/ │ │ │ ├── DbQueryExecutedListener.php │ │ │ └── ResumeExitCoordinatorListener.php │ │ ├── Log.php │ │ ├── Model/ │ │ │ └── Model.php │ │ └── Services/ │ │ └── FileService.php │ ├── bin/ │ │ └── hyperf.php │ ├── composer.json │ ├── config/ │ │ ├── autoload/ │ │ │ ├── annotations.php │ │ │ ├── aspects.php │ │ │ ├── cache.php │ │ │ ├── commands.php │ │ │ ├── config_center.php │ │ │ ├── databases.php │ │ │ ├── dependencies.php │ │ │ ├── devtool.php │ │ │ ├── exceptions.php │ │ │ ├── file.php │ │ │ ├── listeners.php │ │ │ ├── logger.php │ │ │ ├── middlewares.php │ │ │ ├── nacos.php │ │ │ ├── opentracing.php │ │ │ ├── processes.php │ │ │ ├── redis.php │ │ │ ├── server.php │ │ │ └── services.php │ │ ├── config.php │ │ ├── container.php │ │ └── routes.php │ ├── public/ │ │ └── uploads/ │ │ └── Pp7jmGorNbHecq50.xlsx │ └── test/ │ ├── Cases/ │ │ └── ExampleTest.php │ ├── HttpTestCase.php │ └── bootstrap.php ├── order-srv/ │ ├── app/ │ │ ├── Amqp/ │ │ │ └── Producer/ │ │ │ └── OrderProducer.php │ │ ├── Constants/ │ │ │ ├── ErrorCode.php │ │ │ └── ResponseCode.php │ │ ├── Controller/ │ │ │ ├── AbstractController.php │ │ │ └── SagaController.php │ │ ├── Exception/ │ │ │ ├── Handler/ │ │ │ │ ├── DtmExceptionHandler.php │ │ │ │ └── JsonRpcExceptionHandler.php │ │ │ ├── JsonRpcException.php │ │ │ └── ServiceException.php │ │ ├── JsonRpc/ │ │ │ ├── OrderRpcService.php │ │ │ ├── OrderRpcServiceInterface.php │ │ │ └── UserRpcServiceInterface.php │ │ ├── Kernel/ │ │ │ └── Functions.php │ │ ├── Listener/ │ │ │ └── DbQueryExecutedListener.php │ │ ├── Log.php │ │ ├── Model/ │ │ │ ├── Order.php │ │ │ └── OrderGoods.php │ │ └── Services/ │ │ └── OrderService.php │ ├── bin/ │ │ └── hyperf.php │ ├── composer.json │ ├── config/ │ │ ├── autoload/ │ │ │ ├── amqp.php │ │ │ ├── annotations.php │ │ │ ├── aspects.php │ │ │ ├── cache.php │ │ │ ├── commands.php │ │ │ ├── config_center.php │ │ │ ├── databases.php │ │ │ ├── dependencies.php │ │ │ ├── devtool.php │ │ │ ├── dtm.php │ │ │ ├── exceptions.php │ │ │ ├── listeners.php │ │ │ ├── logger.php │ │ │ ├── middlewares.php │ │ │ ├── nacos.php │ │ │ ├── opentracing.php │ │ │ ├── processes.php │ │ │ ├── redis.php │ │ │ ├── server.php │ │ │ ├── services.php │ │ │ ├── tracer.php │ │ │ └── watcher.php │ │ ├── config.php │ │ ├── container.php │ │ └── routes.php │ ├── migrations/ │ │ ├── 2022_06_08_061612_create_order_table.php │ │ └── 2022_06_08_063020_create_order_goods_table.php │ ├── seeders/ │ │ ├── order_goods_seeder.php │ │ └── order_seeder.php │ └── test/ │ ├── Cases/ │ │ └── ExampleTest.php │ ├── HttpTestCase.php │ └── bootstrap.php ├── task-srv/ │ ├── .github/ │ │ └── workflows/ │ │ ├── Dockerfile │ │ ├── build.yml │ │ └── release.yml │ ├── .gitignore │ ├── app/ │ │ ├── Amqp/ │ │ │ └── Consumer/ │ │ │ ├── OrderConsumer.php │ │ │ └── UserConsumer.php │ │ ├── Command/ │ │ │ └── TestCommand.php │ │ ├── Controller/ │ │ │ ├── AbstractController.php │ │ │ └── IndexController.php │ │ ├── Exception/ │ │ │ └── Handler/ │ │ │ └── AppExceptionHandler.php │ │ ├── Listener/ │ │ │ ├── DbQueryExecutedListener.php │ │ │ └── QueueHandleListener.php │ │ ├── Log.php │ │ ├── Model/ │ │ │ └── Model.php │ │ └── Process/ │ │ └── AsyncQueueConsumer.php │ ├── bin/ │ │ └── hyperf.php │ ├── composer.json │ ├── config/ │ │ ├── autoload/ │ │ │ ├── amqp.php │ │ │ ├── annotations.php │ │ │ ├── aspects.php │ │ │ ├── async_queue.php │ │ │ ├── cache.php │ │ │ ├── commands.php │ │ │ ├── config_center.php │ │ │ ├── crontab.php │ │ │ ├── databases.php │ │ │ ├── dependencies.php │ │ │ ├── devtool.php │ │ │ ├── exceptions.php │ │ │ ├── listeners.php │ │ │ ├── logger.php │ │ │ ├── middlewares.php │ │ │ ├── nacos.php │ │ │ ├── opentracing.php │ │ │ ├── processes.php │ │ │ ├── redis.php │ │ │ └── server.php │ │ ├── config.php │ │ ├── container.php │ │ └── routes.php │ └── test/ │ ├── Cases/ │ │ └── ExampleTest.php │ ├── HttpTestCase.php │ └── bootstrap.php └── user-srv/ ├── app/ │ ├── Amqp/ │ │ └── Producer/ │ │ └── UserProducer.php │ ├── Constants/ │ │ ├── ErrorCode.php │ │ └── ResponseCode.php │ ├── Controller/ │ │ ├── AbstractController.php │ │ └── SagaController.php │ ├── Exception/ │ │ ├── Handler/ │ │ │ ├── DtmExceptionHandler.php │ │ │ └── JsonRpcExceptionHandler.php │ │ ├── JsonRpcException.php │ │ └── ServiceException.php │ ├── JsonRpc/ │ │ ├── UserRpcService.php │ │ └── UserRpcServiceInterface.php │ ├── Kernel/ │ │ └── Functions.php │ ├── Listener/ │ │ └── DbQueryExecutedListener.php │ ├── Log.php │ ├── Model/ │ │ ├── User.php │ │ ├── UserBonusLog.php │ │ └── UserStoredLog.php │ └── Services/ │ ├── AuthService.php │ └── UserService.php ├── bin/ │ └── hyperf.php ├── composer.json ├── config/ │ ├── autoload/ │ │ ├── amqp.php │ │ ├── annotations.php │ │ ├── aspects.php │ │ ├── auth.php │ │ ├── cache.php │ │ ├── commands.php │ │ ├── config_center.php │ │ ├── databases.php │ │ ├── dependencies.php │ │ ├── devtool.php │ │ ├── dtm.php │ │ ├── exceptions.php │ │ ├── listeners.php │ │ ├── logger.php │ │ ├── middlewares.php │ │ ├── nacos.php │ │ ├── opentracing.php │ │ ├── processes.php │ │ ├── redis.php │ │ ├── server.php │ │ ├── services.php │ │ ├── tracer.php │ │ └── watcher.php │ ├── config.php │ ├── container.php │ └── routes.php ├── migrations/ │ ├── 2022_06_08_072031_create_user_table.php │ ├── 2022_06_08_072120_create_user_bonus_log_table.php │ └── 2022_06_08_072134_create_user_stored_log_table.php ├── seeders/ │ ├── user_bonus_log_seeder.php │ ├── user_seeder.php │ └── user_stored_log_seeder.php └── test/ ├── Cases/ │ └── ExampleTest.php ├── HttpTestCase.php └── bootstrap.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ */.buildpath */.settings/ */.project */*.patch */.idea/ .idea/ */.git/ .git/ */runtime/ */vendor/ */.phpintel/ */.env */.DS_Store */.phpunit* */*.cache */watch */composer.lock ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Double-Jin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README-CN.md ================================================ [English](./README.md) | [中文] # 介绍 Jin-microservices是基于 php 语言 + hyperf 微服务 框架的完整微服务demo。 github:https://github.com/Double-Jin/jin-microservices gitee:https://gitee.com/ljj96/jin-microservices # 关于 JM 作为php、go双修的开发者, go 语言的微服务体系已经基本掌握,go 语言相关的微服务的文章、开源项目在网上搜索一搜一大堆,这让会 go 语言的开发者能容易地上手并实现微服务,毕竟go语言是除java外最合适做微服务的语言这一。 php语言的优势在于web生态,开发的应用绝大多数为单体应用架构。近年来随着基于 swoole 扩展的 hyperf框架的出现,让php也能开发微服务架构,这里要感谢开源工作者。但用 "php + 微服务 "作为关键词时,搜索出来的文章、开源项目都是一些简单的案例,开发者并不能通过这些简单的案例来了解微服务,这让我有了想写本项目的原始动力。 # 前言 JM 是一款基于 php 语言 + hyperf 微服务 框架编写的完整微服务demo,与网上能找到的单一功能点简单实现的文章不同,JM从实际项目需求出发,力求做到git clone 项目下来后对着文件就能帮你构建微服务完整的知识体系,让你实际用hyperf开发微服务项目时能粘贴复制本项目的代码。 微服务架构并不是比单体架构先进的架构,只是在项目体量、项目开发者人数达到一定量级后的一种选择。切勿盲目鼓吹微服务,在团队开发、运维能力不足的情况下强行推进微服务架构恐怕会适得其反。 下面提到的组件并不是微服务架构才能使用,如elk、nacos、dtm这些,在单体应用里面也有合适的场景用到,取其精华来满足业务上的需要。如在生产上用到这些组件最好选择编译安装或购买云服务 ## 功能亮点 * 完整微服务架构 * JsonRpc调用 * JWT认证 * 文件上传 * 统一异常处理 * 服务注册与服务发现 * 消息队列 * 链接追踪 * 配置中心 * 服务限流 * 服务降级 * 分布式日志 * 分布式事务 # 准备 微服务是把单体应用进行分拆后的架构,通过引用第三方组件来解决分拆后带来的问题,安装部署这些组件的时候你将会遇到很多奇奇怪怪的问题。为减低难度,本项目大部分组件采用docker来安装,整体流程我已在不同的电脑上验证数遍,即便如此还是会存在如composer、github、http/tcp访问、端口、内存、docker版本等问题,同样的操作换了台电脑就可能出问题,这需要你跟据报错内容查找相关资料自行解决。 - 8核16G电脑 - 熟悉docker - 了解网络协议 - 基本的运维能力 # 安装 - docker安装 - [github](https://github.com/Double-Jin/jin-microservices/blob/main/doc/DEPLOY_ONE.md) - [gitee](https://gitee.com/ljj96/jin-microservices/blob/main/doc/DEPLOY_ONE.md) - docker-compose安装 - [github](https://github.com/Double-Jin/jin-microservices/blob/main/doc/DEPLOY_TWO.md) - [gitee](https://gitee.com/ljj96/jin-microservices/blob/main/doc/DEPLOY_TWO.md) # 使用 * 目录结构 ``` |-- api-gateway //网关服务项目代码 |-- order-srv //订单服务项目代码 |-- fIle-srv //文件服务项目代码 |-- user-srv // 用户服务项目代码 |-- task-srv // 定时任务、队列消费服务项目代码 |-- doc // 文档目录 |-- README.md //英文说明 |-- README-CN.md //中文说明 ``` * 完整微服务架构 ![完整的php微服务案例](https://cdn.learnku.com/uploads/images/202206/15/36324/zfxnraiUJx.png!large) * JsonRpc调用 - `GET http://127.0.0.1:9501/User/UserInfo` 通讯单一服务 - `GET http://127.0.0.1:9501/User/UserBonusList` 通讯单一服务 - `GET http://127.0.0.1:9501/User/UserStoredList` 通讯单一服务 - `GET http://127.0.0.1:9501/Order/OrderList` 通讯多个服务 * JWT认证 - `GET http://127.0.0.1:9501/Auth/Login` 用户登录 - `GET http://127.0.0.1:9501/Auth/Logout Authorization : Bearer {{token}}` 用户退出登录 * 文件上传 - 微服务中rpc是轻量级通信框架,擅长传输字符串。对于文本文件传输不太友好, 而对于文件如果强行都成String,就会是一堆乱码。因此,大致的解决思路是,不管文件类型,统统转化成二进制,再将二进制进行Base64编码成String,传输编码后的String到RPC服务,RPC服务按照Base64解码还原成二进制,通过二进制构造File对象即可。 - `POST http://127.0.0.1:9501/File/UploadFile` 文件转为base64字符串通过json-rpc传输 - `file-srv.app.JsonRpc.FileRpcService.uploadFile` 通过json-rpc接收base64字符串生成文件 * 统一异常处理 - 封装AppServiceExceptionHandler.php 统一处理http请求异常 - 封装RateLimitExceptionHandler.php 统一处理限流异常 - 封装JsonRpcExceptionHandler.php 统一处理JsonPrc通讯异常 - 封装DtmExceptionHandler.php 统一处理DTM事务中间件异常 * 服务注册与服务发现 ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/03VknWhiB6.png!large) * 消息队列 - `GET http://127.0.0.1:9501/User/UserRabbitMQ` 调用投递用户消息队列接口 - `GET http://127.0.0.1:9501/Order/OrderRabbitMQ` 调用投递订单消息队列接口 ![完整的php微服务案例](https://cdn.learnku.com/uploads/images/202206/15/36324/0BU5P5RHTL.jpeg!large) * 链接追踪 ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/PaiIwXUVrr.png!large) * 配置中心 ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/SfiKibJ55r.png!large) ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/K4Zf5zlBhq.png!large) * 服务限流 `GET http://127.0.0.1:9501/RateLimit/Test` ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/QpZmGG31WD.png!large) * 服务降级 `GET http://127.0.0.1:9501/CircuitBreaker/Test` ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/bpVBwAgOKl.png!large) * 分布式日志 当系统变为集群后,应用日志在数十台甚至是上百台不同的服务器上,能实现日志的统一查找、分析和归档等功能便可称为分布式日志系统。 生产上方案会有很多,如将日志直接输出来Elasticsearch,如使用云服务商提供的日志收集。本案例采用的是通过filebeat将日志同步到ELK中。 ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/qFqXMfcYu2.png!large) * 分布式事务 数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。但对分布式系统来说,数据的操作来自多个不同的数据库,单个数据库事务的成功或失败不代表整个系统的数据一致性是对的,只能够通过分布式事务来解决。 分布式事务就是指事务的发起者、资源及资源管理器和事务协调者分别位于分布式系统的不同节点之上。行业上常用的有二阶段提交、SAGA、TCC等方案,当了解原理后,你自行用http/tcp也能实现二阶段提交、SAGA、TCC。 下面的接口通过DTM调度实现在一个SAGA案例。 `POST http://127.0.0.1:9501/Order/CreateOrder` 分布式事务 ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/7tiJcnKiXi.png!large) ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/a5v6AdYVT2.png!large) ![完整的php微服务案例](https://cdn.learnku.com/uploads/images/202206/15/36324/txAseIwfVr.png!large) # 不足 * 不支持gRpc的服务注册与服务发现 * 配置中心组件只支持config调用,无法做到env的动态写入与框架重启,但可通过k8s实现 # 感谢 - hyperf:https://hyperf.wiki/2.2/#/README - dtm:https://en.dtm.pub/ - nacos:https://nacos.io/zh-cn/docs/what-is-nacos.html - elk:https://www.elastic.co/cn/what-is/elk-stack ================================================ FILE: README.md ================================================ English | [中文](./README-CN.md) # Introduction Jin microservices is a complete microservice demo based on PHP language + hyperf microservices framework github:https://github.com/Double-Jin/jin-microservices gitee:https://gitee.com/ljj96/jin-microservices # About JM As a developer of both php and go, the microservice system of the go language has been basically mastered. There are a lot of articles and open source projects related to the go language on the Internet, which makes it easy for developers who know the go language Get started and implement microservices. After all, go language is the most suitable language for microservices other than java. The advantage of the php language lies in the web ecosystem, and most of the applications developed are monolithic application architectures. In recent years, with the emergence of hyperf framework based on swoole extension, php can also develop microservice architecture, thanks to open source workers. However, when using "php + microservices" as the keyword, the articles and open source projects searched are simple cases. Developers cannot understand microservices through these simple cases, which makes me want to write this project. raw power. # Foreword JM is a complete microservice demo written based on php language + hyperf microservice framework. Unlike articles that can be found on the Internet with simple implementation of a single function point, JM starts from the actual project requirements and strives to achieve the git clone project. The file can help you build a complete knowledge system for microservices, so that you can paste and copy the code of this project when you actually use hyperf to develop a microservice project. The microservice architecture is not an architecture that is more advanced than the monolithic architecture, but a choice after the project volume and the number of project developers reach a certain order of magnitude. Do not blindly advocate microservices. Forcibly promoting the microservice architecture when the team's development and operation and maintenance capabilities are insufficient may be counterproductive. The components mentioned below are not only used in microservice architecture, such as elk, nacos, and dtm. They are also used in suitable scenarios in monolithic applications, and the essence of them can be used to meet business needs. If these components are used in production, it is best to choose to compile and install or purchase cloud services ## Feature * Complete microservice architecture * JsonRpc call * Jwt authority authentication * Upload the file * Unified exception handling * Service Registration and Service Discovery * Message queue * Link tracking * Configuration Center * Service current limit * Service downgrade * Distributed log * Distributed transaction # Prepare Microservices are an architecture that splits a monolithic application. By using third-party components to solve the problems caused by the split, you will encounter many strange problems when installing and deploying these components. In order to reduce the difficulty, most of the components of this project are installed by docker. I have verified the overall process several times on different computers. Even so, there are still problems such as composer, github, http/tcp access, ports, memory, docker version, etc. , the same operation may cause problems if you change the computer, which requires you to find relevant information according to the content of the error to solve it yourself. - 8 core 16G computer - familiar with docker - Learn about network protocols - Basic operation and maintenance capabilities # Install - docker install - [github](https://github.com/Double-Jin/jin-microservices/blob/main/doc/DEPLOY_ONE.md) - [gitee](https://gitee.com/ljj96/jin-microservices/blob/main/doc/DEPLOY_ONE.md) - docker-compose install - [github](https://github.com/Double-Jin/jin-microservices/blob/main/doc/DEPLOY_TWO.md) - [gitee](https://gitee.com/ljj96/jin-microservices/blob/main/doc/DEPLOY_TWO.md) # Use * Directory Structure ``` |-- api-gateway //Gateway service project code |-- order-srv //Order service project code |-- user-srv // User service project code |-- task-srv // Timed task and queue service project code |-- doc // Documentation directory |-- README.md //English description |-- README-CN.md //Chinese description ``` * Complete microservice architecture ![完整的php微服务案例](https://cdn.learnku.com/uploads/images/202206/15/36324/zfxnraiUJx.png!large) * JsonRpc call - `GET http://127.0.0.1:9501/User/UserInfo` Communication single service - `GET http://127.0.0.1:9501/User/UserBonusList` Communication single service - `GET http://127.0.0.1:9501/User/UserStoredList` Communication single service - `GET http://127.0.0.1:9501/Order/OrderList` Communication multiple services * Jwt authority authentication - `GET http://127.0.0.1:9501/Auth/Login` User login - `GET http://127.0.0.1:9501/Auth/Logout Authorization : Bearer {{token}}` User logged out * Upload the file - RPC in microservices is a lightweight communication framework that is good at transporting strings. Not very friendly for text file transfers, And for the file if it is forced into String, it will be a bunch of garbled characters. Therefore, the general solution idea is that regardless of the file type, - all are converted into binary, and then the binary is Base64 encoded into String, and the encoded String is transferred to the background, and the background is restored to binary according to Base64 decoding, and the File object can be constructed through binary. - `POST http://127.0.0.1:9501/File/UploadFile` The file is converted to a base64 string and transferred via json-rpc - `file-srv.app.JsonRpc.FileRpcService.uploadFile` Generate a file by receiving a base64 string via json-rpc * Unified exception handling - Encapsulate AppServiceExceptionHandler.php http request exceptions - Encapsulate RateLimitExceptionHandler.php current limiting exceptions - Encapsulate JsonRpcExceptionHandler.php JsonPrc exceptions - Encapsulate DtmExceptionHandler.php DTM Distributed transaction exceptions * Service Registration and Service Discovery ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/03VknWhiB6.png!large) * Message queue - `GET http://127.0.0.1:9501/User/UserRabbitMQ` Call the delivery user message queue interface - `GET http://127.0.0.1:9501/Order/OrderRabbitMQ` Call the delivery order message queue interface ![完整的php微服务案例](https://cdn.learnku.com/uploads/images/202206/15/36324/0BU5P5RHTL.jpeg!large) * Link tracking ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/PaiIwXUVrr.png!large) * Configuration Center ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/SfiKibJ55r.png!large) ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/K4Zf5zlBhq.png!large) * Service current limit `GET http://127.0.0.1:9501/RateLimit/Test` ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/QpZmGG31WD.png!large) * Service downgrade `GET http://127.0.0.1:9501/CircuitBreaker/Test` ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/bpVBwAgOKl.png!large) * Distributed log When the system becomes a cluster, the application logs on dozens or even hundreds of different servers can realize the functions of unified search, analysis and archiving of logs, which can be called a distributed log system. There are many solutions in production, such as outputting logs directly to Elasticsearch, such as using log collection provided by cloud service providers. In this case, the log is synchronized to ELK through filebeat. ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/qFqXMfcYu2.png!large) * Distributed transaction A database transaction ensures that all operations within the scope of the transaction can either succeed or fail. But for distributed systems, data operations come from multiple different databases, and the success or failure of a single database transaction does not mean that the data consistency of the entire system is correct, and can only be resolved through distributed transactions. Distributed transaction means that the initiator, resource and resource manager and transaction coordinator of the transaction are located on different nodes of the distributed system. Two-phase submission, SAGA, TCC and other schemes are commonly used in the industry. After understanding the principle, you can also use http/tcp to achieve two-phase submission, SAGA, and TCC. The following interface is implemented in a SAGA case via DTM scheduling. `POST http://127.0.0.1:9501/Order/CreateOrder` Distributed transaction ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/7tiJcnKiXi.png!large) ![带你走进微服务](https://cdn.learnku.com/uploads/images/202205/31/36324/a5v6AdYVT2.png!large) ![完整的php微服务案例](https://cdn.learnku.com/uploads/images/202206/15/36324/txAseIwfVr.png!large) # Question * Service registration and service discovery without gRpc support * The configuration center component only supports config calls, and cannot do dynamic writing of env and framework restarts, but it can be achieved through k8s # Thank - hyperf:https://hyperf.wiki/2.2/#/README - dtm:https://en.dtm.pub/ - nacos:https://nacos.io/zh-cn/docs/what-is-nacos.html - elk:https://www.elastic.co/cn/what-is/elk-stack ================================================ FILE: api-gateway/app/Constants/ErrorCode.php ================================================ authService->getRpcUserLogin('1358888888'); return $this->success($res); } /** * 用户退出 * @param RequestInterface $request * @return \Psr\Http\Message\ResponseInterface */ #[GetMapping(path: 'Logout')] #[Middleware(UserAuthMiddleware::class)] public function Logout(RequestInterface $request) { //调用authService.getRpcUserLogout $res = $this->authService->getRpcUserLogout(getBearerToken($request->header('Authorization'))); return $this->success($res); } } ================================================ FILE: api-gateway/app/Controller/CircuitBreakerController.php ================================================ 0.05],failCounter:1, successCounter:1, fallback:"App\Controller\CircuitBreakerController::circuitBreakerFallback")] public function test() { $this->userService->testCircuitBreaker(); return $this->success('这是服务降级'); } /** * 服务降级异常返回方法 * @return \Psr\Http\Message\ResponseInterface */ public function circuitBreakerFallback() { return $this->success('服务降级啦~'); } } ================================================ FILE: api-gateway/app/Controller/CommonController.php ================================================ response->json(responseSuccess(200, '', $data)); } } ================================================ FILE: api-gateway/app/Controller/ConfigController.php ================================================ success($config); } } ================================================ FILE: api-gateway/app/Controller/FileController.php ================================================ fileService->getRpcUploadFile($request->file('file')); return $this->success($res); } } ================================================ FILE: api-gateway/app/Controller/OrderController.php ================================================ orderService->getRpcOrderList(1); return $this->success($res); } /** * 创建订单 * @param RequestInterface $request * @return \Psr\Http\Message\ResponseInterface */ #[PostMapping(path: 'CreateOrder')] public function createOrder(RequestInterface $request) { //调用orderService.rpcCreateOrder方法 $res = $this->orderService->rpcCreateOrder([ 'user_id' => 1, 'coupon_id' => 0, 'order_money' => '100', 'order_discount' => '100', 'order_fact_money' => '100', 'consume_number' => 1, 'payment' => 1, 'goods' => [ [ 'goods_id' => 1, 'goods_sn' => 'JIN02', 'sku_id' => 1, 'user_id' => 1, 'goods_name' => '测试商品', 'number' => 1, 'goods_tag_price' => '100', 'goods_real_price' => '100', 'goods_discount' => '100', 'goods_fact_money' => '100' ] ] ]); return $this->success($res); } /** * 投递订单消息到RabbitMQ * @param RequestInterface $request * @return \Psr\Http\Message\ResponseInterface */ #[GetMapping(path: 'OrderRabbitMQ')] public function orderRabbitMQ(RequestInterface $request) { //调用orderService.getRpcUserStoredList方法 $res = $this->orderService->getRpcOrderRabbitMQ(); return $this->success($res); } } ================================================ FILE: api-gateway/app/Controller/RateLimitController.php ================================================ success('这是测试限流'); } } ================================================ FILE: api-gateway/app/Controller/UserController.php ================================================ userService->getRpcUserInfo(1); return $this->success($res); } /** * 用户积分列表 * @param RequestInterface $request * @return \Psr\Http\Message\ResponseInterface */ #[GetMapping(path: 'UserBonusList')] public function userBonusList(RequestInterface $request) { //调用userService.getRpcUserBonusList方法 $res = $this->userService->getRpcUserBonusList(1,15); return $this->success($res); } /** * 用户储值列表 * @param RequestInterface $request * @return \Psr\Http\Message\ResponseInterface */ #[GetMapping(path: 'UserStoredList')] public function userStoredList(RequestInterface $request) { //调用userService.getRpcUserStoredList方法 $res = $this->userService->getRpcUserStoredList(1,15); return $this->success($res); } /** * 投递用户消息到RabbitMQ * @param RequestInterface $request * @return \Psr\Http\Message\ResponseInterface */ #[GetMapping(path: 'UserRabbitMQ')] public function userRabbitMQ(RequestInterface $request) { //调用userService.getRpcUserStoredList方法 $res = $this->userService->getRpcUserRabbitMQ(); return $this->success($res); } } ================================================ FILE: api-gateway/app/Exception/BusinessException.php ================================================ withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: api-gateway/app/Exception/Handler/AppServiceExceptionHandler.php ================================================ stopPropagation(); //返回自定义错误数据 $result = responseError($throwable->getCode(), $throwable->getMessage()); return $response->withStatus($throwable->getCode()) ->withAddedHeader('content-type', 'application/json') ->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE))); } // 交给下一个异常处理器 return $response; } /** * * @param Throwable $throwable 抛出的异常 * @return bool 该异常处理器是否处理该异常 */ public function isValid(Throwable $throwable): bool { //当前的异常是否属于业务异常 return true; } } ================================================ FILE: api-gateway/app/Exception/Handler/RateLimitExceptionHandler.php ================================================ stopPropagation(); //返回自定义错误数据 $result = responseError($throwable->getCode(), '您被限流啦'); return $response->withStatus($throwable->getCode()) ->withAddedHeader('content-type', 'application/json') ->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE))); } // 交给下一个异常处理器 return $response; } /** * * @param Throwable $throwable 抛出的异常 * @return bool 该异常处理器是否处理该异常 */ public function isValid(Throwable $throwable): bool { //当前的异常是否属于业务异常 return true; } } ================================================ FILE: api-gateway/app/Exception/ServiceException.php ================================================ get($id); } return $container; } } /** * 控制台日志 */ if (!function_exists('stdLog')) { function stdLog() { return di()->get(StdoutLoggerInterface::class); } } /** * 文件日志 */ if (!function_exists('logger')) { function logger($name = 'log', $group = 'default') { return di()->get(LoggerFactory::class)->get($name, $group); } } /** * redis 客户端实例 */ if (!function_exists('redis')) { function redis() { return di()->get(Redis::class); } } /** * 缓存实例 简单的缓存 */ if (!function_exists('cache')) { function cache() { return di()->get(\Psr\SimpleCache\CacheInterface::class); } } if (!function_exists('format_throwable')) { /** * Format a throwable to string. * @param Throwable $throwable * @return string */ function format_throwable(Throwable $throwable): string { return di()->get(FormatterInterface::class)->format($throwable); } } if (!function_exists('responseSuccess')) { function responseSuccess($code, $message = '', $data = []) { $content = ['code' => $code]; $message ? $content['msg'] = $message : $content['msg'] = \App\Constants\ResponseCode::getMessage($code); $data ? $content['data'] = $data : $content['data'] = []; return $content; } } if (!function_exists('responseError')) { function responseError($code, $message = '', $data = []) { $content = ['code' => $code]; $message ? $content['msg'] = $message : $content['msg'] = \App\Constants\ResponseCode::getMessage($code); $data ? $content['data'] = $data : $content['data'] = []; return $content; } } /** * 判读字符串是否为json */ if (!function_exists('isJson')) { /** * Finds an entry of the container by its identifier and returns it. * @param string $string */ function isJson($string) { json_decode($string); return (json_last_error() == JSON_ERROR_NONE); } } /** * 容器实例 */ if (!function_exists('container')) { function container() { return ApplicationContext::getContainer(); } } /** * 获取BearerToken */ if (!function_exists('getBearerToken')) { function getBearerToken($Authorization) { if (\Hyperf\Utils\Str::startsWith($Authorization, 'Bearer ')) { return \Hyperf\Utils\Str::substr($Authorization, 7); } return null; } } ================================================ FILE: api-gateway/app/Listener/DbQueryExecutedListener.php ================================================ logger = $container->get(LoggerFactory::class)->get('sql'); } public function listen(): array { return [ QueryExecuted::class, ]; } /** * @param QueryExecuted $event */ public function process(object $event) : void { if ($event instanceof QueryExecuted) { $sql = $event->sql; if (! Arr::isAssoc($event->bindings)) { foreach ($event->bindings as $key => $value) { $sql = Str::replaceFirst('?', "'{$value}'", $sql); } } $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } return; } } ================================================ FILE: api-gateway/app/Listener/QueueHandleListener.php ================================================ logger = $loggerFactory->get('queue'); $this->formatter = $formatter; } public function listen(): array { return [ AfterHandle::class, BeforeHandle::class, FailedHandle::class, RetryHandle::class, ]; } public function process(object $event) : void { if ($event instanceof Event && $event->message->job()) { $job = $event->message->job(); $jobClass = get_class($job); if ($job instanceof AnnotationJob) { $jobClass = sprintf('Job[%s@%s]', $job->class, $job->method); } $date = date('Y-m-d H:i:s'); switch (true) { case $event instanceof BeforeHandle: $this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass)); break; case $event instanceof AfterHandle: $this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass)); break; case $event instanceof FailedHandle: $this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass)); $this->logger->error($this->formatter->format($event->getThrowable())); break; case $event instanceof RetryHandle: $this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass)); break; } } } } ================================================ FILE: api-gateway/app/Log.php ================================================ get(LoggerFactory::class)->get($name); } } ================================================ FILE: api-gateway/app/Middleware/UserAuthMiddleware.php ================================================ container = $container; $this->response = $response; $this->request = $request; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $token = $this->getBearerToken($request); if (empty($token)){ throw new ServiceException(422,'token不存在'); } //调用authService.getRpcUserCheckToken方法 $this->authService->getRpcUserCheckToken($token); return $handler->handle($request); } public function getBearerToken($request) { $Authorization = $this->request->header('Authorization'); return getBearerToken($Authorization); } } ================================================ FILE: api-gateway/app/Model/Model.php ================================================ info("调用getRpcUserLogin"); try { //调用用户服务中的用户登录方法 $res = $this->userRpcServiceInterface->userLogin($phone); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 用户退出 * @param string $token * @return mixed */ public function getRpcUserLogout(string $token) { Log::get()->info("调用getRpcUserLogout"); try { //调用用户服务中的用户退出方法 $res = $this->userRpcServiceInterface->userLogout($token); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 检查用户token * @param string $token * @return mixed */ public function getRpcUserCheckToken(string $token) { Log::get()->info("调用getRpcUserLogout"); try { //调用用户服务中的检查用户token方法 $res = $this->userRpcServiceInterface->userCheckToken($token); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430,'token已过期'); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430,'token已过期'); } return $res['data']; } } ================================================ FILE: api-gateway/app/Services/FileService.php ================================================ info("调用uploadFile"); $type =$file->getExtension(); //获取文件类型 $fileData = file_get_contents($file->getRealPath()); //获取文件二进制流 $base64String = 'data:' . $type . ';base64,' . chunk_split(base64_encode($fileData)); $fileName = Str::random(16) . '.' . $type; try { //调用文件服务中的文件上传方法 $res = $this->fileRpcServiceInterface->uploadFile($type,$base64String,$fileName); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } } ================================================ FILE: api-gateway/app/Services/OrderService.php ================================================ info("调用getRpcOrderList"); try { //调用订单服务中的订单列表方法 $res = $this->orderRpcServiceInterface->orderList($userId); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 创建列表 * @param $data * @return mixed */ public function rpcCreateOrder(array $data) { Log::get()->info("调用rpcCreateOrder"); try { //调用订单服务中的创建订单方法 $res = $this->orderRpcServiceInterface->createOrder($data); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 投递用户消息到RabbitMQ * @return mixed */ public function getRpcOrderRabbitMQ() { Log::get()->info("调用getRpcUserRabbitMQ"); try { //调用订单服务中的投递订单消息到RabbitMQ方法 $res = $this->orderRpcServiceInterface->orderRabbitMQ(); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } } ================================================ FILE: api-gateway/app/Services/UserService.php ================================================ info("调用getRpcUserInfo"); try { //调用用户服务中的用户详情方法 $res = $this->userRpcServiceInterface->userInfo($userId); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 用户积分列表 * @param int $page * @param int $pageSize * @return mixed */ public function getRpcUserBonusList(int $page, int $pageSize) { Log::get()->info("调用getRpcUserBonusList"); try { //调用用户服务中的用户积分列表方法 $res = $this->userRpcServiceInterface->userBonusList($page, $pageSize); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 用户储值列表 * @param int $page * @param int $pageSize * @return mixed */ public function getRpcUserStoredList(int $page, int $pageSize) { Log::get()->info("调用getRpcUserStoredList"); try { //调用用户服务中的用户储值列表方法 $res = $this->userRpcServiceInterface->userStoredList($page, $pageSize); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } /** * 测试服务降级 */ public function testCircuitBreaker() { Log::get()->info("调用testCircuitBreaker"); Coroutine::sleep(2); } /** * 投递用户消息到RabbitMQ * @param int $page * @param int $pageSize * @return mixed */ public function getRpcUserRabbitMQ() { Log::get()->info("调用getRpcUserRabbitMQ"); try { //调用用户服务中的投递用户消息到RabbitMQ方法 $res = $this->userRpcServiceInterface->userRabbitMQ(); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败", [ 'code' => $ex->getCode(), 'file' => $ex->getFile(), 'message' => $ex->getMessage(), ]); throw new ServiceException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败", [ 'code' => $res['code'], 'file' => '', 'message' => $res['message'] ]); throw new ServiceException(430); } return $res['data']; } } ================================================ FILE: api-gateway/bin/hyperf.php ================================================ #!/usr/bin/env php get(Hyperf\Contract\ApplicationInterface::class); $application->run(); })(); ================================================ FILE: api-gateway/composer.json ================================================ { "name": "hyperf/hyperf-skeleton", "type": "project", "keywords": [ "php", "swoole", "framework", "hyperf", "microservice", "middleware" ], "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", "license": "Apache-2.0", "require": { "php": ">=8.0", "hyperf/async-queue": "3.0.*", "hyperf/cache": "3.0.*", "hyperf/circuit-breaker": "3.0.*", "hyperf/code-generator": "^0.3.3", "hyperf/command": "3.0.*", "hyperf/config": "3.0.*", "hyperf/config-nacos": "3.0.*", "hyperf/constants": "3.0.*", "hyperf/database": "3.0.*", "hyperf/db-connection": "3.0.*", "hyperf/framework": "3.0.*", "hyperf/guzzle": "3.0.*", "hyperf/http-server": "3.0.*", "hyperf/json-rpc": "3.0.*", "hyperf/logger": "3.0.*", "hyperf/memory": "3.0.*", "hyperf/model-cache": "3.0.*", "hyperf/process": "3.0.*", "hyperf/rate-limit": "3.0.*", "hyperf/redis": "3.0.*", "hyperf/rpc": "3.0.*", "hyperf/rpc-client": "3.0.*", "hyperf/rpc-server": "3.0.*", "hyperf/service-governance": "3.0.*", "hyperf/service-governance-nacos": "3.0.*", "hyperf/tracer": "3.0.*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "hyperf/devtool": "3.0.*", "hyperf/ide-helper": "3.0.*", "hyperf/testing": "3.0.*", "hyperf/watcher": "3.0.*", "mockery/mockery": "^1.0", "phpstan/phpstan": "^0.12", "swoole/ide-helper": "^4.5" }, "suggest": { "ext-openssl": "Required to use HTTPS.", "ext-json": "Required to use JSON.", "ext-pdo": "Required to use MySQL Client.", "ext-pdo_mysql": "Required to use MySQL Client.", "ext-redis": "Required to use Redis Client." }, "autoload": { "psr-4": { "App\\": "app/" }, "files": [ "app/Kernel/Functions.php" ] }, "autoload-dev": { "psr-4": { "HyperfTest\\": "./test/" } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "optimize-autoloader": true, "sort-packages": true }, "extra": [], "scripts": { "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-autoload-dump": [ "rm -rf runtime/container" ], "test": "co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always", "cs-fix": "php-cs-fixer fix $1", "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", "start": [ "Composer\\Config::disableProcessTimeout", "php ./bin/hyperf.php start" ] } } ================================================ FILE: api-gateway/config/autoload/annotations.php ================================================ [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], ], ]; ================================================ FILE: api-gateway/config/autoload/aspects.php ================================================ [ 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class, 'redis' => [ 'pool' => 'default', ], 'channel' => '{queue}', 'timeout' => 2, 'retry_seconds' => 5, 'handle_timeout' => 10, 'processes' => 1, 'concurrent' => [ 'limit' => 10, ], ], ]; ================================================ FILE: api-gateway/config/autoload/cache.php ================================================ [ 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'c:', ], ]; ================================================ FILE: api-gateway/config/autoload/commands.php ================================================ (bool) env('CONFIG_CENTER_ENABLE', true), 'driver' => env('CONFIG_CENTER_DRIVER', 'nacos'), 'mode' => env('CONFIG_CENTER_MODE', Mode::PROCESS), 'drivers' => [ 'nacos' => [ 'driver' => Hyperf\ConfigNacos\NacosDriver::class, 'merge_mode' => Hyperf\ConfigNacos\Constants::CONFIG_MERGE_OVERWRITE, 'interval' => 3, 'default_key' => 'nacos_config', 'listener_config' => [ // dataId, group, tenant, type, content 'nacos_config' => [ // 命名空间/ID 'tenant' => env('NACOS_TENANT'), // corresponding with service.namespaceId // DataID 'data_id' => env('NACOS_DATA_ID'), // 组名 'group' => 'DEFAULT_GROUP', 'type' => 'json', ], ], 'client' => [ // 客户端 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ], ], ], ]; ================================================ FILE: api-gateway/config/autoload/databases.php ================================================ [ 'driver' => env('DB_DRIVER', 'mysql'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'hyperf'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => env('DB_PREFIX', ''), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), ], 'cache' => [ 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, 'cache_key' => '{mc:%s:m:%s}:%s:%s', 'prefix' => 'default', 'ttl' => 3600 * 24, 'empty_model_ttl' => 600, 'load_script' => true, ], 'commands' => [ 'gen:model' => [ 'path' => 'app/Model', 'force_casts' => true, 'inheritance' => 'Model', 'uses' => '', 'table_mapping' => [], ], ], ], ]; ================================================ FILE: api-gateway/config/autoload/dependencies.php ================================================ [ 'amqp' => [ 'consumer' => [ 'namespace' => 'App\\Amqp\\Consumer', ], 'producer' => [ 'namespace' => 'App\\Amqp\\Producer', ], ], 'aspect' => [ 'namespace' => 'App\\Aspect', ], 'command' => [ 'namespace' => 'App\\Command', ], 'controller' => [ 'namespace' => 'App\\Controller', ], 'job' => [ 'namespace' => 'App\\Job', ], 'listener' => [ 'namespace' => 'App\\Listener', ], 'middleware' => [ 'namespace' => 'App\\Middleware', ], 'Process' => [ 'namespace' => 'App\\Processes', ], ], ]; ================================================ FILE: api-gateway/config/autoload/exceptions.php ================================================ [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, App\Exception\Handler\AppExceptionHandler::class, App\Exception\Handler\AppServiceExceptionHandler::class, App\Exception\Handler\RateLimitExceptionHandler::class, ], ], ]; ================================================ FILE: api-gateway/config/autoload/listeners.php ================================================ [ 'handler' => [ 'class' => Monolog\Handler\RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => 'Y-m-d H:i:s', 'allowInlineLineBreaks' => true, ], ], ], ]; ================================================ FILE: api-gateway/config/autoload/middlewares.php ================================================ [ \Hyperf\Tracer\Middleware\TraceMiddleware::class, // \App\Exception\Handler\AppServiceExceptionHandler::class, ], 'jsonrpc-http' => [ \Hyperf\Tracer\Middleware\TraceMiddleware::class, ], ]; ================================================ FILE: api-gateway/config/autoload/nacos.php ================================================ '', // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // The nacos account info 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ]; ================================================ FILE: api-gateway/config/autoload/opentracing.php ================================================ env('TRACER_DRIVER', 'zipkin'), 'enable' => [ 'guzzle' => env('TRACER_ENABLE_GUZZLE', false), 'redis' => env('TRACER_ENABLE_REDIS', false), 'db' => env('TRACER_ENABLE_DB', false), 'method' => env('TRACER_ENABLE_METHOD', false), ], 'tracer' => [ 'zipkin' => [ 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'options' => [ 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], 'sampler' => BinarySampler::createAsAlwaysSample(), ], 'jaeger' => [ 'driver' => \Hyperf\Tracer\Adapter\JaegerTracerFactory::class, 'name' => env('APP_NAME', 'skeleton'), 'options' => [ 'local_agent' => [ 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'), 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775), ], ], ], ], 'tags' => [ // HTTP 客户端 (Guzzle) 'http_client' => [ 'http.url' => 'http.url', 'http.method' => 'http.method', 'http.status_code' => 'http.status_code', ], // Redis 客户端 'redis' => [ 'arguments' => 'arguments', 'result' => 'result', ], // 数据库客户端 (hyperf/database) 'db' => [ 'db.query' => 'db.query', 'db.statement' => 'db.statement', 'db.query_time' => 'db.query_time', ], ] ]; ================================================ FILE: api-gateway/config/autoload/processes.php ================================================ 1, 'consume' => 1, 'capacity' => 1, 'limitCallback' => [], 'waitTimeout' => 1, ]; ================================================ FILE: api-gateway/config/autoload/redis.php ================================================ [ 'host' => env('REDIS_HOST', 'localhost'), 'auth' => env('REDIS_AUTH', null), 'port' => (int) env('REDIS_PORT', 6379), 'db' => (int) env('REDIS_DB', 0), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), ], ], ]; ================================================ FILE: api-gateway/config/autoload/server.php ================================================ SWOOLE_PROCESS, 'servers' => [ [ 'name' => 'http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9501, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], ], ], ], 'settings' => [ Constant::OPTION_ENABLE_COROUTINE => true, Constant::OPTION_WORKER_NUM => swoole_cpu_num(), Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', Constant::OPTION_OPEN_TCP_NODELAY => true, Constant::OPTION_MAX_COROUTINE => 100000, Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, Constant::OPTION_MAX_REQUEST => 100000, Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, ], 'callbacks' => [ Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], ], ]; ================================================ FILE: api-gateway/config/autoload/services.php ================================================ 'nacos', 'address' => 'http://'.env('NACOS_HOST').':'.env('NACOS_PORT'), ]; // 这里配置需要用的rpc服务 $services = [ [ 'name' => 'OrderRpcService', 'service' => \App\JsonRpc\OrderRpcServiceInterface::class ], [ 'name' => 'UserRpcService', 'service' => \App\JsonRpc\UserRpcServiceInterface::class ], [ 'name' => 'FileRpcService', 'service' => \App\JsonRpc\FileRpcServiceInterface::class ] ]; return [ 'enable' => [ // 开启服务发现 'discovery' => true, // 开启服务注册 'register' => true, ], // 服务消费者相关配置 'consumers' => value(function () use ($services, $registry) { // 循环生成rpc消费端 $consumers = []; foreach ($services as $value) { $consumers[] = [ 'name' => $value['name'], 'service' => $value['service'], 'registry' => $registry ]; } return $consumers; }), // 服务提供者相关配置 'providers' => [], // 服务驱动相关配置 'drivers' => [ // nacos 配置,当前使用 'nacos' => [ // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // nacos 账号密码信息 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], // 命名空间,public为默认系统空间 'group_name' => 'api', // 命名空间ID 'namespace_id' => env('NACOS_TENANT'), // 心跳检查秒数 'heartbeat' => 5, 'ephemeral' => true, // 是否注册临时实例 ], ], ]; ================================================ FILE: api-gateway/config/autoload/tracer.php ================================================ env('TRACER_DRIVER', 'jaeger'), // 这里的代码演示不对 enable 内的配置进行展开 'enable' => [], 'tracer' => [ // Zipkin 驱动配置 'zipkin' => [ // 当前应用的配置 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // 如果 ipv6 和 ipv6 为空组件会自动从 Server 中检测 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'options' => [ // Zipkin 服务的 endpoint 地址 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), // 请求超时秒数 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], // 采样器,默认为所有请求的都追踪 'sampler' => \Zipkin\Samplers\BinarySampler::createAsAlwaysSample(), ], ], ]; ================================================ FILE: api-gateway/config/autoload/watcher.php ================================================ ScanFileDriver::class, 'bin' => 'php', 'watch' => [ 'dir' => ['app', 'config'], 'file' => ['.env'], 'scan_interval' => 2000, ], ]; ================================================ FILE: api-gateway/config/config.php ================================================ env('APP_NAME', 'skeleton'), 'app_env' => env('APP_ENV', 'dev'), 'scan_cacheable' => env('SCAN_CACHEABLE', false), StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, LogLevel::CRITICAL, // LogLevel::DEBUG, LogLevel::EMERGENCY, LogLevel::ERROR, LogLevel::INFO, LogLevel::NOTICE, LogLevel::WARNING, ], ], ]; ================================================ FILE: api-gateway/config/container.php ================================================ assertTrue(true); $this->assertTrue(is_array($this->get('/'))); } } ================================================ FILE: api-gateway/test/HttpTestCase.php ================================================ client = make(Client::class); } public function __call($name, $arguments) { return $this->client->{$name}(...$arguments); } } ================================================ FILE: api-gateway/test/bootstrap.php ================================================ get(Hyperf\Contract\ApplicationInterface::class); ================================================ FILE: doc/DEPLOY_ONE.md ================================================ # docker安装 Mysql ```bash 1.安装 - 略 2.新建order-srv数据库 3.新建user-srv数据库 ``` Redis ```bash 1.安装 - 略 ``` Nacos ```bash 1.docker run --name nacos-standalone -e MODE=standalone \ -e JVM_XMS=512m -e JVM_XMN=256m -p 8848:8848 -d nacos/nacos-server:v2.1.0 2.访问http://127.0.0.1:8848/nacos/#/login 3.用户名密码: nacos/nacos 4.命名空间->新建命名空间->增加空间 `dev` 5.配置管理->配置列表->dev->导入配置->/jin-microservices/doc/nacos_config.zip ``` Rabbitmq ```bash 1.docker run -d --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.9.2 2.docker exec -it rabbit /bin/bash 3.rabbitmq-plugins enable rabbitmq_management 4.访问http://127.0.0.1:15672 5.用户名密码: guest/guest ``` ELK ```bash 1.vi /etc/sysctl.conf #末尾添加一行 vm.max_map_count=262144 #查看结果 sysctl -p 2.docker run -p 5601:5601 -p 9200:9200 -p 5044:5044 -itd --name elk sebp/elk:7.17.1 3.docker exec -it elk bin/bash 4.cd /etc/logstash/conf.d 5.vim logstash.conf input { beats { port => 5044 codec => plain { charset => "UTF-8" } } } # 格式化日志 filter { grok { match => [ "message","\[%{TIMESTAMP_ISO8601:logtime}\] %{WORD:env}\.(?[A-Z]{4,5})\: %{GREEDYDATA:msg}}" ] } } output { elasticsearch { action => "index" hosts => ["localhost"] index => "jm-log" } } 6.rm 02-beats-input.conf 10-syslog.conf 11-nginx.conf 30-output.conf 7.docker restart elk 8.测试http://127.0.0.1:9200 9.访问http://127.0.0.1:5601/app/kibana ``` Filebeat ```bash 1.wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.13.3-linux-x86_64.tar.gz 2.tar -xvf filebeat-7.13.3-linux-x86_64.tar.gz 3.vi filebeat.yml filebeat.inputs: - type: log paths: - /jin-microservices/*/runtime/logs/*.log multiline.pattern: '^\[[0-9]{4}-[0-9]{2}-[0-9]{2}' multiline.negate: true multiline.match: after multiline.timeout: 5s scan_frequency: 5s output: # 输出到logstash中,logstash更换为自己的ip logstash: hosts: ["127.0.0.1:5044"] 4. ./filebeat-7.13.3-linux-x86_64/filebeat -e -c filebeat.yml ``` Zipkin ```bash 1.docker run --name zipkin -d -p 9411:9411 openzipkin/zipkin 2.访问 http://127.0.0.1:9411/zipkin/ ``` DTM ```bash 1.docker run -itd --name dtm -p 36789:36789 -p 36790:36790 yedf/dtm:1.14 2.访问http://127.0.0.1:36789 ``` 项目代码 ```bash 1.docker run --name hyperf \ -v /workspace/skeleton:/data/project \ -p 9501:9501 -p 9502:9502 -p 9503:9503 -it -p 9507:9507 -it \ --privileged -u root \ --entrypoint /bin/sh \ hyperf/hyperf:8.1-alpine-v3.15-swoole-v5 2.docker exec -it hyperf /bin/bash 3.cd /data/project/ 4.git clone https://github.com/Double-Jin/jin-microservices.git 5.cd jin-microservices/api-gateway/ composer update 复制.env.example为.env配置 php bin/hyperf.php start 6.cd jin-microservices/user-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php migrate php bin/hyperf.php db:seed php bin/hyperf.php start 7.cd jin-microservices/order-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php migrate php bin/hyperf.php db:seed php bin/hyperf.php start 8.cd jin-microservices/task-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php start 9.cd jin-microservices/file-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php start ``` ================================================ FILE: doc/DEPLOY_TWO.md ================================================ # docker-composer安装 docker-compose ```bash 1.vi /etc/sysctl.conf #末尾添加一行 vm.max_map_count=262144 #查看结果 sysctl -p 2.修改docker-composer.services.hyperf.volumes目录映射地址 3.docker-compose up 4.docker exec -it rabbit /bin/bash 5.rabbitmq-plugins enable rabbitmq_management 6.docker exec -it elk bin/bash 7.cd /etc/logstash/conf.d 8.vim logstash.conf input { beats { port => 5044 codec => plain { charset => "UTF-8" } } } # 格式化日志 filter { grok { match => [ "message","\[%{TIMESTAMP_ISO8601:logtime}\] %{WORD:env}\.(?[A-Z]{4,5})\: %{GREEDYDATA:msg}}" ] } } output { elasticsearch { action => "index" hosts => ["localhost"] index => "jm-log" } } 9.rm 02-beats-input.conf 10-syslog.conf 11-nginx.conf 30-output.conf 10.docker restart elk 11.连接mysql 12.新建order-srv数据库 13.新建user-srv数据库 14.访问http://127.0.0.1:8848/nacos/#/login 15.用户名密码: nacos/nacos 16.命名空间->新建命名空间->增加空间 `dev` 17.配置管理->配置列表->dev->导入配置->/jin-microservices/doc/nacos_config.zip ``` Filebeat ```bash 1.wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.13.3-linux-x86_64.tar.gz 2.tar -xvf filebeat-7.13.3-linux-x86_64.tar.gz 3.vi filebeat.yml filebeat.inputs: - type: log paths: - /jin-microservices/*/runtime/logs/*.log multiline.pattern: '^\[[0-9]{4}-[0-9]{2}-[0-9]{2}' multiline.negate: true multiline.match: after multiline.timeout: 5s scan_frequency: 5s output: # 输出到logstash中,logstash更换为自己的ip logstash: hosts: ["127.0.0.1:5044"] 4. ./filebeat-7.13.3-linux-x86_64/filebeat -e -c filebeat.yml ``` 验证 ```bash 1.访问http://127.0.0.1:8848/nacos/#/login 用户名密码: nacos/nacos 2.访问http://127.0.0.1:15672 用户名密码: guest/guest 2.访问http://127.0.0.1:9200 3.访问http://127.0.0.1:5601/app/kibana 4.访问http://127.0.0.1:9411/zipkin/ 5.访问http://127.0.0.1:36789 ``` 启动服务 ```bash 1.docker exec -it hyperf /bin/bash 2.cd /data/project/ 3.git clone https://github.com/Double-Jin/jin-microservices.git 4.cd jin-microservices/api-gateway/ composer update 复制.env.example为.env配置 php bin/hyperf.php start 5.cd jin-microservices/user-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php migrate php bin/hyperf.php db:seed php bin/hyperf.php start 6.cd jin-microservices/order-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php migrate php bin/hyperf.php db:seed php bin/hyperf.php start 7.cd jin-microservices/task-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php start 8.cd jin-microservices/file-srv/ composer update 复制.env.example为.env配置 php bin/hyperf.php start ``` ================================================ FILE: docker-compose.yaml ================================================ version: '3.9' services: hyperf: container_name: hyperf image: hyperf/hyperf:8.1-alpine-v3.15-swoole-v5 hostname: hyperf restart: always privileged: true ports: - 9501:9501 - 9502:9502 - 9503:9503 - 9507:9507 volumes: - /Users/linjinjin/Documents/dnmp/www:/data/www tty: true networks: - hyperf_networks nacos: container_name: nacos image: nacos/nacos-server:v2.1.0 environment: - MODE=standalone - JVM_XMS=512m - JVM_XMN=256m hostname: nacos restart: always ports: - 8848:8848 networks: - hyperf_networks rabbitmq: container_name: rabbit image: rabbitmq:3.9.2 hostname: rabbit restart: always ports: - 15672:15672 - 5672:5672 networks: - hyperf_networks redis: container_name: redis image: redis:7.0 restart: always hostname: redis ports: - 6379:6379 networks: - hyperf_networks elk: container_name: elk image: sebp/elk:7.17.1 restart: always hostname: elk ports: - 5601:5601 - 9200:9200 - 5044:5044 networks: - hyperf_networks dtm: image: yedf/dtm:1.14 container_name: dtm hostname: dtm restart: always ports: - "36789:36789" - "36790:36790" networks: - hyperf_networks zipkin: image: openzipkin/zipkin:2 container_name: zipkin hostname: zipkin restart: always ports: - "9411:9411" networks: - hyperf_networks networks: hyperf_networks: ================================================ FILE: file-srv/.github/workflows/Dockerfile ================================================ # Default Dockerfile # # @link https://www.hyperf.io # @document https://hyperf.wiki # @contact group@hyperf.io # @license https://github.com/hyperf/hyperf/blob/master/LICENSE FROM hyperf/hyperf:8.0-alpine-v3.16-swoole LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" app.name="Hyperf" ## # ---------- env settings ---------- ## # --build-arg timezone=Asia/Shanghai ARG timezone ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ APP_ENV=prod \ SCAN_CACHEABLE=(true) # update RUN set -ex \ # show php version and extensions && php -v \ && php -m \ && php --ri swoole \ # ---------- some config ---------- && cd /etc/php* \ # - config PHP && { \ echo "upload_max_filesize=128M"; \ echo "post_max_size=128M"; \ echo "memory_limit=1G"; \ echo "date.timezone=${TIMEZONE}"; \ } | tee conf.d/99_overrides.ini \ # - config timezone && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ && echo "${TIMEZONE}" > /etc/timezone \ # ---------- clear works ---------- && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ && echo -e "\033[42;37m Build Completed :).\033[0m\n" WORKDIR /opt/www # Composer Cache # COPY ./composer.* /opt/www/ # RUN composer install --no-dev --no-scripts COPY . /opt/www RUN print "\n" | composer install -o && php bin/hyperf.php EXPOSE 9501 ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] ================================================ FILE: file-srv/.github/workflows/build.yml ================================================ name: Build Docker on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Build run: cp -rf .github/workflows/Dockerfile . && docker build -t hyperf . ================================================ FILE: file-srv/.github/workflows/release.yml ================================================ on: push: # Sequence of patterns matched against refs/tags tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 name: Release jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false prerelease: false ================================================ FILE: file-srv/.gitignore ================================================ .buildpath .settings/ .project *.patch .idea/ .git/ runtime/ vendor/ .phpintel/ .env .DS_Store .phpunit* *.cache ================================================ FILE: file-srv/.watcher.php ================================================ ScanFileDriver::class, 'bin' => 'php', 'watch' => [ 'dir' => ['app', 'config'], 'file' => ['.env'], 'scan_interval' => 2000, ], ]; ================================================ FILE: file-srv/app/Constants/ErrorCode.php ================================================ request->input('user', 'Hyperf'); $method = $this->request->getMethod(); return [ 'method' => $method, 'message' => "Hello {$user}.", ]; } } ================================================ FILE: file-srv/app/Exception/BusinessException.php ================================================ logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); $this->logger->error($throwable->getTraceAsString()); return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: file-srv/app/Exception/JsonRpcException.php ================================================ 200, 'data' => $this->fileService->uploadFile($type,$base64string,$fileName) ]; } } ================================================ FILE: file-srv/app/JsonRpc/FileRpcServiceInterface.php ================================================ logger = $container->get(LoggerFactory::class)->get('sql'); } public function listen(): array { return [ QueryExecuted::class, ]; } /** * @param QueryExecuted $event */ public function process(object $event): void { if ($event instanceof QueryExecuted) { $sql = $event->sql; if (! Arr::isAssoc($event->bindings)) { $position = 0; foreach ($event->bindings as $value) { $position = strpos($sql, '?', $position); if ($position === false) { break; } $value = "'{$value}'"; $sql = substr_replace($sql, $value, $position, 1); $position += strlen($value); } } $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } } } ================================================ FILE: file-srv/app/Listener/ResumeExitCoordinatorListener.php ================================================ resume(); } } ================================================ FILE: file-srv/app/Log.php ================================================ get(LoggerFactory::class)->get($name); } } ================================================ FILE: file-srv/app/Model/Model.php ================================================ info("调用uploadFile"); try { if (!file_exists(config('file.storage.local.root'))){ @mkdir(config('file.storage.local.root'), 0755, true); } $filePath = config('file.storage.local.root').'/'.$fileName; //base64转换成文件 $baseFile = str_replace( 'data:' . $type . ';base64,', '', $base64string); $newFile = fopen( $filePath, "wb" ); fwrite( $newFile, base64_decode( $baseFile) ); fclose( $newFile ); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败"); throw new JsonRpcException(430); } return [ 'file_name' => $fileName, 'file_path' => $filePath ]; } } ================================================ FILE: file-srv/bin/hyperf.php ================================================ #!/usr/bin/env php get(Hyperf\Contract\ApplicationInterface::class); $application->run(); })(); ================================================ FILE: file-srv/composer.json ================================================ { "name": "hyperf/hyperf-skeleton", "type": "project", "keywords": [ "php", "swoole", "framework", "hyperf", "microservice", "middleware" ], "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", "license": "Apache-2.0", "require": { "php": ">=8.0", "hyperf/cache": "3.0.*", "hyperf/command": "3.0.*", "hyperf/config": "3.0.*", "hyperf/config-nacos": "3.0.*", "hyperf/constants": "3.0.*", "hyperf/database": "3.0.*", "hyperf/db-connection": "3.0.*", "hyperf/filesystem": "3.0.*", "hyperf/framework": "3.0.*", "hyperf/guzzle": "3.0.*", "hyperf/http-server": "3.0.*", "hyperf/json-rpc": "3.0.*", "hyperf/logger": "3.0.*", "hyperf/memory": "3.0.*", "hyperf/model-cache": "3.0.*", "hyperf/process": "3.0.*", "hyperf/redis": "3.0.*", "hyperf/rpc": "3.0.*", "hyperf/rpc-client": "3.0.*", "hyperf/rpc-server": "3.0.*", "hyperf/service-governance": "3.0.*", "hyperf/service-governance-nacos": "3.0.*", "hyperf/tracer": "3.0.*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "hyperf/devtool": "3.0.*", "hyperf/testing": "3.0.*", "mockery/mockery": "^1.0", "phpstan/phpstan": "^1.0", "swoole/ide-helper": "^5.0", "hyperf/watcher": "3.0.*" }, "suggest": { "ext-openssl": "Required to use HTTPS.", "ext-json": "Required to use JSON.", "ext-pdo": "Required to use MySQL Client.", "ext-pdo_mysql": "Required to use MySQL Client.", "ext-redis": "Required to use Redis Client." }, "autoload": { "psr-4": { "App\\": "app/" }, "files": [] }, "autoload-dev": { "psr-4": { "HyperfTest\\": "./test/" } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "optimize-autoloader": true, "sort-packages": true }, "extra": [], "scripts": { "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-autoload-dump": [ "rm -rf runtime/container" ], "test": "co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always", "cs-fix": "php-cs-fixer fix $1", "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", "start": [ "Composer\\Config::disableProcessTimeout", "php ./bin/hyperf.php start" ] } } ================================================ FILE: file-srv/config/autoload/annotations.php ================================================ [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], ], ]; ================================================ FILE: file-srv/config/autoload/aspects.php ================================================ [ 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'c:', ], ]; ================================================ FILE: file-srv/config/autoload/commands.php ================================================ (bool) env('CONFIG_CENTER_ENABLE', true), 'driver' => env('CONFIG_CENTER_DRIVER', 'nacos'), 'mode' => env('CONFIG_CENTER_MODE', Mode::PROCESS), 'drivers' => [ 'nacos' => [ 'driver' => Hyperf\ConfigNacos\NacosDriver::class, 'merge_mode' => Hyperf\ConfigNacos\Constants::CONFIG_MERGE_OVERWRITE, 'interval' => 3, 'default_key' => 'nacos_config', 'listener_config' => [ // dataId, group, tenant, type, content 'nacos_config' => [ // 命名空间/ID 'tenant' => env('NACOS_TENANT'), // corresponding with service.namespaceId // DataID 'data_id' => env('NACOS_DATA_ID'), // 组名 'group' => 'DEFAULT_GROUP', 'type' => 'json', ], ], 'client' => [ // 客户端 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ], ], ], ]; ================================================ FILE: file-srv/config/autoload/databases.php ================================================ [ 'driver' => env('DB_DRIVER', 'mysql'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'hyperf'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => env('DB_PREFIX', ''), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), ], 'cache' => [ 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, 'cache_key' => '{mc:%s:m:%s}:%s:%s', 'prefix' => 'default', 'ttl' => 3600 * 24, 'empty_model_ttl' => 600, 'load_script' => true, ], 'commands' => [ 'gen:model' => [ 'path' => 'app/Model', 'force_casts' => true, 'inheritance' => 'Model', 'uses' => '', 'table_mapping' => [], ], ], ], ]; ================================================ FILE: file-srv/config/autoload/dependencies.php ================================================ [ 'amqp' => [ 'consumer' => [ 'namespace' => 'App\\Amqp\\Consumer', ], 'producer' => [ 'namespace' => 'App\\Amqp\\Producer', ], ], 'aspect' => [ 'namespace' => 'App\\Aspect', ], 'command' => [ 'namespace' => 'App\\Command', ], 'controller' => [ 'namespace' => 'App\\Controller', ], 'job' => [ 'namespace' => 'App\\Job', ], 'listener' => [ 'namespace' => 'App\\Listener', ], 'middleware' => [ 'namespace' => 'App\\Middleware', ], 'Process' => [ 'namespace' => 'App\\Processes', ], ], ]; ================================================ FILE: file-srv/config/autoload/exceptions.php ================================================ [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, App\Exception\Handler\AppExceptionHandler::class, ], ], ]; ================================================ FILE: file-srv/config/autoload/file.php ================================================ 'local', 'storage' => [ 'local' => [ 'driver' => \Hyperf\Filesystem\Adapter\LocalAdapterFactory::class, 'root' => __DIR__ . '/../../public/uploads/', ], 'ftp' => [ 'driver' => \Hyperf\Filesystem\Adapter\FtpAdapterFactory::class, 'host' => 'ftp.example.com', 'username' => 'username', 'password' => 'password', // 'port' => 21, // 'root' => '/path/to/root', // 'passive' => true, // 'ssl' => true, // 'timeout' => 30, // 'ignorePassiveAddress' => false, // 'timestampsOnUnixListingsEnabled' => true, ], 'memory' => [ 'driver' => \Hyperf\Filesystem\Adapter\MemoryAdapterFactory::class, ], 's3' => [ 'driver' => \Hyperf\Filesystem\Adapter\S3AdapterFactory::class, 'credentials' => [ 'key' => env('S3_KEY'), 'secret' => env('S3_SECRET'), ], 'region' => env('S3_REGION'), 'version' => 'latest', 'bucket_endpoint' => false, 'use_path_style_endpoint' => false, 'endpoint' => env('S3_ENDPOINT'), 'bucket_name' => env('S3_BUCKET'), ], 'minio' => [ 'driver' => \Hyperf\Filesystem\Adapter\S3AdapterFactory::class, 'credentials' => [ 'key' => env('S3_KEY'), 'secret' => env('S3_SECRET'), ], 'region' => env('S3_REGION'), 'version' => 'latest', 'bucket_endpoint' => false, 'use_path_style_endpoint' => true, 'endpoint' => env('S3_ENDPOINT'), 'bucket_name' => env('S3_BUCKET'), ], 'oss' => [ 'driver' => \Hyperf\Filesystem\Adapter\AliyunOssAdapterFactory::class, 'accessId' => env('OSS_ACCESS_ID'), 'accessSecret' => env('OSS_ACCESS_SECRET'), 'bucket' => env('OSS_BUCKET'), 'endpoint' => env('OSS_ENDPOINT'), // 'timeout' => 3600, // 'connectTimeout' => 10, // 'isCName' => false, // 'token' => null, // 'proxy' => null, ], 'qiniu' => [ 'driver' => \Hyperf\Filesystem\Adapter\QiniuAdapterFactory::class, 'accessKey' => env('QINIU_ACCESS_KEY'), 'secretKey' => env('QINIU_SECRET_KEY'), 'bucket' => env('QINIU_BUCKET'), 'domain' => env('QINBIU_DOMAIN'), ], 'cos' => [ 'driver' => \Hyperf\Filesystem\Adapter\CosAdapterFactory::class, 'region' => env('COS_REGION'), 'app_id' => env('COS_APPID'), 'secret_id' => env('COS_SECRET_ID'), 'secret_key' => env('COS_SECRET_KEY'), // 可选,如果 bucket 为私有访问请打开此项 // 'signed_url' => false, 'bucket' => env('COS_BUCKET'), 'read_from_cdn' => false, // 'timeout' => 60, // 'connect_timeout' => 60, // 'cdn' => '', // 'scheme' => 'https', ], ], ]; ================================================ FILE: file-srv/config/autoload/listeners.php ================================================ [ 'handler' => [ 'class' => Monolog\Handler\StreamHandler::class, 'constructor' => [ 'stream' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => 'Y-m-d H:i:s', 'allowInlineLineBreaks' => true, ], ], ], ]; ================================================ FILE: file-srv/config/autoload/middlewares.php ================================================ [ ], ]; ================================================ FILE: file-srv/config/autoload/nacos.php ================================================ '', // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // The nacos account info 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ]; ================================================ FILE: file-srv/config/autoload/opentracing.php ================================================ env('TRACER_DRIVER', 'zipkin'), 'enable' => [ 'guzzle' => env('TRACER_ENABLE_GUZZLE', false), 'redis' => env('TRACER_ENABLE_REDIS', false), 'db' => env('TRACER_ENABLE_DB', false), 'method' => env('TRACER_ENABLE_METHOD', false), ], 'tracer' => [ 'zipkin' => [ 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'options' => [ 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], 'sampler' => BinarySampler::createAsAlwaysSample(), ], 'jaeger' => [ 'driver' => \Hyperf\Tracer\Adapter\JaegerTracerFactory::class, 'name' => env('APP_NAME', 'skeleton'), 'options' => [ 'local_agent' => [ 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'), 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775), ], ], ], ], 'tags' => [ // HTTP 客户端 (Guzzle) 'http_client' => [ 'http.url' => 'http.url', 'http.method' => 'http.method', 'http.status_code' => 'http.status_code', ], // Redis 客户端 'redis' => [ 'arguments' => 'arguments', 'result' => 'result', ], // 数据库客户端 (hyperf/database) 'db' => [ 'db.query' => 'db.query', 'db.statement' => 'db.statement', 'db.query_time' => 'db.query_time', ], ] ]; ================================================ FILE: file-srv/config/autoload/processes.php ================================================ [ 'host' => env('REDIS_HOST', 'localhost'), 'auth' => env('REDIS_AUTH', null), 'port' => (int) env('REDIS_PORT', 6379), 'db' => (int) env('REDIS_DB', 0), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), ], ], ]; ================================================ FILE: file-srv/config/autoload/server.php ================================================ SWOOLE_PROCESS, 'servers' => [ [ 'name' => 'jsonrpc-http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9507, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [\Hyperf\JsonRpc\HttpServer::class, 'onRequest'], ], 'settings' => [ 'open_length_check' => true, 'package_length_type' => 'N', 'package_length_offset' => 0, 'package_body_offset' => 4, 'package_max_length' => 1024 * 1024 * 20, ], ], ], 'settings' => [ Constant::OPTION_ENABLE_COROUTINE => true, Constant::OPTION_WORKER_NUM => swoole_cpu_num(), Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', Constant::OPTION_OPEN_TCP_NODELAY => true, Constant::OPTION_MAX_COROUTINE => 100000, Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, Constant::OPTION_MAX_REQUEST => 100000, Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, ], 'callbacks' => [ Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], ], ]; ================================================ FILE: file-srv/config/autoload/services.php ================================================ [ // 开启服务发现 'discovery' => true, // 开启服务注册 'register' => true, ], // 服务消费者相关配置 'consumers' => [], // 服务提供者相关配置 'providers' => [], // 服务驱动相关配置 'drivers' => [ // nacos 配置,当前使用 'nacos' => [ // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // nacos 账号密码信息 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], // 命名空间,public为默认系统空间 'group_name' => 'api', // 命名空间ID 'namespace_id' => env('NACOS_TENANT'), // 心跳检查秒数 'heartbeat' => 5, 'ephemeral' => true, // 是否注册临时实例 ], ], ]; ================================================ FILE: file-srv/config/config.php ================================================ env('APP_NAME', 'skeleton'), 'app_env' => env('APP_ENV', 'dev'), 'scan_cacheable' => env('SCAN_CACHEABLE', false), StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, LogLevel::CRITICAL, // LogLevel::DEBUG, LogLevel::EMERGENCY, LogLevel::ERROR, LogLevel::INFO, LogLevel::NOTICE, LogLevel::WARNING, ], ], ]; ================================================ FILE: file-srv/config/container.php ================================================ assertTrue(true); $this->assertTrue(is_array($this->get('/'))); } } ================================================ FILE: file-srv/test/HttpTestCase.php ================================================ client = make(Client::class); } public function __call($name, $arguments) { return $this->client->{$name}(...$arguments); } } ================================================ FILE: file-srv/test/bootstrap.php ================================================ get(Hyperf\Contract\ApplicationInterface::class); ================================================ FILE: order-srv/app/Amqp/Producer/OrderProducer.php ================================================ payload = $data; } } ================================================ FILE: order-srv/app/Constants/ErrorCode.php ================================================ orderService->sageCreateOrder($request->all()); return [ 'dtm_result' => 'SUCCESS', ]; } /** * 订单创建成功补偿 * @param RequestInterface $request * @return string[] */ #[PostMapping(path: 'sageCreateOrderCompensate')] public function sageCreateOrderCompensate(RequestInterface $request): array { //调用orderService.sageCreateOrderCompensate方法 $this->orderService->sageCreateOrderCompensate($request->all()); return [ 'dtm_result' => 'SUCCESS', ]; } } ================================================ FILE: order-srv/app/Exception/Handler/DtmExceptionHandler.php ================================================ stopPropagation(); return $response->withStatus(409); } // 交给下一个异常处理器 return $response; } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: order-srv/app/Exception/Handler/JsonRpcExceptionHandler.php ================================================ stopPropagation(); $request = Context::get(ServerRequestInterface::class); $jsonRpcTreaty = $request->getAttribute('data'); //返回自定义错误数据 // return di(ResponseBuilder::class)->buildResponse($request,$response); $result = responseError($throwable->getCode(), $throwable->getMessage()); $jsonRpc = [ "jsonrpc" => $jsonRpcTreaty['jsonrpc'], "id" => $jsonRpcTreaty['id'], "result" => $result, "context" => '' ]; return $response->withStatus($throwable->getCode()) ->withAddedHeader('content-type', 'application/json; charset=utf-8') ->withBody(new SwooleStream(json_encode($jsonRpc, JSON_UNESCAPED_UNICODE))); } // 交给下一个异常处理器 return $response; } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: order-srv/app/Exception/JsonRpcException.php ================================================ 200, 'data' => $this->orderService->orderList($userId) ]; } /** * 创建订单 * @param array $data * @return array */ public function createOrder(array $data): array { return [ 'code' => 200, 'data' => $this->orderService->createOrder($data) ]; } /** * 投递订单消息到RabbitMQ * @return array */ public function orderRabbitMQ() : array { return [ 'code' => 200, 'data' => $this->orderService->orderRabbitMQ() ]; } } ================================================ FILE: order-srv/app/JsonRpc/OrderRpcServiceInterface.php ================================================ get($id); } return $container; } } /** * 控制台日志 */ if (!function_exists('stdLog')) { function stdLog() { return di()->get(StdoutLoggerInterface::class); } } /** * 文件日志 */ if (!function_exists('logger')) { function logger($name = 'log', $group = 'default') { return di()->get(LoggerFactory::class)->get($name, $group); } } /** * redis 客户端实例 */ if (!function_exists('redis')) { function redis() { return di()->get(Redis::class); } } /** * 缓存实例 简单的缓存 */ if (!function_exists('cache')) { function cache() { return di()->get(\Psr\SimpleCache\CacheInterface::class); } } if (!function_exists('format_throwable')) { /** * Format a throwable to string. * @param Throwable $throwable * @return string */ function format_throwable(Throwable $throwable): string { return di()->get(FormatterInterface::class)->format($throwable); } } if (!function_exists('responseSuccess')) { function responseSuccess($code, $message = '', $data = []) { $content = ['code' => $code]; $message ? $content['msg'] = $message : $content['msg'] = \App\Constants\ResponseCode::getMessage($code); $data ? $content['data'] = $data : $content['data'] = []; return $content; } } if (!function_exists('responseError')) { function responseError($code, $message = '', $data = []) { $content = ['code' => $code]; $data ? $content['data'] = $data : $content['data'] = []; return $content; } } /** * 判读字符串是否为json */ if (!function_exists('isJson')) { /** * Finds an entry of the container by its identifier and returns it. * @param string $string */ function isJson($string) { json_decode($string); return (json_last_error() == JSON_ERROR_NONE); } } /** * 返回jsonRpc结构 */ if (!function_exists('successJsonRpc')) { function successJsonRpc($code, $data) { return [ 'code' => $code, 'data' => $data ]; } } ================================================ FILE: order-srv/app/Listener/DbQueryExecutedListener.php ================================================ logger = $container->get(LoggerFactory::class)->get('sql'); } public function listen() : array { return [QueryExecuted::class]; } /** * @param QueryExecuted $event */ public function process(object $event) : void { if ($event instanceof QueryExecuted) { $sql = $event->sql; if (!Arr::isAssoc($event->bindings)) { foreach ($event->bindings as $key => $value) { $sql = Str::replaceFirst('?', "'{$value}'", $sql); } } $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } } } ================================================ FILE: order-srv/app/Log.php ================================================ get(LoggerFactory::class)->get($name); } } ================================================ FILE: order-srv/app/Model/Order.php ================================================ 'integer']; public function orderGoods() { return $this->hasMany(OrderGoods::class, 'order_no', 'order_no'); } } ================================================ FILE: order-srv/app/Model/OrderGoods.php ================================================ 'integer']; } ================================================ FILE: order-srv/app/Services/OrderService.php ================================================ info("调用orderList"); $list = Order::query() ->where('user_id', $userId) ->get(); if (empty($list)) { Log::get()->info("订单为空"); throw new JsonRpcException(10001); } //调用用户服务拿到用户信息 foreach ($list as $item) { try { //调用用户服务中的用户详情方法 $res = $this->userRpcServiceInterface->userInfo($item->user_id); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败"); throw new JsonRpcException(430); } if ($res['code'] !== 200) { Log::get()->info("rpc调用失败"); throw new JsonRpcException(430); } //拼装数据 $item->user = $res['data']; } return $list->toArray(); } /** * 创建订单 * @param $data * @return string */ public function createOrder($data) { Log::get()->info("调用createOrder"); try { //分布式事务 $data['order_no'] = date("YmdHis"); //获取用户储值 //todo //判断商品库存 //todo $this->saga->init(); //创建订单 $this->saga->add( env('DTM_ORDER_URL') . '/saga/sageCreateOrder', env('DTM_ORDER_URL') . '/saga/sageCreateOrderCompensate', $data ); //扣用户余额 $this->saga->add( env('DTM_USER_URL') . '/saga/changeStored', env('DTM_USER_URL'). '/saga/changeStoredCompensate', ['order_no'=>$data['order_no'] ,'user_id'=>$data['user_id'],'amount'=>-$data['order_fact_money']] ); // 提交 Saga 事务 $this->saga->submit(); } catch (\Throwable $ex) { Log::get()->info("rpc调用失败"); throw new JsonRpcException(430); } return TransContext::getGid(); } /** * SAGA订单创建成功 * @param $data * @return string */ public function sageCreateOrder($data) { Log::get()->info("分布式事务-sageCreateOrder",$data); //分布式事务 try { Order::create([ 'order_no' => $data['order_no'], 'user_id' => $data['user_id'], 'coupon_id' => $data['coupon_id'], 'order_money' => $data['order_money'], 'order_discount' => $data['order_fact_money'], 'consume_number' => $data['consume_number'], 'order_status' => 0, 'payment' => $data['payment'], ]); foreach ($data['goods'] as $item) { OrderGoods::create([ 'order_no' => $data['order_no'], 'goods_id' => $item['goods_id'], 'goods_sn' => $item['goods_sn'], 'sku_id' => $item['sku_id'], 'user_id' => $item['user_id'], 'goods_name' => $item['goods_name'], 'number' => $item['number'], 'goods_tag_price' => $item['goods_tag_price'], 'goods_real_price' => $item['goods_real_price'], 'goods_discount' => $item['goods_fact_money'], ]); } } catch (\Throwable $e) { Log::get()->info("分布式事务-sageCreateOrder-调用失败"); throw new ServiceException(); } return true; } /** * SAGA订单创建补偿 * @param $data * @return string */ public function sageCreateOrderCompensate($data) { Log::get()->info("分布式事务-sageCreateOrderCompensate",$data); //分布式事务 try { Order::query() ->where('order_no',$data['order_no']) ->where('user_id',$data['user_id']) ->delete(); OrderGoods::query() ->where('order_no',$data['order_no']) ->where('user_id',$data['user_id']) ->delete(); } catch (\Throwable $e) { Log::get()->info("分布式事务-sageCreateOrderCompensate-调用失败"); throw new ServiceException(); } return true; } /** * 投递订单消息到RabbitMQ */ public function orderRabbitMQ() { //拼装数据 $message = new OrderProducer([ 'id' => 1 ]); $producer = di()->get(Producer::class); //投递消息 $result = $producer->produce($message); //投递消息失败 if ($result != true){ throw new JsonRpcException(430); } } } ================================================ FILE: order-srv/bin/hyperf.php ================================================ #!/usr/bin/env php get(Hyperf\Contract\ApplicationInterface::class); $application->run(); })(); ================================================ FILE: order-srv/composer.json ================================================ { "name": "hyperf/hyperf-skeleton", "type": "project", "keywords": [ "php", "swoole", "framework", "hyperf", "microservice", "middleware" ], "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", "license": "Apache-2.0", "require": { "php": ">=8.0", "dtm/dtm-client": "^0.3.0", "hyperf/amqp": "3.0.*", "hyperf/cache": "3.0.*", "hyperf/command": "3.0.*", "hyperf/config": "3.0.*", "hyperf/config-nacos": "3.0.*", "hyperf/constants": "3.0.*", "hyperf/database": "3.0.*", "hyperf/db-connection": "3.0.*", "hyperf/framework": "3.0.*", "hyperf/guzzle": "3.0.*", "hyperf/http-server": "3.0.*", "hyperf/json-rpc": "3.0.*", "hyperf/logger": "3.0.*", "hyperf/memory": "3.0.*", "hyperf/model-cache": "3.0.*", "hyperf/paginator": "3.0.*", "hyperf/process": "3.0.*", "hyperf/redis": "3.0.*", "hyperf/rpc": "3.0.*", "hyperf/rpc-client": "3.0.*", "hyperf/rpc-server": "3.0.*", "hyperf/service-governance": "3.0.*", "hyperf/service-governance-nacos": "3.0.*", "hyperf/tracer": "3.0.*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "hyperf/devtool": "3.0.*", "hyperf/ide-helper": "3.0.*", "hyperf/testing": "3.0.*", "hyperf/watcher": "3.0.*", "mockery/mockery": "^1.0", "phpstan/phpstan": "^0.12", "swoole/ide-helper": "^4.5" }, "suggest": { "ext-openssl": "Required to use HTTPS.", "ext-json": "Required to use JSON.", "ext-pdo": "Required to use MySQL Client.", "ext-pdo_mysql": "Required to use MySQL Client.", "ext-redis": "Required to use Redis Client." }, "autoload": { "psr-4": { "App\\": "app/" }, "files": [ "app/Kernel/Functions.php" ] }, "autoload-dev": { "psr-4": { "HyperfTest\\": "./test/" } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "optimize-autoloader": true, "sort-packages": true }, "extra": [], "scripts": { "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-autoload-dump": [ "rm -rf runtime/container" ], "test": "co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always", "cs-fix": "php-cs-fixer fix $1", "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", "start": [ "Composer\\Config::disableProcessTimeout", "php ./bin/hyperf.php start" ] } } ================================================ FILE: order-srv/config/autoload/amqp.php ================================================ [ 'host' => env('AMQP_HOST', 'localhost'), 'port' => (int) env('AMQP_PORT', 5672), 'user' => env('AMQP_USER', 'guest'), 'password' => env('AMQP_PASSWORD', 'guest'), 'vhost' => env('AMQP_VHOST', '/'), 'open_ssl' => false, 'concurrent' => [ 'limit' => 1, ], 'pool' => [ 'connections' => 2, ], 'params' => [ 'insist' => false, 'login_method' => 'AMQPLAIN', 'login_response' => null, 'locale' => 'en_US', 'connection_timeout' => 3, 'read_write_timeout' => 6, 'context' => null, 'keepalive' => true, 'heartbeat' => 3, 'channel_rpc_timeout' => 0.0, 'close_on_destruct' => false, 'max_idle_channels' => 10, ], ], ]; ================================================ FILE: order-srv/config/autoload/annotations.php ================================================ [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], ], ]; ================================================ FILE: order-srv/config/autoload/aspects.php ================================================ [ 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'c:', ], ]; ================================================ FILE: order-srv/config/autoload/commands.php ================================================ (bool) env('CONFIG_CENTER_ENABLE', true), 'driver' => env('CONFIG_CENTER_DRIVER', 'nacos'), 'mode' => env('CONFIG_CENTER_MODE', Mode::PROCESS), 'drivers' => [ 'nacos' => [ 'driver' => Hyperf\ConfigNacos\NacosDriver::class, 'merge_mode' => Hyperf\ConfigNacos\Constants::CONFIG_MERGE_OVERWRITE, 'interval' => 3, 'default_key' => 'nacos_config', 'listener_config' => [ // dataId, group, tenant, type, content 'nacos_config' => [ // 命名空间/ID 'tenant' => env('NACOS_TENANT'), // corresponding with service.namespaceId // DataID 'data_id' => env('NACOS_DATA_ID'), // 组名 'group' => 'DEFAULT_GROUP', 'type' => 'json', ], ], 'client' => [ // 客户端 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ], ], ], ]; ================================================ FILE: order-srv/config/autoload/databases.php ================================================ [ 'driver' => env('DB_DRIVER', 'mysql'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'hyperf'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => env('DB_PREFIX', ''), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), ], // 'cache' => [ // 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, // 'cache_key' => '{mc:%s:m:%s}:%s:%s', // 'prefix' => 'default', // 'ttl' => 3600 * 24, // 'empty_model_ttl' => 600, // 'load_script' => true, // ], 'commands' => [ 'gen:model' => [ 'path' => 'app/Model', 'force_casts' => true, 'inheritance' => 'Model', 'uses' => '', 'table_mapping' => [], ], ], ], ]; ================================================ FILE: order-srv/config/autoload/dependencies.php ================================================ [ 'amqp' => [ 'consumer' => [ 'namespace' => 'App\\Amqp\\Consumer', ], 'producer' => [ 'namespace' => 'App\\Amqp\\Producer', ], ], 'aspect' => [ 'namespace' => 'App\\Aspect', ], 'command' => [ 'namespace' => 'App\\Command', ], 'controller' => [ 'namespace' => 'App\\Controller', ], 'job' => [ 'namespace' => 'App\\Job', ], 'listener' => [ 'namespace' => 'App\\Listener', ], 'middleware' => [ 'namespace' => 'App\\Middleware', ], 'Process' => [ 'namespace' => 'App\\Processes', ], ], ]; ================================================ FILE: order-srv/config/autoload/dtm.php ================================================ Protocol::HTTP, 'server' => env('DTM_HOST'), 'port' => [ 'http' => 36789, 'grpc' => 36790, ], 'barrier' => [ // DB 模式下的子事务屏障配置 'db' => [ 'type' => DbType::MySQL ], // Redis 模式下的子事务屏障配置 'redis' => [ // 子事务屏障记录的超时时间 'expire_seconds' => 7 * 86400, ], 'apply' => [], ], 'guzzle' => [ 'options' => [], ], ]; ================================================ FILE: order-srv/config/autoload/exceptions.php ================================================ [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, App\Exception\Handler\DtmExceptionHandler::class, ], 'jsonrpc-http' => [ App\Exception\Handler\JsonRpcExceptionHandler::class, ] ], ]; ================================================ FILE: order-srv/config/autoload/listeners.php ================================================ [ 'handler' => [ 'class' => Monolog\Handler\RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => 'Y-m-d H:i:s', 'allowInlineLineBreaks' => true, ], ], ], ]; ================================================ FILE: order-srv/config/autoload/middlewares.php ================================================ [ \DtmClient\Middleware\DtmMiddleware::class, ], 'jsonrpc-http' => [ \Hyperf\Tracer\Middleware\TraceMiddleware::class, \DtmClient\Middleware\DtmMiddleware::class, ], ]; ================================================ FILE: order-srv/config/autoload/nacos.php ================================================ '', // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // The nacos account info 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ]; ================================================ FILE: order-srv/config/autoload/opentracing.php ================================================ env('TRACER_DRIVER', 'zipkin'), 'enable' => [ 'guzzle' => env('TRACER_ENABLE_GUZZLE', false), 'redis' => env('TRACER_ENABLE_REDIS', false), 'db' => env('TRACER_ENABLE_DB', false), 'method' => env('TRACER_ENABLE_METHOD', false), ], 'tracer' => [ 'zipkin' => [ 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'options' => [ 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], 'sampler' => BinarySampler::createAsAlwaysSample(), ], 'jaeger' => [ 'driver' => \Hyperf\Tracer\Adapter\JaegerTracerFactory::class, 'name' => env('APP_NAME', 'skeleton'), 'options' => [ 'local_agent' => [ 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'), 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775), ], ], ], ], 'tags' => [ // HTTP 客户端 (Guzzle) 'http_client' => [ 'http.url' => 'http.url', 'http.method' => 'http.method', 'http.status_code' => 'http.status_code', ], // Redis 客户端 'redis' => [ 'arguments' => 'arguments', 'result' => 'result', ], // 数据库客户端 (hyperf/database) 'db' => [ 'db.query' => 'db.query', 'db.statement' => 'db.statement', 'db.query_time' => 'db.query_time', ], ] ]; ================================================ FILE: order-srv/config/autoload/processes.php ================================================ [ 'host' => env('REDIS_HOST', 'localhost'), 'auth' => env('REDIS_AUTH', null), 'port' => (int) env('REDIS_PORT', 6379), 'db' => (int) env('REDIS_DB', 0), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), ], ], ]; ================================================ FILE: order-srv/config/autoload/server.php ================================================ SWOOLE_PROCESS, 'servers' => [ [ 'name' => 'http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9502, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], ], ], [ 'name' => 'jsonrpc-http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9505, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [\Hyperf\JsonRpc\HttpServer::class, 'onRequest'], ], ], ], 'settings' => [ Constant::OPTION_ENABLE_COROUTINE => true, Constant::OPTION_WORKER_NUM => swoole_cpu_num(), Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', Constant::OPTION_OPEN_TCP_NODELAY => true, Constant::OPTION_MAX_COROUTINE => 100000, Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, Constant::OPTION_MAX_REQUEST => 100000, Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, ], 'callbacks' => [ Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], ], ]; ================================================ FILE: order-srv/config/autoload/services.php ================================================ 'nacos', 'address' => 'http://'.env('NACOS_HOST').':'.env('NACOS_PORT'), ]; // 这里配置需要用的rpc服务 $services = [ [ 'name' => 'UserRpcService', 'service' => \App\JsonRpc\UserRpcServiceInterface::class ] ]; return [ 'enable' => [ // 开启服务发现 'discovery' => true, // 开启服务注册 'register' => true, ], // 服务消费者相关配置 'consumers' => value(function () use ($services, $registry) { // 循环生成rpc消费端 $consumers = []; foreach ($services as $value) { $consumers[] = [ 'name' => $value['name'], 'service' => $value['service'], 'registry' => $registry ]; } return $consumers; }), // 服务提供者相关配置 'providers' => [], // 服务驱动相关配置 'drivers' => [ // nacos 配置,当前使用 'nacos' => [ // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // nacos 账号密码信息 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], // 命名空间,public为默认系统空间 'group_name' => 'api', // 命名空间ID 'namespace_id' => env('NACOS_TENANT'), // 心跳检查秒数 'heartbeat' => 5, 'ephemeral' => true, // 是否注册临时实例 ], ], ]; ================================================ FILE: order-srv/config/autoload/tracer.php ================================================ env('TRACER_DRIVER', 'jaeger'), // 这里的代码演示不对 enable 内的配置进行展开 'enable' => [], 'tracer' => [ // Zipkin 驱动配置 'zipkin' => [ // 当前应用的配置 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // 如果 ipv6 和 ipv6 为空组件会自动从 Server 中检测 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'options' => [ // Zipkin 服务的 endpoint 地址 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), // 请求超时秒数 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], // 采样器,默认为所有请求的都追踪 'sampler' => \Zipkin\Samplers\BinarySampler::createAsAlwaysSample(), ], ], ]; ================================================ FILE: order-srv/config/autoload/watcher.php ================================================ ScanFileDriver::class, 'bin' => 'php', 'watch' => [ 'dir' => ['app', 'config'], 'file' => ['.env'], 'scan_interval' => 2000, ], ]; ================================================ FILE: order-srv/config/config.php ================================================ env('APP_NAME', 'skeleton'), 'app_env' => env('APP_ENV', 'dev'), 'scan_cacheable' => env('SCAN_CACHEABLE', false), StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, LogLevel::CRITICAL, // LogLevel::DEBUG, LogLevel::EMERGENCY, LogLevel::ERROR, LogLevel::INFO, LogLevel::NOTICE, LogLevel::WARNING, ], ], ]; ================================================ FILE: order-srv/config/container.php ================================================ bigIncrements('id'); $table->string('order_no')->nullable()->comment('订单编号'); $table->integer('user_id')->default(0)->comment('线下订单编号'); $table->integer('coupon_id')->default(0)->comment('会员优惠券id'); $table->decimal('order_money',10,2)->default(0)->comment('订单金额'); $table->decimal('order_discount',10,2)->default(0)->comment('订单折扣'); $table->decimal('order_fact_money',10,2)->default(0)->comment('订单实际金额'); $table->integer('consume_number')->default(0)->comment('商品数量'); $table->tinyInteger('order_status')->default(0)->comment('订单状态, -1 已关闭 0 待支付 1 支付成功 2 已发货 3 退款处理 4 已完成'); $table->tinyInteger('payment')->default(1)->comment('支付方式 1储值 2微信'); $table->integer('create_time')->default(0)->comment('创建时间'); $table->integer('update_time')->default(0)->comment('更新时间'); $table->comment('订单表'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('order'); } } ================================================ FILE: order-srv/migrations/2022_06_08_063020_create_order_goods_table.php ================================================ bigIncrements('id'); $table->string('order_no',35)->nullable()->comment('订单编号'); $table->integer('goods_id')->default(0)->comment('商品id'); $table->string('goods_sn',35)->nullable()->comment('商品编号'); $table->integer('sku_id')->default(0)->comment('skuid'); $table->integer('user_id')->default(0)->comment('用户id'); $table->string('goods_name',35)->nullable()->comment('商品名'); $table->integer('number')->default(0)->comment('商品个数'); $table->decimal('goods_tag_price',10,2)->default(0)->comment('吊牌价'); $table->decimal('goods_real_price',10,2)->default(0)->comment('实际价'); $table->decimal('goods_discount',10,2)->default(0)->comment('商户折扣'); $table->decimal('goods_fact_money',10,2)->default(0)->comment('商品实际金额'); $table->integer('create_time')->default(0)->comment('创建时间'); $table->integer('update_time')->default(0)->comment('更新时间'); $table->comment('订单商品表'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('order_goods'); } } ================================================ FILE: order-srv/seeders/order_goods_seeder.php ================================================ insert([ 'order_no' => '20220527', 'goods_id' => 2, 'goods_sn' => 'AS12', 'sku_id' => 10, 'user_id' => 1, 'goods_name' => '测试商品', 'number' => 1, 'goods_tag_price' => 100.00, 'goods_real_price' => 100.00, 'goods_discount' => 100.00, 'goods_fact_money' => 100.00, 'create_time' =>time(), 'update_time' =>time(), ]); } } ================================================ FILE: order-srv/seeders/order_seeder.php ================================================ insert([ 'order_no' => '20220527', 'user_id' => 1, 'coupon_id' => 0, 'order_money' => 100.00, 'order_discount' => 100.00, 'order_fact_money' => 100.00, 'consume_number' => 1, 'order_status' => 1, 'payment' => 1, 'create_time' =>time(), 'update_time' =>time(), ]); } } ================================================ FILE: order-srv/test/Cases/ExampleTest.php ================================================ assertTrue(true); $this->assertTrue(is_array($this->get('/'))); } } ================================================ FILE: order-srv/test/HttpTestCase.php ================================================ client = make(Client::class); } public function __call($name, $arguments) { return $this->client->{$name}(...$arguments); } } ================================================ FILE: order-srv/test/bootstrap.php ================================================ get(Hyperf\Contract\ApplicationInterface::class); ================================================ FILE: task-srv/.github/workflows/Dockerfile ================================================ # Default Dockerfile # # @link https://www.hyperf.io # @document https://hyperf.wiki # @contact group@hyperf.io # @license https://github.com/hyperf/hyperf/blob/master/LICENSE FROM hyperf/hyperf:8.0-alpine-v3.15-swoole LABEL maintainer="Hyperf Developers " version="1.0" license="MIT" app.name="Hyperf" ## # ---------- env settings ---------- ## # --build-arg timezone=Asia/Shanghai ARG timezone ENV TIMEZONE=${timezone:-"Asia/Shanghai"} \ APP_ENV=prod \ SCAN_CACHEABLE=(true) # update RUN set -ex \ # show php version and extensions && php -v \ && php -m \ && php --ri swoole \ # ---------- some config ---------- && cd /etc/php8 \ # - config PHP && { \ echo "upload_max_filesize=128M"; \ echo "post_max_size=128M"; \ echo "memory_limit=1G"; \ echo "date.timezone=${TIMEZONE}"; \ } | tee conf.d/99_overrides.ini \ # - config timezone && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ && echo "${TIMEZONE}" > /etc/timezone \ # ---------- clear works ---------- && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \ && echo -e "\033[42;37m Build Completed :).\033[0m\n" WORKDIR /opt/www # Composer Cache # COPY ./composer.* /opt/www/ # RUN composer install --no-dev --no-scripts COPY . /opt/www RUN print "\n" | composer install -o && php bin/hyperf.php EXPOSE 9501 ENTRYPOINT ["php", "/opt/www/bin/hyperf.php", "start"] ================================================ FILE: task-srv/.github/workflows/build.yml ================================================ name: Build Docker on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Build run: cp -rf .github/workflows/Dockerfile . && docker build -t hyperf . ================================================ FILE: task-srv/.github/workflows/release.yml ================================================ on: push: # Sequence of patterns matched against refs/tags tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 name: Release jobs: release: name: Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false prerelease: false ================================================ FILE: task-srv/.gitignore ================================================ .buildpath .settings/ .project *.patch .idea/ .git/ runtime/ vendor/ .phpintel/ .env .DS_Store .phpunit* *.cache ================================================ FILE: task-srv/app/Amqp/Consumer/OrderConsumer.php ================================================ info("消费订单队列",$data); return Result::ACK; } } ================================================ FILE: task-srv/app/Amqp/Consumer/UserConsumer.php ================================================ info("消费用户队列",$data); return Result::ACK; } } ================================================ FILE: task-srv/app/Command/TestCommand.php ================================================ info("运行TestCommand任务:".date("Y-m-d H:i:s")); } } ================================================ FILE: task-srv/app/Controller/AbstractController.php ================================================ request->input('user', 'Hyperf'); $method = $this->request->getMethod(); return [ 'method' => $method, 'message' => "Hello {$user}.", ]; } } ================================================ FILE: task-srv/app/Exception/Handler/AppExceptionHandler.php ================================================ logger->error(sprintf('%s[%s] in %s', $throwable->getMessage(), $throwable->getLine(), $throwable->getFile())); $this->logger->error($throwable->getTraceAsString()); return $response->withHeader('Server', 'Hyperf')->withStatus(500)->withBody(new SwooleStream('Internal Server Error.')); } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: task-srv/app/Listener/DbQueryExecutedListener.php ================================================ logger = $container->get(LoggerFactory::class)->get('sql'); } public function listen(): array { return [ QueryExecuted::class, ]; } /** * @param QueryExecuted $event */ public function process(object $event): void { if ($event instanceof QueryExecuted) { $sql = $event->sql; if (! Arr::isAssoc($event->bindings)) { foreach ($event->bindings as $key => $value) { $sql = Str::replaceFirst('?', "'{$value}'", $sql); } } $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } } } ================================================ FILE: task-srv/app/Listener/QueueHandleListener.php ================================================ logger = $loggerFactory->get('queue'); } public function listen(): array { return [ AfterHandle::class, BeforeHandle::class, FailedHandle::class, RetryHandle::class, ]; } public function process(object $event): void { if ($event instanceof Event && $event->message->job()) { $job = $event->message->job(); $jobClass = get_class($job); if ($job instanceof AnnotationJob) { $jobClass = sprintf('Job[%s@%s]', $job->class, $job->method); } $date = date('Y-m-d H:i:s'); switch (true) { case $event instanceof BeforeHandle: $this->logger->info(sprintf('[%s] Processing %s.', $date, $jobClass)); break; case $event instanceof AfterHandle: $this->logger->info(sprintf('[%s] Processed %s.', $date, $jobClass)); break; case $event instanceof FailedHandle: $this->logger->error(sprintf('[%s] Failed %s.', $date, $jobClass)); $this->logger->error($this->formatter->format($event->getThrowable())); break; case $event instanceof RetryHandle: $this->logger->warning(sprintf('[%s] Retried %s.', $date, $jobClass)); break; } } } } ================================================ FILE: task-srv/app/Log.php ================================================ get(LoggerFactory::class)->get($name); } } ================================================ FILE: task-srv/app/Model/Model.php ================================================ get(Hyperf\Contract\ApplicationInterface::class); $application->run(); })(); ================================================ FILE: task-srv/composer.json ================================================ { "name": "hyperf/hyperf-skeleton", "type": "project", "keywords": [ "php", "swoole", "framework", "hyperf", "microservice", "middleware" ], "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", "license": "Apache-2.0", "require": { "php": ">=8.0", "hyperf/amqp": "3.0.*", "hyperf/async-queue": "3.0.*", "hyperf/cache": "3.0.*", "hyperf/command": "3.0.*", "hyperf/config": "3.0.*", "hyperf/config-nacos": "3.0.*", "hyperf/crontab": "3.0.*", "hyperf/database": "3.0.*", "hyperf/db-connection": "3.0.*", "hyperf/framework": "3.0.*", "hyperf/guzzle": "3.0.*", "hyperf/http-server": "3.0.*", "hyperf/logger": "3.0.*", "hyperf/memory": "3.0.*", "hyperf/model-cache": "3.0.*", "hyperf/process": "3.0.*", "hyperf/redis": "3.0.*", "hyperf/tracer": "3.0.*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "hyperf/devtool": "3.0.*", "hyperf/ide-helper": "3.0.*", "hyperf/testing": "3.0.*", "mockery/mockery": "^1.0", "phpstan/phpstan": "^0.12", "swoole/ide-helper": "^4.5" }, "suggest": { "ext-openssl": "Required to use HTTPS.", "ext-json": "Required to use JSON.", "ext-pdo": "Required to use MySQL Client.", "ext-pdo_mysql": "Required to use MySQL Client.", "ext-redis": "Required to use Redis Client." }, "autoload": { "psr-4": { "App\\": "app/" }, "files": [] }, "autoload-dev": { "psr-4": { "HyperfTest\\": "./test/" } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "optimize-autoloader": true, "sort-packages": true }, "extra": [], "scripts": { "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-autoload-dump": [ "rm -rf runtime/container" ], "test": "co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always", "cs-fix": "php-cs-fixer fix $1", "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", "start": [ "Composer\\Config::disableProcessTimeout", "php ./bin/hyperf.php start" ] } } ================================================ FILE: task-srv/config/autoload/amqp.php ================================================ [ 'host' => env('AMQP_HOST', 'localhost'), 'port' => (int) env('AMQP_PORT', 5672), 'user' => env('AMQP_USER', 'guest'), 'password' => env('AMQP_PASSWORD', 'guest'), 'vhost' => env('AMQP_VHOST', '/'), 'concurrent' => [ 'limit' => 1, ], 'pool' => [ 'connections' => 2, ], 'params' => [ 'insist' => false, 'login_method' => 'AMQPLAIN', 'login_response' => null, 'locale' => 'en_US', 'connection_timeout' => 3, 'read_write_timeout' => 6, 'context' => null, 'keepalive' => true, 'heartbeat' => 3, 'channel_rpc_timeout' => 0.0, 'close_on_destruct' => false, 'max_idle_channels' => 10, ], ], ]; ================================================ FILE: task-srv/config/autoload/annotations.php ================================================ [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], ], ]; ================================================ FILE: task-srv/config/autoload/aspects.php ================================================ [ 'driver' => Hyperf\AsyncQueue\Driver\RedisDriver::class, 'redis' => [ 'pool' => 'default', ], 'channel' => '{queue}', 'timeout' => 2, 'retry_seconds' => 5, 'handle_timeout' => 10, 'processes' => 1, 'concurrent' => [ 'limit' => 10, ], ], ]; ================================================ FILE: task-srv/config/autoload/cache.php ================================================ [ 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'c:', ], ]; ================================================ FILE: task-srv/config/autoload/commands.php ================================================ (bool) env('CONFIG_CENTER_ENABLE', true), 'driver' => env('CONFIG_CENTER_DRIVER', 'nacos'), 'mode' => env('CONFIG_CENTER_MODE', Mode::PROCESS), 'drivers' => [ 'nacos' => [ 'driver' => Hyperf\ConfigNacos\NacosDriver::class, 'merge_mode' => Hyperf\ConfigNacos\Constants::CONFIG_MERGE_OVERWRITE, 'interval' => 3, 'default_key' => 'nacos_config', 'listener_config' => [ // dataId, group, tenant, type, content 'nacos_config' => [ // 命名空间/ID 'tenant' => env('NACOS_TENANT'), // corresponding with service.namespaceId // DataID 'data_id' => env('NACOS_DATA_ID'), // 组名 'group' => 'DEFAULT_GROUP', 'type' => 'json', ], ], 'client' => [ // 客户端 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ], ], ], ]; ================================================ FILE: task-srv/config/autoload/crontab.php ================================================ true, 'crontab' => [ (new Crontab())->setType('command') ->setName('TestCommand') ->setRule('* * * * *')->setCallback([ 'command' => 'TestCommand', ]), ], ]; ================================================ FILE: task-srv/config/autoload/databases.php ================================================ [ 'driver' => env('DB_DRIVER', 'mysql'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'hyperf'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => env('DB_PREFIX', ''), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), ], 'cache' => [ 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, 'cache_key' => '{mc:%s:m:%s}:%s:%s', 'prefix' => 'default', 'ttl' => 3600 * 24, 'empty_model_ttl' => 600, 'load_script' => true, ], 'commands' => [ 'gen:model' => [ 'path' => 'app/Model', 'force_casts' => true, 'inheritance' => 'Model', 'uses' => '', 'table_mapping' => [], ], ], ], ]; ================================================ FILE: task-srv/config/autoload/dependencies.php ================================================ [ 'amqp' => [ 'consumer' => [ 'namespace' => 'App\\Amqp\\Consumer', ], 'producer' => [ 'namespace' => 'App\\Amqp\\Producer', ], ], 'aspect' => [ 'namespace' => 'App\\Aspect', ], 'command' => [ 'namespace' => 'App\\Command', ], 'controller' => [ 'namespace' => 'App\\Controller', ], 'job' => [ 'namespace' => 'App\\Job', ], 'listener' => [ 'namespace' => 'App\\Listener', ], 'middleware' => [ 'namespace' => 'App\\Middleware', ], 'Process' => [ 'namespace' => 'App\\Processes', ], ], ]; ================================================ FILE: task-srv/config/autoload/exceptions.php ================================================ [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, App\Exception\Handler\AppExceptionHandler::class, ], ], ]; ================================================ FILE: task-srv/config/autoload/listeners.php ================================================ [ 'handler' => [ 'class' => Monolog\Handler\RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => 'Y-m-d H:i:s', 'allowInlineLineBreaks' => true, ], ], ], ]; ================================================ FILE: task-srv/config/autoload/middlewares.php ================================================ [ ], ]; ================================================ FILE: task-srv/config/autoload/nacos.php ================================================ '', // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // The nacos account info 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ]; ================================================ FILE: task-srv/config/autoload/opentracing.php ================================================ env('TRACER_DRIVER', 'zipkin'), 'enable' => [ 'guzzle' => env('TRACER_ENABLE_GUZZLE', false), 'redis' => env('TRACER_ENABLE_REDIS', false), 'db' => env('TRACER_ENABLE_DB', false), 'method' => env('TRACER_ENABLE_METHOD', false), ], 'tracer' => [ 'zipkin' => [ 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'options' => [ 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], 'sampler' => BinarySampler::createAsAlwaysSample(), ], 'jaeger' => [ 'driver' => \Hyperf\Tracer\Adapter\JaegerTracerFactory::class, 'name' => env('APP_NAME', 'skeleton'), 'options' => [ 'local_agent' => [ 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'), 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775), ], ], ], ], ]; ================================================ FILE: task-srv/config/autoload/processes.php ================================================ [ 'host' => env('REDIS_HOST', 'localhost'), 'auth' => env('REDIS_AUTH', null), 'port' => (int) env('REDIS_PORT', 6379), 'db' => (int) env('REDIS_DB', 0), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), ], ], ]; ================================================ FILE: task-srv/config/autoload/server.php ================================================ SWOOLE_PROCESS, 'servers' => [ [ 'name' => 'http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9506, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], ], ], ], 'settings' => [ Constant::OPTION_ENABLE_COROUTINE => true, Constant::OPTION_WORKER_NUM => swoole_cpu_num(), Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', Constant::OPTION_OPEN_TCP_NODELAY => true, Constant::OPTION_MAX_COROUTINE => 100000, Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, Constant::OPTION_MAX_REQUEST => 100000, Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, ], 'callbacks' => [ Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], ], ]; ================================================ FILE: task-srv/config/config.php ================================================ env('APP_NAME', 'skeleton'), 'app_env' => env('APP_ENV', 'dev'), 'scan_cacheable' => env('SCAN_CACHEABLE', false), StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, LogLevel::CRITICAL, // LogLevel::DEBUG, LogLevel::EMERGENCY, LogLevel::ERROR, LogLevel::INFO, LogLevel::NOTICE, LogLevel::WARNING, ], ], ]; ================================================ FILE: task-srv/config/container.php ================================================ assertTrue(true); $this->assertTrue(is_array($this->get('/'))); } } ================================================ FILE: task-srv/test/HttpTestCase.php ================================================ client = make(Client::class); } public function __call($name, $arguments) { return $this->client->{$name}(...$arguments); } } ================================================ FILE: task-srv/test/bootstrap.php ================================================ get(Hyperf\Contract\ApplicationInterface::class); ================================================ FILE: user-srv/app/Amqp/Producer/UserProducer.php ================================================ payload = $data; } } ================================================ FILE: user-srv/app/Constants/ErrorCode.php ================================================ userService->changeStored( $request->input('user_id'), $request->input('amount'), $request->input('order_no'), ); return [ 'dtm_result' => 'SUCCESS', ]; } /** * 改变储值成功补偿 * @param RequestInterface $request * @return string[] */ #[PostMapping(path: 'changeStoredCompensate')] public function changeStoredCompensate(RequestInterface $request): array { //调用userService.changeStoredCompensate方法 $this->userService->changeStoredCompensate( $request->input('user_id'), $request->input('amount'), $request->input('order_no'), ); return [ 'dtm_result' => 'SUCCESS', ]; } } ================================================ FILE: user-srv/app/Exception/Handler/DtmExceptionHandler.php ================================================ stopPropagation(); return $response->withStatus(409); } // 交给下一个异常处理器 return $response; } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: user-srv/app/Exception/Handler/JsonRpcExceptionHandler.php ================================================ stopPropagation(); $request = Context::get(ServerRequestInterface::class); $jsonRpcTreaty = $request->getAttribute('data'); //返回自定义错误数据 // return di(ResponseBuilder::class)->buildResponse($request,$response); $result = responseError($throwable->getCode(), $throwable->getMessage()); $jsonRpc = [ "jsonrpc" => $jsonRpcTreaty['jsonrpc'], "id" => $jsonRpcTreaty['id'], "result" => $result, "context" => '' ]; return $response->withStatus($throwable->getCode()) ->withAddedHeader('content-type', 'application/json; charset=utf-8') ->withBody(new SwooleStream(json_encode($jsonRpc, JSON_UNESCAPED_UNICODE))); } // 交给下一个异常处理器 return $response; } public function isValid(Throwable $throwable): bool { return true; } } ================================================ FILE: user-srv/app/Exception/JsonRpcException.php ================================================ 200, 'data' => $this->userService->userInfo($userId) ]; } /** * 用户积分列表 * @param int $page * @param int $pageSize * @return array */ public function userBonusList(int $page, int $pageSize): array { return [ 'code' => 200, 'data' => $this->userService->userBonusList($page, $pageSize) ]; } /** * 用户储值列表 * @param int $page * @param int $pageSize * @return array */ public function userStoredList(int $page, int $pageSize): array { return [ 'code' => 200, 'data' => $this->userService->userStoredList($page, $pageSize) ]; } /** * 投递用户消息到RabbitMQ * @return array */ public function userRabbitMQ() : array { return [ 'code' => 200, 'data' => $this->userService->userRabbitMQ() ]; } /** * 用户登录 * @param string $phone * @return array */ public function userLogin(string $phone): array { return [ 'code' => 200, 'data' => $this->authService->userLogin($phone) ]; } /** * 用户退出 * @param string $token * @return array */ public function userLogout(string $token): array { return [ 'code' => 200, 'data' => $this->authService->userLogout($token) ]; } /** * 检查用户token * @param string $token * @return array */ public function userCheckToken(string $token): array { return [ 'code' => 200, 'data' => $this->authService->userCheckToken($token) ]; } } ================================================ FILE: user-srv/app/JsonRpc/UserRpcServiceInterface.php ================================================ get($id); } return $container; } } /** * 控制台日志 */ if (!function_exists('stdLog')) { function stdLog() { return di()->get(StdoutLoggerInterface::class); } } /** * 文件日志 */ if (!function_exists('logger')) { function logger($name = 'log', $group = 'default') { return di()->get(LoggerFactory::class)->get($name, $group); } } /** * redis 客户端实例 */ if (!function_exists('redis')) { function redis() { return di()->get(Redis::class); } } /** * 缓存实例 简单的缓存 */ if (!function_exists('cache')) { function cache() { return di()->get(\Psr\SimpleCache\CacheInterface::class); } } if (!function_exists('format_throwable')) { /** * Format a throwable to string. * @param Throwable $throwable * @return string */ function format_throwable(Throwable $throwable): string { return di()->get(FormatterInterface::class)->format($throwable); } } if (!function_exists('responseSuccess')) { function responseSuccess($code, $message = '', $data = []) { $content = ['code' => $code]; $message ? $content['msg'] = $message : $content['msg'] = \App\Constants\ResponseCode::getMessage($code); $data ? $content['data'] = $data : $content['data'] = []; return $content; } } if (!function_exists('responseError')) { function responseError($code, $message = '', $data = []) { $content = ['code' => $code]; $data ? $content['data'] = $data : $content['data'] = []; return $content; } } /** * 判读字符串是否为json */ if (!function_exists('isJson')) { /** * Finds an entry of the container by its identifier and returns it. * @param string $string */ function isJson($string) { json_decode($string); return (json_last_error() == JSON_ERROR_NONE); } } /** * 返回jsonRpc结构 */ if (!function_exists('successJsonRpc')) { function successJsonRpc($code, $data) { return [ 'code' => $code, 'data' => $data ]; } } ================================================ FILE: user-srv/app/Listener/DbQueryExecutedListener.php ================================================ logger = $container->get(LoggerFactory::class)->get('sql'); } public function listen(): array { return [ QueryExecuted::class, ]; } /** * @param QueryExecuted $event */ public function process(object $event) : void { if ($event instanceof QueryExecuted) { $sql = $event->sql; if (! Arr::isAssoc($event->bindings)) { foreach ($event->bindings as $key => $value) { $sql = Str::replaceFirst('?', "'{$value}'", $sql); } } $this->logger->info(sprintf('[%s] %s', $event->time, $sql)); } } } ================================================ FILE: user-srv/app/Log.php ================================================ get(LoggerFactory::class)->get($name); } } ================================================ FILE: user-srv/app/Model/User.php ================================================ 'integer']; public function bonus() { return $this->hasMany(UserBonusLog::class, 'user_id', 'id'); } public function stored() { return $this->hasMany(UserStoredLog::class, 'user_id', 'id'); } } ================================================ FILE: user-srv/app/Model/UserBonusLog.php ================================================ 'integer']; } ================================================ FILE: user-srv/app/Model/UserStoredLog.php ================================================ 'integer']; } ================================================ FILE: user-srv/app/Services/AuthService.php ================================================ info("调用userLogin"); $user = User::query() ->withSum('bonus', 'bonus') ->withSum('stored', 'amount') ->where('phone',$phone) ->first(); if (empty($user)) { Log::get()->info("用户不存在"); throw new JsonRpcException(10001); } return [ 'user_info' => $user, 'token_type' => 'bearer_token', 'token' => $this->auth->guard('jwt')->login($user), ]; } /** * 用户退出 * @param $token * @return mixed */ public function userLogout($token) { Log::get()->info("调用userLogout"); $this->auth->guard('jwt')->logout($token); return ; } /** * 检查用户token * @param $token * @return mixed */ public function userCheckToken($token) { Log::get()->info("调用userCheckToken"); $guard = $this->auth->guard('jwt'); try { if (! $guard->user($token) instanceof Authenticatable) { throw new JsonRpcException(422); } }catch (\Throwable $exception){ throw new JsonRpcException(422); } return ; } } ================================================ FILE: user-srv/app/Services/UserService.php ================================================ info("调用userInfo"); $User = User::query() ->withSum('bonus', 'bonus') ->withSum('stored', 'amount') ->find($userId); if (empty($User)) { Log::get()->info("用户不存在"); throw new JsonRpcException(10001); } return $User->toArray(); } /** * 用户积分列表 * @param $page * @param $pageSize * @return mixed */ public function userBonusList($page, $pageSize) { Log::get()->info("调用userBonusList"); $list = UserBonusLog::query() ->paginate($pageSize, ['*'], 'page', $page); if ($list->isEmpty()) { Log::get()->info("用户积分不存在"); throw new JsonRpcException(10002); } return $list->toArray(); } /** * 用户储值列表 * @param $page * @param $pageSize * @return mixed */ public function userStoredList($page, $pageSize) { Log::get()->info("调用userBonusList"); $list = UserStoredLog::query() ->paginate($pageSize, ['*'], 'page', $page); if ($list->isEmpty()) { Log::get()->info("用户储值不存在"); throw new JsonRpcException(10003); } return $list->toArray(); } /** * SAGA改变用户储值成功 * @param $userId * @param $amount * @param $orderNo */ public function changeStored($userId, $amount, $orderNo) { Log::get()->info("分布式事务-changeStored", [ 'user_id' => $userId, 'amount' => $amount, 'order_no' => $orderNo, ]); //分布式事务 try { UserStoredLog::create([ 'user_id' => $userId, 'type' => $amount >= 0 ? 1 : -1, 'amount' => $amount, 'source' => 'order', 'remark' => $orderNo ]); } catch (\Throwable $e) { Log::get()->info("分布式事务-changeStored-调用失败"); throw new ServiceException(); } } /** * SAGA改变用户储值补偿 * @param $userId * @param $amount * @param $orderNo */ public function changeStoredCompensate($userId, $amount, $orderNo) { Log::get()->info("分布式事务-changeStoredCompensate", [ 'user_id' => $userId, 'amount' => $amount, 'order_no' => $orderNo, ]); //分布式事务 try { UserStoredLog::query() ->where('user_id', $userId) ->where('source', 'order') ->where('remark', $orderNo) ->delete(); } catch (\Throwable $e) { Log::get()->info("分布式事务-changeStoredCompensate-调用失败"); throw new ServiceException(); } } /** * 投递用户消息到RabbitMQ */ public function userRabbitMQ() { //拼装数据 $message = new UserProducer([ 'id' => 1 ]); $producer = di()->get(Producer::class); //投递消息 $result = $producer->produce($message); //投递消息失败 if ($result != true){ throw new JsonRpcException(430); } } } ================================================ FILE: user-srv/bin/hyperf.php ================================================ #!/usr/bin/env php get(Hyperf\Contract\ApplicationInterface::class); $application->run(); })(); ================================================ FILE: user-srv/composer.json ================================================ { "name": "hyperf/hyperf-skeleton", "type": "project", "keywords": [ "php", "swoole", "framework", "hyperf", "microservice", "middleware" ], "description": "A coroutine framework that focuses on hyperspeed and flexible, specifically use for build microservices and middlewares.", "license": "Apache-2.0", "require": { "php": ">=8.0", "96qbhy/hyperf-auth": "^3.0.0", "dtm/dtm-client": "^0.3.0", "hyperf/amqp": "3.0.*", "hyperf/cache": "3.0.*", "hyperf/command": "3.0.*", "hyperf/config": "3.0.*", "hyperf/config-nacos": "3.0.*", "hyperf/constants": "3.0.*", "hyperf/database": "3.0.*", "hyperf/db-connection": "3.0.*", "hyperf/framework": "3.0.*", "hyperf/guzzle": "3.0.*", "hyperf/http-server": "3.0.*", "hyperf/json-rpc": "3.0.*", "hyperf/logger": "3.0.*", "hyperf/memory": "3.0.*", "hyperf/model-cache": "3.0.*", "hyperf/paginator": "3.0.*", "hyperf/process": "3.0.*", "hyperf/redis": "3.0.*", "hyperf/rpc": "3.0.*", "hyperf/rpc-client": "3.0.*", "hyperf/rpc-server": "3.0.*", "hyperf/service-governance": "3.0.*", "hyperf/service-governance-nacos": "3.0.*", "hyperf/tracer": "3.0.*" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.0", "hyperf/devtool": "3.0.*", "hyperf/ide-helper": "3.0.*", "hyperf/testing": "3.0.*", "hyperf/watcher": "3.0.*", "mockery/mockery": "^1.0", "phpstan/phpstan": "^0.12", "swoole/ide-helper": "^4.5" }, "suggest": { "ext-openssl": "Required to use HTTPS.", "ext-json": "Required to use JSON.", "ext-pdo": "Required to use MySQL Client.", "ext-pdo_mysql": "Required to use MySQL Client.", "ext-redis": "Required to use Redis Client." }, "autoload": { "psr-4": { "App\\": "app/" }, "files": [ "app/Kernel/Functions.php" ] }, "autoload-dev": { "psr-4": { "HyperfTest\\": "./test/" } }, "minimum-stability": "dev", "prefer-stable": true, "config": { "optimize-autoloader": true, "sort-packages": true }, "extra": [], "scripts": { "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-autoload-dump": [ "rm -rf runtime/container" ], "test": "co-phpunit --prepend test/bootstrap.php -c phpunit.xml --colors=always", "cs-fix": "php-cs-fixer fix $1", "analyse": "phpstan analyse --memory-limit 300M -l 0 -c phpstan.neon ./app ./config", "start": [ "Composer\\Config::disableProcessTimeout", "php ./bin/hyperf.php start" ] } } ================================================ FILE: user-srv/config/autoload/amqp.php ================================================ [ 'host' => env('AMQP_HOST', 'localhost'), 'port' => (int) env('AMQP_PORT', 5672), 'user' => env('AMQP_USER', 'guest'), 'password' => env('AMQP_PASSWORD', 'guest'), 'vhost' => env('AMQP_VHOST', '/'), 'open_ssl' => false, 'concurrent' => [ 'limit' => 1, ], 'pool' => [ 'connections' => 2, ], 'params' => [ 'insist' => false, 'login_method' => 'AMQPLAIN', 'login_response' => null, 'locale' => 'en_US', 'connection_timeout' => 3, 'read_write_timeout' => 6, 'context' => null, 'keepalive' => true, 'heartbeat' => 3, 'channel_rpc_timeout' => 0.0, 'close_on_destruct' => false, 'max_idle_channels' => 10, ], ], ]; ================================================ FILE: user-srv/config/autoload/annotations.php ================================================ [ 'paths' => [ BASE_PATH . '/app', ], 'ignore_annotations' => [ 'mixin', ], ], ]; ================================================ FILE: user-srv/config/autoload/aspects.php ================================================ [ 'guard' => 'jwt', 'provider' => 'user', ], 'guards' => [ 'jwt' => [ 'driver' => Qbhy\HyperfAuth\Guard\JwtGuard::class, 'provider' => 'user', /* * 以下是 simple-jwt 配置 * 必填 * jwt 服务端身份标识 */ 'secret' => env('SIMPLE_JWT_SECRET'), /* * 可选配置 * jwt 默认头部token使用的字段 */ 'header_name' => env('JWT_HEADER_NAME', 'Authorization'), /* * 可选配置 * jwt 生命周期,单位秒 */ 'ttl' => (int) env('SIMPLE_JWT_TTL', 60 * 60 * 24*3), /* * 可选配置 * 允许过期多久以内的 token 进行刷新 */ 'refresh_ttl' => (int) env('SIMPLE_JWT_REFRESH_TTL', 60 * 60 * 24 * 7), /* * 可选配置 * 默认使用的加密类 */ 'default' => Encrypter\PasswordHashEncrypter::class, /* * 可选配置 * 加密类必须实现 Qbhy\SimpleJwt\Interfaces\Encrypter 接口 */ 'drivers' => [ Encrypter\PasswordHashEncrypter::alg() => Encrypter\PasswordHashEncrypter::class, Encrypter\CryptEncrypter::alg() => Encrypter\CryptEncrypter::class, Encrypter\SHA1Encrypter::alg() => Encrypter\SHA1Encrypter::class, Encrypter\Md5Encrypter::alg() => Encrypter\Md5Encrypter::class, ], /* * 可选配置 * 编码类 */ 'encoder' => new Encoders\Base64UrlSafeEncoder(), // 'encoder' => new Encoders\Base64Encoder(), /* * 可选配置 * 缓存类 */ // 'cache' => new \Doctrine\Common\Cache\FilesystemCache(sys_get_temp_dir()), // 如果需要分布式部署,请选择 redis 或者其他支持分布式的缓存驱动 'cache' => function () { return make(Qbhy\HyperfAuth\HyperfRedisCache::class); }, /* * 可选配置 * 缓存前缀 */ 'prefix' => env('SIMPLE_JWT_PREFIX', 'jwt'), ], 'session' => [ 'driver' => Qbhy\HyperfAuth\Guard\SessionGuard::class, 'provider' => 'users', ], ], 'providers' => [ 'user' => [ 'driver' => \Qbhy\HyperfAuth\Provider\EloquentProvider::class, 'model' => App\Model\User::class, // 需要实现 Qbhy\HyperfAuth\Authenticatable 接口 ], ], ]; ================================================ FILE: user-srv/config/autoload/cache.php ================================================ [ 'driver' => Hyperf\Cache\Driver\RedisDriver::class, 'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class, 'prefix' => 'c:', ], ]; ================================================ FILE: user-srv/config/autoload/commands.php ================================================ (bool) env('CONFIG_CENTER_ENABLE', true), 'driver' => env('CONFIG_CENTER_DRIVER', 'nacos'), 'mode' => env('CONFIG_CENTER_MODE', Mode::PROCESS), 'drivers' => [ 'nacos' => [ 'driver' => Hyperf\ConfigNacos\NacosDriver::class, 'merge_mode' => Hyperf\ConfigNacos\Constants::CONFIG_MERGE_OVERWRITE, 'interval' => 3, 'default_key' => 'nacos_config', 'listener_config' => [ // dataId, group, tenant, type, content 'nacos_config' => [ // 命名空间/ID 'tenant' => env('NACOS_TENANT'), // corresponding with service.namespaceId // DataID 'data_id' => env('NACOS_DATA_ID'), // 组名 'group' => 'DEFAULT_GROUP', 'type' => 'json', ], ], 'client' => [ // 客户端 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ], ], ], ]; ================================================ FILE: user-srv/config/autoload/databases.php ================================================ [ 'driver' => env('DB_DRIVER', 'mysql'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', 3306), 'database' => env('DB_DATABASE', 'hyperf'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => env('DB_CHARSET', 'utf8mb4'), 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => env('DB_PREFIX', ''), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), ], // 'cache' => [ // 'handler' => Hyperf\ModelCache\Handler\RedisHandler::class, // 'cache_key' => '{mc:%s:m:%s}:%s:%s', // 'prefix' => 'default', // 'ttl' => 3600 * 24, // 'empty_model_ttl' => 600, // 'load_script' => true, // ], 'commands' => [ 'gen:model' => [ 'path' => 'app/Model', 'force_casts' => true, 'inheritance' => 'Model', 'uses' => '', 'table_mapping' => [], ], ], ], ]; ================================================ FILE: user-srv/config/autoload/dependencies.php ================================================ [ 'amqp' => [ 'consumer' => [ 'namespace' => 'App\\Amqp\\Consumer', ], 'producer' => [ 'namespace' => 'App\\Amqp\\Producer', ], ], 'aspect' => [ 'namespace' => 'App\\Aspect', ], 'command' => [ 'namespace' => 'App\\Command', ], 'controller' => [ 'namespace' => 'App\\Controller', ], 'job' => [ 'namespace' => 'App\\Job', ], 'listener' => [ 'namespace' => 'App\\Listener', ], 'middleware' => [ 'namespace' => 'App\\Middleware', ], 'Process' => [ 'namespace' => 'App\\Processes', ], ], ]; ================================================ FILE: user-srv/config/autoload/dtm.php ================================================ Protocol::HTTP, 'server' => '127.0.0.1', 'port' => [ 'http' => 36789, 'grpc' => 36790, ], 'barrier' => [ 'db' => [ 'type' => DbType::MySQL ], 'apply' => [], ], 'guzzle' => [ 'options' => [], ], ]; ================================================ FILE: user-srv/config/autoload/exceptions.php ================================================ [ 'http' => [ Hyperf\HttpServer\Exception\Handler\HttpExceptionHandler::class, App\Exception\Handler\DtmExceptionHandler::class, ], 'jsonrpc-http' => [ App\Exception\Handler\JsonRpcExceptionHandler::class, ] ], ]; ================================================ FILE: user-srv/config/autoload/listeners.php ================================================ [ 'handler' => [ 'class' => Monolog\Handler\RotatingFileHandler::class, 'constructor' => [ 'filename' => BASE_PATH . '/runtime/logs/hyperf.log', 'level' => Monolog\Logger::DEBUG, ], ], 'formatter' => [ 'class' => Monolog\Formatter\LineFormatter::class, 'constructor' => [ 'format' => null, 'dateFormat' => 'Y-m-d H:i:s', 'allowInlineLineBreaks' => true, ], ], ], ]; ================================================ FILE: user-srv/config/autoload/middlewares.php ================================================ [ \DtmClient\Middleware\DtmMiddleware::class, ], 'jsonrpc-http' => [ \Hyperf\Tracer\Middleware\TraceMiddleware::class, \DtmClient\Middleware\DtmMiddleware::class, ], ]; ================================================ FILE: user-srv/config/autoload/nacos.php ================================================ '', // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // The nacos account info 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], ]; ================================================ FILE: user-srv/config/autoload/opentracing.php ================================================ env('TRACER_DRIVER', 'zipkin'), 'enable' => [ 'guzzle' => env('TRACER_ENABLE_GUZZLE', false), 'redis' => env('TRACER_ENABLE_REDIS', false), 'db' => env('TRACER_ENABLE_DB', false), 'method' => env('TRACER_ENABLE_METHOD', false), ], 'tracer' => [ 'zipkin' => [ 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // Hyperf will detect the system info automatically as the value if ipv4, ipv6, port is null 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'options' => [ 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], 'sampler' => BinarySampler::createAsAlwaysSample(), ], 'jaeger' => [ 'driver' => \Hyperf\Tracer\Adapter\JaegerTracerFactory::class, 'name' => env('APP_NAME', 'skeleton'), 'options' => [ 'local_agent' => [ 'reporting_host' => env('JAEGER_REPORTING_HOST', 'localhost'), 'reporting_port' => env('JAEGER_REPORTING_PORT', 5775), ], ], ], ], 'tags' => [ // HTTP 客户端 (Guzzle) 'http_client' => [ 'http.url' => 'http.url', 'http.method' => 'http.method', 'http.status_code' => 'http.status_code', ], // Redis 客户端 'redis' => [ 'arguments' => 'arguments', 'result' => 'result', ], // 数据库客户端 (hyperf/database) 'db' => [ 'db.query' => 'db.query', 'db.statement' => 'db.statement', 'db.query_time' => 'db.query_time', ], ] ]; ================================================ FILE: user-srv/config/autoload/processes.php ================================================ [ 'host' => env('REDIS_HOST', 'localhost'), 'auth' => env('REDIS_AUTH', null), 'port' => (int) env('REDIS_PORT', 6379), 'db' => (int) env('REDIS_DB', 0), 'pool' => [ 'min_connections' => 1, 'max_connections' => 10, 'connect_timeout' => 10.0, 'wait_timeout' => 3.0, 'heartbeat' => -1, 'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60), ], ], ]; ================================================ FILE: user-srv/config/autoload/server.php ================================================ SWOOLE_PROCESS, 'servers' => [ [ 'name' => 'http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9503, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], ], ], [ 'name' => 'jsonrpc-http', 'type' => Server::SERVER_HTTP, 'host' => '0.0.0.0', 'port' => 9504, 'sock_type' => SWOOLE_SOCK_TCP, 'callbacks' => [ Event::ON_REQUEST => [\Hyperf\JsonRpc\HttpServer::class, 'onRequest'], ], ], ], 'settings' => [ Constant::OPTION_ENABLE_COROUTINE => true, Constant::OPTION_WORKER_NUM => swoole_cpu_num(), Constant::OPTION_PID_FILE => BASE_PATH . '/runtime/hyperf.pid', Constant::OPTION_OPEN_TCP_NODELAY => true, Constant::OPTION_MAX_COROUTINE => 100000, Constant::OPTION_OPEN_HTTP2_PROTOCOL => true, Constant::OPTION_MAX_REQUEST => 100000, Constant::OPTION_SOCKET_BUFFER_SIZE => 2 * 1024 * 1024, Constant::OPTION_BUFFER_OUTPUT_SIZE => 2 * 1024 * 1024, ], 'callbacks' => [ Event::ON_WORKER_START => [Hyperf\Framework\Bootstrap\WorkerStartCallback::class, 'onWorkerStart'], Event::ON_PIPE_MESSAGE => [Hyperf\Framework\Bootstrap\PipeMessageCallback::class, 'onPipeMessage'], Event::ON_WORKER_EXIT => [Hyperf\Framework\Bootstrap\WorkerExitCallback::class, 'onWorkerExit'], ], ]; ================================================ FILE: user-srv/config/autoload/services.php ================================================ [ // 开启服务发现 'discovery' => true, // 开启服务注册 'register' => true, ], // 服务消费者相关配置 'consumers' => [], // 服务提供者相关配置 'providers' => [], // 服务驱动相关配置 'drivers' => [ // nacos 配置,当前使用 'nacos' => [ // The nacos host info 'host' => env('NACOS_HOST'), 'port' => env('NACOS_PORT'), // nacos 账号密码信息 'username' => env('NACOS_USERNAME'), 'password' => env('NACOS_PASSWORD'), 'guzzle' => [ 'config' => null, ], // 命名空间,public为默认系统空间 'group_name' => 'api', // 命名空间ID 'namespace_id' => env('NACOS_TENANT'), // 心跳检查秒数 'heartbeat' => 5, 'ephemeral' => true, // 是否注册临时实例 ], ], ]; ================================================ FILE: user-srv/config/autoload/tracer.php ================================================ env('TRACER_DRIVER', 'jaeger'), // 这里的代码演示不对 enable 内的配置进行展开 'enable' => [], 'tracer' => [ // Zipkin 驱动配置 'zipkin' => [ // 当前应用的配置 'app' => [ 'name' => env('APP_NAME', 'skeleton'), // 如果 ipv6 和 ipv6 为空组件会自动从 Server 中检测 'ipv4' => '127.0.0.1', 'ipv6' => null, 'port' => 9501, ], 'driver' => \Hyperf\Tracer\Adapter\ZipkinTracerFactory::class, 'options' => [ // Zipkin 服务的 endpoint 地址 'endpoint_url' => env('ZIPKIN_ENDPOINT_URL', 'http://localhost:9411/api/v2/spans'), // 请求超时秒数 'timeout' => env('ZIPKIN_TIMEOUT', 1), ], // 采样器,默认为所有请求的都追踪 'sampler' => \Zipkin\Samplers\BinarySampler::createAsAlwaysSample(), ], ], ]; ================================================ FILE: user-srv/config/autoload/watcher.php ================================================ ScanFileDriver::class, 'bin' => 'php', 'watch' => [ 'dir' => ['app', 'config'], 'file' => ['.env'], 'scan_interval' => 2000, ], ]; ================================================ FILE: user-srv/config/config.php ================================================ env('APP_NAME', 'skeleton'), 'app_env' => env('APP_ENV', 'dev'), 'scan_cacheable' => env('SCAN_CACHEABLE', false), StdoutLoggerInterface::class => [ 'log_level' => [ LogLevel::ALERT, LogLevel::CRITICAL, // LogLevel::DEBUG, LogLevel::EMERGENCY, LogLevel::ERROR, LogLevel::INFO, LogLevel::NOTICE, LogLevel::WARNING, ], ], ]; ================================================ FILE: user-srv/config/container.php ================================================ bigIncrements('id'); $table->string('user_name', 50)->nullable()->comment('用户名'); $table->string('user_image', 255)->nullable()->comment('头像'); $table->string('phone', 30)->nullable()->comment('手机号'); $table->tinyInteger('sex')->default(0)->comment('性别,1男,2女,0未知'); $table->integer('create_time')->default(0)->comment('创建时间'); $table->integer('update_time')->default(0)->comment('更新时间'); $table->comment('用户表'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('user'); } } ================================================ FILE: user-srv/migrations/2022_06_08_072120_create_user_bonus_log_table.php ================================================ bigIncrements('id'); $table->integer('user_id')->default(0)->comment('用户id'); $table->tinyInteger('type')->default(0)->comment('积分类型 -1减去积分 1 增加积分'); $table->integer('bonus')->default(0)->comment('积分值'); $table->string('source',50)->nullable()->comment('来源: order订单 admin_change后台调整'); $table->string('remark',50)->nullable()->comment('备注'); $table->integer('create_time')->default(0)->comment('创建时间'); $table->integer('update_time')->default(0)->comment('更新时间'); $table->comment('用户积分表'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('user_bonus_log'); } } ================================================ FILE: user-srv/migrations/2022_06_08_072134_create_user_stored_log_table.php ================================================ bigIncrements('id'); $table->integer('user_id')->default(0)->comment('用户id'); $table->tinyInteger('type')->default(0)->comment('积分类型 -1减去储值 1 增加储值'); $table->decimal('amount',10,2)->default(0)->comment('金额'); $table->string('source',50)->nullable()->comment('来源: pay充值 order订单 admin_change后台调整'); $table->string('remark',50)->nullable()->comment('备注'); $table->integer('create_time')->default(0)->comment('创建时间'); $table->integer('update_time')->default(0)->comment('更新时间'); $table->comment('用户储值表'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('user_stored_log'); } } ================================================ FILE: user-srv/seeders/user_bonus_log_seeder.php ================================================ insert([ 'user_id' => 1, 'type' => 1, 'bonus' => mt_rand(1,10), 'source' => 1, 'remark' => 'order', 'create_time' =>time(), 'update_time' =>time(), ]); } } } ================================================ FILE: user-srv/seeders/user_seeder.php ================================================ insert([ 'user_name' => 'DoubleJin', 'user_image' => 'http://thirdwx.qlogo.cn/mmopen/Px0DzaTpW96d5vicVMbA6hkjic31msWnF1gU1yicrfNsmkxd1UT8N4vlTV6UArXQm1cBo3AB9I74dfGIquZ21SuJRLCuA5fIia7v/132', 'phone' => '1358888888', 'sex' => 1, 'create_time' =>time(), 'update_time' =>time(), ]); } } ================================================ FILE: user-srv/seeders/user_stored_log_seeder.php ================================================ insert([ 'user_id' => 1, 'type' => 1, 'amount' => mt_rand(1,10), 'source' => 1, 'remark' => 'pay', 'create_time' =>time(), 'update_time' =>time(), ]); } } } ================================================ FILE: user-srv/test/Cases/ExampleTest.php ================================================ assertTrue(true); $this->assertTrue(is_array($this->get('/'))); } } ================================================ FILE: user-srv/test/HttpTestCase.php ================================================ client = make(Client::class); } public function __call($name, $arguments) { return $this->client->{$name}(...$arguments); } } ================================================ FILE: user-srv/test/bootstrap.php ================================================ get(Hyperf\Contract\ApplicationInterface::class);