实习学习项目
一、启动整个项目时报错
错误原因:
解决方案:打开 GoodsApiContextTest.java
和 TestZebra.java
等空的测试类,在类中添加一个简单的、能通过的测试方法。
例如,修改 TestZebra.java
:
package com.newbie.user;// ... 其他 import ...
import org.junit.Assert; // 引入断言@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {UserContext.class})
@ActiveProfiles("test")
public class TestZebra {@ResourceUserService userService;@Testpublic void testRegister() {// 由于没有实现,我们不能真的调用它,否则会空指针// BaseResponse<Long> register = userService.register(new UserRegisterDTO());// 我们可以只做一个简单的断言,确保 userService 被成功注入,不为nullAssert.assertNotNull(userService);}// 你也可以加一个空的测试方法,让测试任务通过@Testpublic void contextLoads() {// 这个测试什么都不做,但因为它存在,JUnit就不会报错}
}
注意:请务必采用“分别启动各个微服务的
main
方法,即启动类”的方式来运行项目,这才是微服务开发的正确姿势。
二、项目配置
好的,这两个问题是项目能跑起来的最后关键一步,我们来彻底弄清楚。
简单来说:Zookeeper 你只需要在电脑上下载并启动即可,项目通过XML里的地址自动连接。Zebra 是一个数据库中间件,它最终连接的还是MySQL数据库,你不需要为Zebra做额外配置,只需要在你的MySQL里准备好数据表就行。
下面是详细的配置步骤和解释。
1、Zookeeper的配置与使用
1. RPC端口号问题
“rpc的xml配置中,为服务分配的端口号直接自己指定吗?”
是的,可以自己指定,但需要遵守两个规则:
- 端口不能被占用:你指定的端口号(如
20880
,20881
)在你的电脑上必须是空闲的。 - 同一台机器上的服务不能重复:如果你在同一台电脑上同时运行
user
服务和goods
服务,它们的Dubbo端口号必须不同,否则后启动的服务会因端口冲突而失败。这就是为什么我们给user
服务用20880
,给goods
服务用20881
。
2. 如何在项目中使用Zookeeper
你需要在两个层面来理解“使用”:一是运行Zookeeper服务本身,二是让你的项目连接上它。
步骤一:在你的电脑上安装并启动Zookeeper服务(一次性操作)
Zookeeper是一个独立软件,你需要先把它跑起来。
- 下载: 前往 Apache Zookeeper 官网 下载一个稳定版本(例如 3.5.x 或 3.6.x 的
bin
版本)。 - 解压: 将下载的压缩包解压到你电脑的某个目录下,例如
D:\dev\zookeeper
。 - 配置:
- 进入
conf
目录,将zoo_sample.cfg
文件复制一份并重命名为zoo.cfg
。 - 用文本编辑器打开
zoo.cfg
,通常默认配置就足够本地开发使用了,无需修改。
- 进入
- 启动:
- Windows: 进入
bin
目录,双击运行zkServer.cmd
。一个黑色窗口会一闪而过,然后保持在后台运行。 - macOS/Linux: 进入
bin
目录,在终端中执行命令./zkServer.sh start
。
- Windows: 进入
当你看到 "STARTED" 或类似提示,就说明Zookeeper服务已经成功运行在你的电脑上了。
步骤二:让你的项目自动连接Zookeeper
这一步你其实已经做完了,是在XML配置中完成的。
在你之前配置的 rpc-server.xml
(提供方) 和 rpc-client.xml
(消费方) 中,都有这样一行关键代码: <dubbo:registry address="zookeeper://127.0.0.1:2181"/>
- 它的作用: 这行配置就是在告诉项目里的Dubbo框架:
- “请使用Zookeeper作为我的注册中心。”
- “Zookeeper服务正运行在本机(
127.0.0.1
)的2181
端口,快去连接它吧。”
总结: 你的应用程序是通过读取这个XML配置自动去连接和使用Zookeeper的。你不需要编写任何Java代码来手动操作Zookeeper。Dubbo框架会在服务启动时,自动将服务信息(如IP和端口)注册到Zookeeper;在调用时,自动从Zookeeper获取服务列表。
2、项目数据库的配置与使用 (Zebra vs MySQL)
1. Zebra到底是什么?还需不需要MySQL?
这是最关键的一点:Zebra 不是数据库,它是一个数据库中间件,可以理解为一个更强大的数据库连接池和管理平台。它的底层连接的仍然是标准的MySQL数据库。
所以:你仍然需要一个MySQL数据库。
2. 如何配置和使用?
你作为开发者,职责非常清晰,我们分一下工:
你的任务 (Your Task) | Zebra的任务 (Zebra's Task) |
---|---|
1. 在你的电脑上安装并运行MySQL数据库服务。 | 1. 在公司的平台上管理数据库的真实连接信息(IP, 端口, 用户名, 密码)。 |
2. 在你的MySQL中创建库和表(即执行我们之前用到的user.sql , goods.sql 等文件)。 | 2. 根据你代码中提供的资源密钥(Key),自动创建并优化数据库连接池(DataSource )。 |
导出到 Google 表格
简单来说,你的操作步骤如下:
- 准备MySQL: 确保你的电脑上MySQL服务正在运行。
- 执行SQL: 使用Navicat或命令行工具,连接到你的MySQL,并执行项目给出的所有
.sql
文件,确保user
,goods
,orders
等数据表都已创建。 - 确认代码中的Key: 确保
repository
包下的MySQLDataSourceConfig.java
文件中,getDataSource()
方法里的那个长长的资源密钥是正确的。 Java@Configuration public class MySQLDataSourceConfig {@Beanpublic DataSource dataSource(){// 你只需要保证这个Key是正确的return ZebretteDataSourceFactory.getDataSource("zebrette-auth-center.htj_..."); } }
完成这三步后,当你启动user
或goods
等微服务时,服务内的Zebra客户端库就会拿着这个Key,自动帮你处理好所有与MySQL数据库的连接事宜。你无需再进行任何其他数据库相关的配置。
三、项目设计
1、项目架构
goodsth-newbie-api
(API网关):作为系统的统一入口,负责接收外部的HTTP请求。它将对请求进行初步处理(如认证、参数校验),然后通过Dubbo RPC协议将请求路由到相应的后端业务服务。goodsth-newbie-user
(用户服务):负责处理所有与用户相关的业务逻辑,提供用户注册、登录等Dubbo接口。goodsth-newbie-goods
(商品服务):负责处理所有与商品相关的业务逻辑,提供查询商品列表、详情等Dubbo接口。goodsth-newbie-order
(订单服务):负责处理所有与订单相关的业务逻辑,提供创建订单、查询订单等Dubbo接口。newbie-contract
:契约模块。定义Dubbo服务的接口(Interface)和数据传输对象(DTOs)。该模块会被API网关和其他服务依赖,是服务间通信的“合同”。newbie-common
:公共模块。存放跨模块可复用的工具类、常量定义、通用枚举、异常类等。newbie-repository
:数据仓储模块。负责数据持久化,包含MyBatis的Mapper接口、实体类(POJO)和XML配置文件。直接与数据库中间件(Zebra)交互。(这里实际上应该是各个微服务的内部维护自己的repository,但这只是学习项目,为了方便管理,就写到同一个包下)newbie-integration
:集成模块。用于调用其他微服务或外部第三方服务。例如,订单服务可能会在这里集成调用用户服务和商品服务。newbie-tool
:工具模块。可以放置一些项目特有的工具,如MyBatis代码生成器等。
2、实体关系
用户(User)、商品(Goods)和订单(Orders)三个核心实体之间的关系如下:
- 一个
User
可以拥有多个Orders
。 - 一个
Goods
可以出现在多个Orders
中。 Orders
表通过user_id
和goods_id
与另外两张表关联。
3、包作用
Repository包下的MySQLDataSourceConfig类定义了数据源的加载:
- 它使用
@Bean
和@Configuration
注解,这是 Spring 的标准配置方式。 - 核心代码是
ZebretteDataSourceFactory.getDataSource(...)
。这表明 Zebra 提供了一个工厂类,你只需要传入一个唯一的资源标识符(DSN),它就能自动帮你创建并配置好数据源。 - 这个长长的字符串
zebrette-auth-center.htj_...
就是这个唯一标识,Zebra 会根据它去查找(很可能是在公司的配置中心,如README.md
提到的Leo)对应的数据库地址、用户名、密码等信息。
Repository包下的RepositoryContext类的作用:
- 配置事务管理器 (
transactionManager
):让 Spring Boot 能够管理数据库事务。 - 配置 MyBatis 的
SqlSessionFactoryBean
:这是 MyBatis 的核心,负责创建与数据库的会话。它绑定了上面 Zebra 配置好的dataSource
。 - 配置 Mapper 扫描器 (
ZebraMapperScannerConfigurer
):它会自动扫描com.newbie.repository.mybatis.mapper
这个包路径下的所有 MyBatis Mapper 接口,并把它们注册到 Spring 容器中,这样你就可以在业务代码中直接@Autowired
注入UserMapper
等接口并使用了。
tool
包下的 UserMybatisGenerator.java类
是一个开发工具,用来自动生成代码。它会读取一个 generatorConfig.xml
配置文件,然后根据你的数据库表结构,自动创建 repository
层所需的三个核心文件:
Entity
类 (如User.java
)Mapper
接口 (如UserMapper.java
)Mapper.xml
文件 (包含基础的增删改查SQL)
integration
包和contract
包的作用区别
-
contract
(合同/契约包):- 职责: 定义能力。它只包含接口(
interface
)和数据传输对象(DTO
),不包含任何具体的实现逻辑。 - 共享性: 它是共享的,服务提供方(
user
服务)和消费方(api
服务)都会依赖它。 - 目的: 实现解耦。只要这份“合同”不变,两边的开发团队就可以独立工作。
- 职责: 定义能力。它只包含接口(
-
integration
(集成/整合包):- 职责: 实现调用。它包含了消费方(
api
服务)如何去调用远程服务的具体实现代码(比如内部封装了@DubboReference
)。 - 共享性: 它是消费方独有的。只有
api
服务需要它,user
服务对它一无所知。 - 目的: 封装通信细节。让
api
服务的上层业务(UserManager
)不用关心底层是用Dubbo还是其他技术进行通信的,只需调用一个本地的integration
方法即可。
- 职责: 实现调用。它包含了消费方(
四、用户注册功能实现
1、实现 Repository (数据访问层)
这是与数据库直接交互的基础。我们需要实现“根据用户名查询”和“插入新用户”两个功能。
实体类: repository/src/main/java/com/newbie/repository/entity/User.java
这个实体类与数据库的 user
表字段一一对应。(原则上是通过generator自动生成的)
package com.newbie.repository.entity;import lombok.Data;
import java.sql.Timestamp;@Data
public class User {private Long id;private String userName;private String passwd;private String receiveName;private String shippingAddress;private String mobile;private Integer gender;private Boolean isDeleted;private Timestamp gmtCreated;private Timestamp gmtModified;
}
mapper类:repository/src/main/java/com/newbie/repository/mapper/UserMapper.java
package com.newbie.repository.mapper;import com.newbie.repository.entity.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;@Repository
public interface UserMapper {/*** 根据用户名查询未被删除的用户* @param userName 用户名* @return 用户实体*/User findByUserName(@Param("userName") String userName);/*** 插入一个新用户* @param user 用户实体* @return 影响的行数*/int insertUser(User user);
}
xml文件:repository/src/main/resources/mybatis/mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newbie.repository.mapper.UserMapper"><select id="findByUserName" resultType="com.newbie.repository.entity.User">SELECTid, user_name, passwdFROMuserWHEREuser_name = #{userName} AND is_deleted = 0LIMIT 1;</select><insert id="insertUser" parameterType="com.newbie.repository.entity.User" useGeneratedKeys="true" keyProperty="id">INSERT INTO user(user_name, passwd, receive_name, shipping_address, mobile, gender)VALUES(#{userName}, #{passwd}, #{receiveName}, #{shippingAddress}, #{mobile}, #{gender})</insert>
</mapper>
2、实现 user
微服务 (服务提供方)
这里是注册功能的核心业务逻辑。
Impl具体逻辑实现类: user/src/main/java/com/newbie/user/service/UserServiceImpl.java
package com.newbie.user.service;import com.newbie.contract.UserService;
import com.newbie.contract.dto.UserRegisterDTO;
import com.newbie.user.enums.UserErrorCode;
import com.newbie.repository.entity.User;
import com.newbie.repository.mapper.UserMapper;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.dubbo.config.annotation.DubboService;// 其他需要的import...@DubboService(timeout = 5000)
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic BaseResponse<Long> register(UserRegisterDTO request) {// 1. 检查用户名是否已存在if (userMapper.findByUserName(request.getUserName()) != null) {// 使用定义好的枚举返回错误码return BaseResponse.ofError(UserErrorCode.USER_EXIST.getCode(), UserErrorCode.USER_EXIST.getDesc());}// 2. 将DTO转换为Entity,准备写入数据库User user = new User();BeanUtils.copyProperties(request, user);// !!安全警告!!: 作为你的主管,我必须强调,生产环境绝不能明文存储密码!// 必须使用 BCrypt 等哈希算法对密码进行加密处理。// 示例: user.setPasswd(bCryptPasswordEncoder.encode(request.getPassWd()));user.setPasswd(request.getPassWd()); // 此处为新手项目简化处理// 3. 执行数据库插入操作userMapper.insertUser(user);// 4. 返回新用户的ID (通过 useGeneratedKeys 回填)return BaseResponse.ofSuccess(user.getId());}// ... 其他接口方法的实现 ...
}
3、实现 api
网关 (服务消费方)
网关负责接收HTTP请求,并通过Dubbo调用user
服务。
UserIntegrationServiceImpl.java
:integration
/src/main/java/com/newbie/integration/service/impl/UserIntegrationServiceImpl.java
package com.newbie.integration.service.impl;import com.newbie.contract.UserService;
import com.newbie.contract.dto.UserRegisterDTO;
import com.newbie.integration.service.UserIntegrationService;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;// 其他需要的import...@Service("userIntegrationService")
public class UserIntegrationServiceImpl implements UserIntegrationService {// 使用Dubbo注解注入远程服务代理@DubboReference(timeout = 5000, check = false) private UserService userService;@Overridepublic Long register(UserRegisterDTO userRegisterDTO) {// 调用远程服务BaseResponse<Long> response = userService.register(userRegisterDTO);// 进行错误处理,如果调用失败,可以抛出异常或返回特定值if (!response.isSuccess()) {// 此处可以根据业务需求抛出自定义异常throw new RuntimeException(response.getMessage());}return response.getData();}// ... 其他接口方法的实现 ...
}
UserManager:api/src/main/java/com/newbie/api/manager/UserManager.java
package com.newbie.api.manager;import com.newbie.contract.dto.UserRegisterDTO;
import com.newbie.contract.request.UserRegisterRequest;
import com.newbie.integration.service.UserIntegrationService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;// 其他需要的import...@Component
public class UserManager {@Resourceprivate UserIntegrationService userIntegrationService;public Long register(UserRegisterRequest request) { //// 将Controller层的Request对象 转换为Service层需要的DTO对象UserRegisterDTO userRegisterDTO = new UserRegisterDTO();BeanUtils.copyProperties(request, userRegisterDTO);return userIntegrationService.register(userRegisterDTO);}// ... 其他方法的实现 ...
}
UserController:api/src/main/java/com/newbie/api/controller/UserController.java
package com.newbie.api.controller;import com.newbie.api.manager.UserManager;
import com.newbie.contract.request.UserRegisterRequest;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;// 其他需要的import...@RestController
@RequestMapping("/api/newbie/user")
public class UserController {@Resourceprivate UserManager userManager;// 重点:注册是创建资源,应该使用POST方法,这是更规范的做法。@RequestMapping(value = "/register", method = RequestMethod.POST)public BaseResponse<Long> register(@RequestBody UserRegisterRequest request) { //return BaseResponse.ofSuccess(userManager.register(request));}// ... 其他接口 ...
}
4、rpc-client.xml和rpc-server.xml配置
integration
包中的 rpc-client.xml
(服务消费者配置)
-
它的角色: 这个文件属于服务消费者 (
api
网关服务)。它的作用是告诉api
服务:“你需要去连接(消费)一个远程的Dubbo服务,这是它的地址和接口信息”。 -
如何加载:
api
服务的启动类ApiContext
通过@Import(value = {IntegrationContext.class})
引入了集成层的配置,而IntegrationContext
通常会负责加载这个rpc-client.xml
文件,从而让其中的Dubbo配置生效。 -
完整配置代码:
-
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"><dubbo:application name="goodsth-newbie-api-consumer"/><dubbo:registry address="zookeeper://127.0.0.1:2181"/><dubbo:reference id="userService" interface="com.newbie.contract.UserService" check="false" timeout="5000"/></beans>
解释与关联:
- 配置了
<dubbo:reference>
后,Dubbo就会在Spring容器中创建一个ID为userService
的Bean,这个Bean是com.newbie.contract.UserService
接口的一个远程代理对象。 - 这样,在
integration
层的UserIntegrationServiceImpl
中,就可以通过@Resource(name = "userService")
或@Autowired
来注入这个代理对象,从而实现RPC调用。 - 注解方式补充: 在现代的Dubbo+Spring Boot整合中,我们更常用
@DubboReference
注解直接在Java代码中注入,就像我之前给你的代码一样。注解方式可以替代XML中的<dubbo:reference>
配置,更简洁。如果同时存在,注解的优先级通常更高。对于学习来说,理解XML配置的原理非常有帮助。
integration
包里的rpc-client.xml
:必须配置,用来声明api
服务需要调用哪些远程服务。user
微服务包里的rpc-client.xml
:在当前用户注册/登录功能中无需配置,保持为空,因为user
服务只提供服务,不消费其他服务。
user
微服务中 rpc-server.xml
的作用与完整配置
这个rpc-server.xml
文件是user
微服务作为服务提供方的核心配置文件。它主要告诉Dubbo框架四件事:
- 我是谁? (应用名叫什么)
- 我在哪注册? (注册中心的地址是什么,以便消费者能找到我)
- 我用什么方式提供服务? (使用什么协议,在哪个端口监听)
- 我具体提供什么服务? (暴露哪个Java接口,以及这个接口的具体实现是哪个类)
完整配置代码:请将user
微服务模块下的 src/main/resources/rpc-server.xml
文件补充为以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"><dubbo:application name="goodsth-newbie-user-provider"/><dubbo:registry address="zookeeper://127.0.0.1:2181"/><dubbo:protocol name="dubbo" port="20880"/><bean id="userServiceImpl" class="com.newbie.user.service.UserServiceImpl"/><dubbo:service interface="com.newbie.contract.UserService" ref="userServiceImpl" timeout="5000"/></beans>
配置与代码的关联
user
服务的启动类UserContext.java
中有一行@ImportResource("classpath:rpc-server.xml")
,正是这行代码加载了我们上面编写的配置文件,让它生效。<dubbo:service ref="userServiceImpl"/>
中的ref
属性,精确地指向了<bean id="userServiceImpl" ...>
。- 而这个 bean 的
class
属性,又指向了我们之前编写的包含所有业务逻辑的com.newbie.user.service.UserServiceImpl.java
文件。
通过这个配置,整个链路就完全打通了:Dubbo框架知道了要将 UserService
接口的远程请求,转发给 userServiceImpl
这个Bean实例来处理。
五、用户登录功能实现
1、user
微服务实现 (服务提供方)
user
服务需要提供两个接口:一个用于验证密码对错,另一个用于获取密码。
1. Repository层代码 (与之前相同):这部分无需改动,我们只需要findByUserName
方法。repository/src/main/java/com/newbie/repository/mapper/UserMapper.java
(确认)
// ...
public interface UserMapper {User findByUserName(@Param("userName") String userName);// ...
}
2. Service层实现 (UserServiceImpl.java
) 这里我们需要完整实现login
和getPassWd
两个方法。
文件: user/src/main/java/com/newbie/user/service/UserServiceImpl.java
(修改)
package com.newbie.user.service;import com.newbie.contract.UserService;
import com.newbie.contract.request.UserPwRequest;
import com.newbie.user.repository.entity.User;
import com.newbie.repository.mapper.UserMapper;
import com.newbie.user.enums.UserErrorCode;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
// 其他必要的import...@DubboService(timeout = 5000)
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;// ... register等其他方法的实现 .../*** 实现原设计的login方法,只做密码校验,返回布尔值。*/@Overridepublic BaseResponse<Boolean> login(UserPwRequest request) {User user = userMapper.findByUserName(request.getUserName());if (user != null && request.getPassWd().equals(user.getPasswd())) {return BaseResponse.ofSuccess(true);}return BaseResponse.ofSuccess(false);}/*** 实现原设计的getPassWd方法,返回用户密码。*/@Overridepublic BaseResponse<String> getPassWd(String userName) {// !!严重安全警告!!: 此方法将明文密码通过网络传输,仅为演示原有设计,生产环境严禁使用。User user = userMapper.findByUserName(userName);if (user == null) {return BaseResponse.ofError(UserErrorCode.USER_NOT_FOUND.getCode(), UserErrorCode.USER_NOT_FOUND.getDesc());}return BaseResponse.ofSuccess(user.getPassWd());}// ... 其他接口方法的桩代码 ...
}
2、api
网关实现 (服务消费方)
api
网关将编排两个RPC调用来完成登录。
1. Integration层实现
integration/src/main/java/com/newbie/integration/service/UserIntegrationService.java
(确认接口):确保接口中包含login
和getPassWd
方法。
文件: integration/src/main/java/com/newbie/integration/service/impl/UserIntegrationServiceImpl.java
(修改)
package com.newbie.integration.service.impl;import com.newbie.contract.UserService;
import com.newbie.contract.request.UserPwRequest;
import com.newbie.integration.service.UserIntegrationService;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;@Service("userIntegrationService")
public class UserIntegrationServiceImpl implements UserIntegrationService {@DubboReference(timeout = 5000, check = false) private UserService userService;// ... 其他方法的实现 ...@Overridepublic Boolean login(String userName, String passWd) {UserPwRequest request = new UserPwRequest();request.setUserName(userName);request.setPassWd(passWd);BaseResponse<Boolean> response = userService.login(request);// 如果调用成功且结果为true,则返回truereturn response.isSuccess() && response.getData();}@Overridepublic String getPassWd(String userName) {BaseResponse<String> response = userService.getPassWd(userName);if (!response.isSuccess()) {// 如果调用失败,可以抛出异常或返回nullthrow new RuntimeException(response.getMessage());}return response.getData();}
}
2. Manager层实现
文件: api/src/main/java/com/newbie/api/manager/UserManager.java
(修改)
package com.newbie.api.manager;import com.newbie.contract.request.UserPwRequest;
import com.newbie.integration.service.UserIntegrationService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;@Component
public class UserManager {@Resourceprivate UserIntegrationService userIntegrationService;// ... register等其他方法 .../*** 对应原设计的login方法,只做验证*/public Boolean login(UserPwRequest request) {return userIntegrationService.login(request.getUserName(), request.getPassWd());}/*** 对应原设计的getPassWd方法*/public String getPassWd(String userName) {return userIntegrationService.getPassWd(userName);}
}
3. Controller层实现 (关键的编排逻辑)
文件: api/src/main/java/com/newbie/api/controller/UserController.java
(修改)
package com.newbie.api.controller;import com.newbie.api.manager.UserManager;
import com.newbie.common.util.JWTUtil;
import com.newbie.contract.request.UserPwRequest;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;@RestController
@RequestMapping("/api/newbie/user")
public class UserController {@Resourceprivate UserManager userManager;// ... register等其他接口 ...@RequestMapping(value = "/login", method = RequestMethod.POST)public BaseResponse<String> login(@RequestBody UserPwRequest request) {// 第一步:调用login方法,验证用户名和密码是否匹配Boolean loginSuccess = userManager.login(request);// 如果验证不通过,直接返回错误信息if (loginSuccess == null || !loginSuccess) {return BaseResponse.ofError(1002, "用户名或密码错误"); // 使用自定义错误码}// 第二步:验证通过后,调用getPassWd方法获取密码原文(这是不安全的操作)String passWd = userManager.getPassWd(request.getUserName());// 第三步:在Controller层使用获取到的密码生成JWT TokenString token = JWTUtil.getToken(request.getUserName(), passWd, 3600);// 返回成功响应和Tokenreturn BaseResponse.ofSuccess(token);}
}
六、修改密码功能实现
此功能将复用项目中已有的updateUser
相关接口。我们的实现会聚焦于密码的更新,但同样的设计也适用于更新其他用户信息。
1. Repository层代码
我们需要一个方法来根据用户ID更新密码。
文件: repository/src/main/java/com/newbie/repository/mapper/UserMapper.java
(修改)
- 在接口中增加
updateUser
方法。
package com.newbie.repository.mapper;import com.newbie.repository.entity.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;@Repository
public interface UserMapper {User findByUserName(@Param("userName") String userName);int insertUser(User user);// 新增:根据ID查询用户,用于更新前的检查User findById(@Param("id") Long id);// 新增:更新用户信息的方法int updateUser(User user);
}
文件: repository/src/main/resources/mybatis/mapper/UserMapper.xml
(修改)
- 添加对应的
select
和update
语句。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newbie.repository.mapper.UserMapper"><select id="findById" resultType="com.newbie.repository.entity.User">SELECT id, user_name, passwd FROM user WHERE id = #{id} AND is_deleted = 0;</select><update id="updateUser" parameterType="com.newbie.repository.entity.User">UPDATE user<set><if test="passwd != null and passwd != ''">passwd = #{passwd},</if><if test="userName != null and userName != ''">user_name = #{userName},</if></set>WHERE id = #{id};</update>
</mapper>
- 解释:我们增加了一个
findById
用于前置校验,以及一个动态SQL的updateUser
方法。这个update
语句非常灵活,它只会更新你传入的User
对象中非空的字段。
2. user
微服务实现
文件: user/src/main/java/com/newbie/user/service/UserServiceImpl.java
(修改)
- 实现
updateUser
接口方法。
package com.newbie.user.service;import com.newbie.contract.UserService;
import com.newbie.contract.request.UserUpdateRequest;
import com.newbie.repository.entity.User;
import com.newbie.repository.mapper.UserMapper;
import com.newbie.user.enums.UserErrorCode;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;@DubboService(timeout = 5000)
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;// ... register 和 login 的实现 ...@Overridepublic BaseResponse<Boolean> updateUser(UserUpdateRequest request) {// 1. 检查用户是否存在if (userMapper.findById(request.getUserId()) == null) {return BaseResponse.ofError(UserErrorCode.USER_NOT_FOUND.getCode(), UserErrorCode.USER_NOT_FOUND.getDesc());}// 2. 准备更新数据User userToUpdate = new User();BeanUtils.copyProperties(request, userToUpdate);userToUpdate.setId(request.getUserId()); // 确保ID已设置// 3. 如果传入了新密码,进行加密处理if (StringUtils.hasText(request.getPassWd())) {// !!安全警告!!: 生产环境必须使用BCrypt等哈希算法对新密码加密userToUpdate.setPasswd(request.getPassWd()); // 此处为新手项目简化}// 4. 执行更新int updatedRows = userMapper.updateUser(userToUpdate);return BaseResponse.ofSuccess(updatedRows > 0);}// ... 其他接口方法的实现 ...
}
3. api
网关实现
Controller
层已有对应接口,我们只需打通Integration
和Manager
层。
文件: integration/src/main/java/com/newbie/integration/service/impl/UserIntegrationServiceImpl.java
(修改)
- 实现
updateUser
的远程调用。
// ...
import com.newbie.contract.request.UserUpdateRequest;@Service("userIntegrationService")
public class UserIntegrationServiceImpl implements UserIntegrationService {// ...@Overridepublic Boolean updateUser(UserUpdateDTO userUpdateDTO) {// 注意:接口定义用的是UserUpdateDTO,我们需要转换UserUpdateRequest request = new UserUpdateRequest();BeanUtils.copyProperties(userUpdateDTO, request);BaseResponse<Boolean> response = userService.updateUser(request);if (!response.isSuccess()) {throw new RuntimeException(response.getMessage());}return response.getData();}// ...
}
文件: api/src/main/java/com/newbie/api/manager/UserManager.java
(修改)
- 实现
updateUser
的门面方法。
// ...
import com.newbie.contract.dto.UserUpdateDTO;
import com.newbie.contract.request.UserUpdateRequest;@Component
public class UserManager {// ...public Boolean updateUser(UserUpdateRequest request) {// Manager层负责将Controller的Request适配成Integration层需要的DTOUserUpdateDTO updateDTO = new UserUpdateDTO();BeanUtils.copyProperties(request, updateDTO);return userIntegrationService.updateUser(updateDTO);}// ...
}
- 解释:
Controller
,Manager
,Integration
之间的数据对象转换是这一层的主要工作之一,它确保了各层之间的解耦。
七、查询用户信息功能实现
这是一个只读操作,相对简单。
1. Repository层代码
findById
方法我们已在上面添加,可以直接复用。但为了安全,我们最好再创建一个专门用于查询用户信息、不包含密码字段的查询。
文件: repository/src/main/resources/mybatis/mapper/UserMapper.xml
(修改)
- 我们在
findById
中明确列出查询字段,不包含passwd
,这是个好习惯。
<select id="findById" resultType="com.newbie.repository.entity.User">SELECTid, user_name, gender, mobile, receive_name, shipping_addressFROMuserWHEREid = #{id} AND is_deleted = 0;</select>
2. user
微服务实现
文件: user/src/main/java/com/newbie/user/service/UserServiceImpl.java
(修改)
- 实现
userInfo
接口方法。
// ...
import com.newbie.contract.dto.UserDTO;
import com.newbie.contract.request.UserIdRequest;@DubboService(timeout = 5000)
public class UserServiceImpl implements UserService {// ...@Overridepublic BaseResponse<UserDTO> userInfo(UserIdRequest request) {User user = userMapper.findById(request.getUserId());if (user == null) {return BaseResponse.ofError(UserErrorCode.USER_NOT_FOUND.getCode(), UserErrorCode.USER_NOT_FOUND.getDesc());}// 将数据库实体Entity转换为不含敏感信息的DTO返回给调用方UserDTO userDTO = new UserDTO();BeanUtils.copyProperties(user, userDTO);userDTO.setUserId(user.getId()); // 确保userId被正确复制return BaseResponse.ofSuccess(userDTO);}// ...
}
- 解释:这里的核心是数据转换(Entity -> DTO)。永远不要将包含密码等敏感信息的数据库实体直接返回给外部调用者,
DTO
(数据传输对象)正是为此而生。
3. api
网关实现
文件: integration/src/main/java/com/newbie/integration/service/impl/UserIntegrationServiceImpl.java
(修改)
// ...
import com.newbie.contract.dto.UserDTO;@Service("userIntegrationService")
public class UserIntegrationServiceImpl implements UserIntegrationService {// ...@Overridepublic UserDTO userInfo(Long userId) {UserIdRequest request = new UserIdRequest();request.setUserId(userId);BaseResponse<UserDTO> response = userService.userInfo(request);if (!response.isSuccess()) {throw new RuntimeException(response.getMessage());}return response.getData();}// ...
}
文件: api/src/main/java/com/newbie/api/manager/UserManager.java
(修改)
// ...
import com.newbie.contract.dto.UserDTO;@Component
public class UserManager {// ...public UserDTO userInfo(Long userId) {return userIntegrationService.userInfo(userId);}// ...
}
八、商品模块
1、实现 Repository (数据访问层)
这是所有功能的基础,我们需要在共享的repository
模块中创建与goods
表对应的Entity
和Mapper
。
文件: repository/src/main/java/com/newbie/repository/entity/Goods.java
(新建)
这个类对应goods.sql
的表结构。
package com.newbie.repository.entity;import lombok.Data;
import java.math.BigDecimal;
import java.sql.Timestamp;@Data
public class Goods {private Long id;private String goodsName;private String goodsDesc;private BigDecimal price; // 注意:数据库是BIGINT,这里用BigDecimal更精确,稍后处理类型转换private Long quantity;private Long soldQuantity;private String thumbUrl;private String imageUrl;private Boolean isDeleted;private Timestamp gmtCreated;private Timestamp gmtModified;
}
文件: repository/src/main/java/com/newbie/repository/mapper/GoodsMapper.java
(新建)
定义所有需要的数据库操作方法。
package com.newbie.repository.mapper;import com.newbie.repository.entity.Goods;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface GoodsMapper {// 根据ID查询商品详情Goods findById(@Param("id") Long id);// 根据ID列表查询商品List<Goods> findByIds(@Param("ids") List<Long> ids);// 插入新商品int insertGoods(Goods goods);// 更新商品信息int updateGoods(Goods goods);// 逻辑删除商品int deleteGoodsById(@Param("id") Long id);
}
文件: repository/src/main/resources/mybatis/mapper/GoodsMapper.xml
(新建)
提供上述接口方法的具体SQL实现。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newbie.repository.mapper.GoodsMapper"><select id="findById" resultType="com.newbie.repository.entity.Goods">SELECT id, goods_name, goods_desc, price, quantity, sold_quantity, thumb_url, image_url, is_deletedFROM goodsWHERE id = #{id} AND is_deleted = 0;</select><select id="findByIds" resultType="com.newbie.repository.entity.Goods">SELECT id, goods_name, price, quantity, sold_quantity, thumb_url, is_deletedFROM goodsWHERE id IN<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>AND is_deleted = 0;</select><insert id="insertGoods" parameterType="com.newbie.repository.entity.Goods" useGeneratedKeys="true" keyProperty="id">INSERT INTO goods (goods_name, goods_desc, price, quantity, sold_quantity, thumb_url, image_url)VALUES (#{goodsName}, #{goodsDesc}, #{price}, #{quantity}, #{soldQuantity}, #{thumbUrl}, #{imageUrl});</insert><update id="updateGoods" parameterType="com.newbie.repository.entity.Goods">UPDATE goods<set><if test="goodsName != null">goods_name = #{goodsName},</if><if test="goodsDesc != null">goods_desc = #{goodsDesc},</if><if test="price != null">price = #{price},</if><if test="quantity != null">quantity = #{quantity},</if><if test="thumbUrl != null">thumb_url = #{thumbUrl},</if><if test="imageUrl != null">image_url = #{imageUrl},</if></set>WHERE id = #{id};</update><update id="deleteGoodsById">UPDATE goods SET is_deleted = 1 WHERE id = #{id};</update>
</mapper>
2、实现 goods
微服务 (服务提供方)
创建GoodsServiceImpl
来实现contract
中定义的GoodsService
接口。
文件: goodsth-newbie-goods-ms/src/main/java/com/newbie/goods/service/GoodsServiceImpl.java
(新建)
package com.newbie.goods.service;import com.newbie.contract.GoodsService;
import com.newbie.contract.dto.GoodsDTO;
import com.newbie.contract.dto.GoodsListDTO;
import com.newbie.contract.dto.PreOrderGoodsDTO;
import com.newbie.contract.request.GoodsIdRequest;
import com.newbie.contract.request.GoodsIdsRequest;
import com.newbie.contract.request.GoodsRequest;
import com.newbie.goods.enums.GoodsErrorCode;
import com.newbie.repository.entity.Goods;
import com.newbie.repository.mapper.GoodsMapper;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;@DubboService(timeout = 5000)
public class GoodsServiceImpl implements GoodsService {@Autowiredprivate GoodsMapper goodsMapper;@Overridepublic BaseResponse<GoodsDTO> queryGoodsDetail(GoodsIdRequest request) {Goods goods = goodsMapper.findById(request.getId());if (goods == null) {return BaseResponse.ofSuccess(null);}GoodsDTO goodsDTO = new GoodsDTO();BeanUtils.copyProperties(goods, goodsDTO);goodsDTO.setGoodsId(goods.getId());// 注意:数据库中price是BIGINT,需要转换为BigDecimal,这里假设单位是分goodsDTO.setPrice(new BigDecimal(goods.getPrice()).divide(new BigDecimal(100)));return BaseResponse.ofSuccess(goodsDTO);}@Overridepublic BaseResponse<List<GoodsListDTO>> queryGoodsList(GoodsIdsRequest request) {List<Goods> goodsList = goodsMapper.findByIds(request.getIds());List<GoodsListDTO> dtoList = goodsList.stream().map(goods -> {GoodsListDTO dto = new GoodsListDTO();BeanUtils.copyProperties(goods, dto);dto.setGoodsId(goods.getId());dto.setPrice(new BigDecimal(goods.getPrice()).divide(new BigDecimal(100)));return dto;}).collect(Collectors.toList());return BaseResponse.ofSuccess(dtoList);}@Overridepublic BaseResponse<PreOrderGoodsDTO> queryPreOrderGoodsDetail(GoodsIdRequest request) {// 此功能与查询详情类似,但返回的DTO不同,可根据需要定制Goods goods = goodsMapper.findById(request.getId());if (goods == null) {return BaseResponse.ofSuccess(null);}PreOrderGoodsDTO dto = new PreOrderGoodsDTO();BeanUtils.copyProperties(goods, dto);dto.setGoodsId(goods.getId());dto.setPrice(new BigDecimal(goods.getPrice()).divide(new BigDecimal(100)));return BaseResponse.ofSuccess(dto);}@Overridepublic BaseResponse<Boolean> insertGoods(GoodsRequest request) {Goods goods = new Goods();BeanUtils.copyProperties(request, goods);goods.setPrice(request.getPrice().multiply(new BigDecimal(100)).longValue()); // 将元转为分存储goods.setSoldQuantity(0L); // 新增商品销量为0int result = goodsMapper.insertGoods(goods);if (result <= 0) {return BaseResponse.ofError(GoodsErrorCode.INSTER_ERROR.getCode(), GoodsErrorCode.INSTER_ERROR.getDesc());}return BaseResponse.ofSuccess(true);}@Overridepublic BaseResponse<Boolean> deleteGoods(GoodsIdRequest request) {int result = goodsMapper.deleteGoodsById(request.getId());if (result <= 0) {return BaseResponse.ofError(GoodsErrorCode.DELETE_ERROR.getCode(), GoodsErrorCode.DELETE_ERROR.getDesc());}return BaseResponse.ofSuccess(true);}@Overridepublic BaseResponse<Boolean> updateGoods(GoodsRequest request) {Goods goods = new Goods();BeanUtils.copyProperties(request, goods);goods.setId(request.getGoodsId());if (request.getPrice() != null) {goods.setPrice(request.getPrice().multiply(new BigDecimal(100)).longValue());}int result = goodsMapper.updateGoods(goods);if (result <= 0) {return BaseResponse.ofError(GoodsErrorCode.UPDATE_ERROR.getCode(), GoodsErrorCode.UPDATE_ERROR.getDesc());}return BaseResponse.ofSuccess(true);}
}
- 解释:
sql
文件中price
是BIGINT
类型,通常用来存储以“分”为单位的价格,避免浮点数精度问题。所以在代码中,我们做了“元”和“分”的转换。
3、实现 api
网关 (服务消费方)
文件: integration/src/main/java/com/newbie/integration/service/impl/GoodsIntegrationServiceImpl.java
(新建)
package com.newbie.integration.service.impl;import com.newbie.contract.GoodsService;
import com.newbie.contract.dto.GoodsDTO;
import com.newbie.contract.dto.GoodsListDTO;
import com.newbie.contract.dto.PreOrderGoodsDTO;
import com.newbie.contract.request.GoodsIdRequest;
import com.newbie.contract.request.GoodsIdsRequest;
import com.newbie.contract.request.GoodsRequest;
import com.newbie.integration.service.GoodsIntegrationService;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;import java.util.List;@Service("goodsIntegrationService")
public class GoodsIntegrationServiceImpl implements GoodsIntegrationService {@DubboReference(timeout = 5000, check = false)private GoodsService goodsService;private <T> T getData(BaseResponse<T> response) {if (!response.isSuccess()) {throw new RuntimeException("RPC call failed: " + response.getMessage());}return response.getData();}@Overridepublic GoodsDTO queryGoodsDetail(Long id) {GoodsIdRequest request = new GoodsIdRequest();request.setId(id);return getData(goodsService.queryGoodsDetail(request));}@Overridepublic List<GoodsListDTO> queryGoodsList(List<Long> ids) {GoodsIdsRequest request = new GoodsIdsRequest();request.setIds(ids);return getData(goodsService.queryGoodsList(request));}@Overridepublic PreOrderGoodsDTO queryPreOrderGoodsDetail(Long id) {GoodsIdRequest request = new GoodsIdRequest();request.setId(id);return getData(goodsService.queryPreOrderGoodsDetail(request));}@Overridepublic Boolean insertGoods(GoodsDTO goodsDTO) {GoodsRequest request = new GoodsRequest();BeanUtils.copyProperties(goodsDTO, request);return getData(goodsService.insertGoods(request));}@Overridepublic Boolean deleteGoods(Long id) {GoodsIdRequest request = new GoodsIdRequest();request.setId(id);return getData(goodsService.deleteGoods(request));}@Overridepublic Boolean updateGoods(GoodsDTO goodsDTO) {GoodsRequest request = new GoodsRequest();BeanUtils.copyProperties(goodsDTO, request);return getData(goodsService.updateGoods(request));}
}
文件: api/src/main/java/com/newbie/api/manager/GoodsManager.java
(修改)
package com.newbie.api.manager;import com.newbie.contract.dto.GoodsDTO;
import com.newbie.contract.dto.GoodsListDTO;
import com.newbie.contract.dto.PreOrderGoodsDTO;
import com.newbie.contract.request.GoodsIdRequest;
import com.newbie.contract.request.GoodsRequest;
import com.newbie.integration.service.GoodsIntegrationService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;@Component
public class GoodsManager {@Resourceprivate GoodsIntegrationService goodsIntegrationService;public GoodsDTO query(Long id) {return goodsIntegrationService.queryGoodsDetail(id);}public List<GoodsListDTO> queryByIds(List<Long> ids) {return goodsIntegrationService.queryGoodsList(ids);}public PreOrderGoodsDTO queryPreOrderGoodsDetail(Long id) {return goodsIntegrationService.queryPreOrderGoodsDetail(id);}public Boolean insertGoods(GoodsRequest request) {GoodsDTO goodsDTO = new GoodsDTO();BeanUtils.copyProperties(request, goodsDTO);return goodsIntegrationService.insertGoods(goodsDTO);}public Boolean updateGoods(GoodsRequest request) {GoodsDTO goodsDTO = new GoodsDTO();BeanUtils.copyProperties(request, goodsDTO);return goodsIntegrationService.updateGoods(goodsDTO);}public Boolean deleteGoods(GoodsIdRequest request) {return goodsIntegrationService.deleteGoods(request.getId());}
}
4、rpc-server.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"><dubbo:application name="goodsth-newbie-goods-provider"/><dubbo:registry address="zookeeper://127.0.0.1:2181"/><dubbo:protocol name="dubbo" port="20881"/><bean id="goodsServiceImpl" class="com.newbie.goods.service.GoodsServiceImpl"/><dubbo:service interface="com.newbie.contract.GoodsService" ref="goodsServiceImpl" timeout="5000"/></beans>
配置解释
-
<dubbo:application name="goodsth-newbie-goods-provider"/>
: 为你的goods
服务起一个唯一的应用名,便于服务治理和区分。 -
<dubbo:registry address="..."/>
: 和user
服务一样,将自己注册到同一个Zookeeper实例中,这样消费者(如api
网关)就能在这个注册中心里同时发现user
服务和goods
服务。 -
<dubbo:protocol name="dubbo" port="20881"/>
: 这是非常关键的一点。当你在同一台机器上运行多个Dubbo服务时,它们的RPC端口必须不同。之前我们为user
服务分配了20880
端口,这里我们为goods
服务分配20881
端口,以避免冲突。 -
<bean id="goodsServiceImpl" .../>
: 将我们刚刚编写的、包含所有商品业务逻辑的GoodsServiceImpl
类声明为一个Spring Bean,并给它一个IDgoodsServiceImpl
。 -
<dubbo:service interface="..." ref="..."/>
: 这是“挂牌营业”的核心声明。它告诉Dubbo:interface
: 我要对外提供com.newbie.contract.GoodsService
这个接口中定义的所有服务。ref
: 这些服务的具体实现逻辑,请到ID为goodsServiceImpl
的那个Bean里去找。
配置好这个文件后,当你启动goods
微服务时,它就会成功地将自己注册为一个可以处理商品相关请求的服务提供方了
5、配置与运行
- 数据库: 确保已在MySQL中执行
goods.sql
建表。 - Dubbo配置:
- 在
goodsth-newbie-goods-ms
服务的rpc-server.xml
中,添加对GoodsServiceImpl
的暴露。 - 在
api
服务的rpc-client.xml
中,添加对GoodsService
的引用。
- 在
- 启动: 依次启动Zookeeper、MySQL、
user
服务、goods
服务和api
服务。 - 测试: 使用Postman等工具调用
api
网关暴露的商品相关接口。
九、订单模块
1、Repository层实现 (数据访问)
这部分代码位于共享的repository
模块中,负责与orders
表进行直接交互。
文件: repository/src/main/java/com/newbie/repository/entity/Orders.java
(新建)
package com.newbie.repository.entity;import lombok.Data;
import java.util.Date;// 数据库实体类,与orders表字段对应
@Data
public class Orders {private Long id;private Long userId;private Long goodsId;private String goodsName;private String goodsThumbUrl;private Long goodsPrice; // 以“分”为单位存储,用Long类型避免精度问题private Integer status;private Integer orderAmount;private Date orderTime;private Date payTime;private Date shippingTime;private Date receiveTime;private String receiveName;private String mobile;private String shippingAddress;private Boolean isDeleted;
}
文件: repository/src/main/java/com/newbie/repository/mapper/OrderMapper.java
(新建)
package com.newbie.repository.mapper;import com.newbie.repository.entity.Orders;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;@Repository
public interface OrderMapper {// 根据订单ID查询Orders findById(@Param("id") Long id);// 根据用户ID查询订单列表(可自行扩展分页等功能)List<Orders> findByUserId(@Param("userId") Long userId);// 插入新订单int insertOrder(Orders order);// 更新订单状态(例如支付、发货)int updateOrder(Orders order);
}
文件: repository/src/main/resources/mybatis/mapper/OrderMapper.xml
(新建)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.newbie.repository.mapper.OrderMapper"><select id="findById" resultType="com.newbie.repository.entity.Orders">SELECT * FROM orders WHERE id = #{id} AND is_deleted = 0;</select><select id="findByUserId" resultType="com.newbie.repository.entity.Orders">SELECT * FROM orders WHERE user_id = #{userId} AND is_deleted = 0 ORDER BY id DESC;</select><insert id="insertOrder" parameterType="com.newbie.repository.entity.Orders" useGeneratedKeys="true" keyProperty="id">INSERT INTO orders (user_id, goods_id, goods_name, goods_thumb_url, goods_price, status, order_amount, order_time, receive_name, mobile, shipping_address)VALUES (#{userId}, #{goodsId}, #{goodsName}, #{goodsThumbUrl}, #{goodsPrice}, #{status}, #{orderAmount}, #{orderTime}, #{receiveName}, #{mobile}, #{shippingAddress});</insert><update id="updateOrder" parameterType="com.newbie.repository.entity.Orders">UPDATE orders<set><if test="status != null">status = #{status},</if><if test="payTime != null">pay_time = #{payTime},</if></set>WHERE id = #{id};</update>
</mapper>
2、order
微服务实现 (服务提供方)
这是订单模块的核心,它既是服务提供者,也需要消费user
和goods
服务。
文件: goodsth-newbie-order-ms/src/main/resources/rpc-client.xml
(新建)
- 解释:因为
order
服务需要调用user
和goods
服务,所以它自己也需要一个客户端配置。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://dubbo.apache.org/schema/dubbo http://dubbo.apache.org/schema/dubbo/dubbo.xsd"><dubbo:application name="goodsth-newbie-order-consumer"/><dubbo:registry address="zookeeper://127.0.0.1:2181"/><dubbo:reference id="userService" interface="com.newbie.contract.UserService" check="false" timeout="5000"/><dubbo:reference id="goodsService" interface="com.newbie.contract.GoodsService" check="false" timeout="5000"/>
</beans>
文件: goodsth-newbie-order-ms/src/main/java/com/newbie/order/service/OrderServiceImpl.java
(新建)
package com.newbie.order.service;import com.newbie.contract.GoodsService;
import com.newbie.contract.OrderService;
import com.newbie.contract.UserService;
import com.newbie.contract.dto.*;
import com.newbie.contract.request.*;
import com.newbie.repository.entity.Orders;
import com.newbie.repository.mapper.OrderMapper;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;@DubboService(timeout = 5000)
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderMapper orderMapper;// 关键:注入其他微服务的远程代理@DubboReference(timeout = 5000, check = false)private GoodsService goodsService;@DubboReference(timeout = 5000, check = false)private UserService userService;@Overridepublic BaseResponse<Long> createOrder(OrderRequest request) {// 1. 调用商品服务,获取商品信息并校验库存GoodsIdRequest goodsIdRequest = new GoodsIdRequest();goodsIdRequest.setId(request.getGoodsId());BaseResponse<GoodsDTO> goodsResponse = goodsService.queryGoodsDetail(goodsIdRequest);if (!goodsResponse.isSuccess() || goodsResponse.getData() == null) {return BaseResponse.ofError(-1, "商品不存在");}GoodsDTO goodsDTO = goodsResponse.getData();if (goodsDTO.getQuantity() < request.getOrderAmount()) {return BaseResponse.ofError(-1, "商品库存不足");}// 2. 调用用户服务,获取收货信息UserIdRequest userIdRequest = new UserIdRequest();userIdRequest.setUserId(request.getUserId());BaseResponse<UserDTO> userResponse = userService.userInfo(userIdRequest);if (!userResponse.isSuccess() || userResponse.getData() == null) {return BaseResponse.ofError(-1, "用户不存在");}UserDTO userDTO = userResponse.getData();// 3. (重要) 此处应调用商品服务扣减库存,并与创建订单在同一个分布式事务中完成// 本新手项目简化,暂不实现扣减库存和分布式事务// 4. 创建订单实体Orders order = new Orders();order.setUserId(request.getUserId());order.setGoodsId(request.getGoodsId());order.setOrderAmount(request.getOrderAmount());order.setGoodsName(goodsDTO.getGoodsName());order.setGoodsThumbUrl(goodsDTO.getThumbUrl());order.setGoodsPrice(goodsDTO.getPrice().multiply(new BigDecimal(100)).longValue()); // 元转分order.setReceiveName(userDTO.getReceiveName());order.setMobile(userDTO.getMobile());order.setShippingAddress(userDTO.getShippingAddress());order.setStatus(0); // 0: 待支付order.setOrderTime(new Date());// 5. 插入数据库orderMapper.insertOrder(order);return BaseResponse.ofSuccess(order.getId());}@Overridepublic BaseResponse<OrderDetailDTO> queryOrderDetail(OrderIdRequest request) {Orders order = orderMapper.findById(request.getOrderId());if (order == null) return BaseResponse.ofSuccess(null);OrderDetailDTO dto = new OrderDetailDTO();BeanUtils.copyProperties(order, dto);dto.setOrderId(order.getId());dto.setGoodsPrice(new BigDecimal(order.getGoodsPrice()).divide(new BigDecimal(100))); // 分转元return BaseResponse.ofSuccess(dto);}@Overridepublic BaseResponse<PreOrderDTO> createPreOrderDetail(OrderRequest request) {// ... 此处省略与我上一轮回答中完全相同的代码实现 ...// 逻辑就是分别调用goodsService和userService,然后将数据聚合到PreOrderDTO中return null; // 请参考上一轮回答填充}@Overridepublic BaseResponse<List<OrderListDTO>> queryOrderList(OrderIdsRequest request) {// 在这个项目中,通常是查询某个用户的所有订单,而不是根据订单ID列表// 这里我们先简单实现根据用户ID查询,你可以根据需要调整// 假设request中的ids实际上是userIdif (request.getIds() == null || request.getIds().isEmpty()) {return BaseResponse.ofError(-1, "用户ID不能为空");}Long userId = request.getIds().get(0); // 简化处理,只取第一个ID作为用户IDList<Orders> ordersList = orderMapper.findByUserId(userId);List<OrderListDTO> dtoList = ordersList.stream().map(order -> {OrderListDTO dto = new OrderListDTO();BeanUtils.copyProperties(order, dto);dto.setOrderId(order.getId());dto.setGoodsPrice(new BigDecimal(order.getGoodsPrice()).divide(new BigDecimal(100)));return dto;}).collect(Collectors.toList());return BaseResponse.ofSuccess(dtoList);}@Overridepublic BaseResponse<Boolean> pay(OrderIdRequest request) {Orders order = new Orders();order.setId(request.getOrderId());order.setStatus(1); // 1: 已支付order.setPayTime(new Date());int result = orderMapper.updateOrder(order);return BaseResponse.ofSuccess(result > 0);}
}
3、api
网关实现 (服务消费方)
文件: integration/src/main/java/com/newbie/integration/service/impl/OrderIntegrationServiceImpl.java
(新建)
package com.newbie.integration.service.impl;import com.newbie.contract.OrderService;
import com.newbie.contract.dto.OrderDetailDTO;
import com.newbie.contract.dto.OrderListDTO;
import com.newbie.contract.dto.PreOrderDTO;
import com.newbie.contract.request.OrderIdRequest;
import com.newbie.contract.request.OrderIdsRequest;
import com.newbie.contract.request.OrderRequest;
import com.newbie.integration.service.OrderIntegrationService;
import com.pinpinxiaozhan.service.base.response.BaseResponse;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
import java.util.List;@Service("orderIntegrationService")
public class OrderIntegrationServiceImpl implements OrderIntegrationService {@DubboReference(timeout = 5000, check = false)private OrderService orderService;private <T> T getData(BaseResponse<T> response) {if (!response.isSuccess()) throw new RuntimeException("RPC call failed: " + response.getMessage());return response.getData();}@Overridepublic OrderDetailDTO queryOrderDetail(Long id) {OrderIdRequest request = new OrderIdRequest();request.setOrderId(id);return getData(orderService.queryOrderDetail(request));}@Overridepublic PreOrderDTO preOrderDetail(Long goodsId, Long userId, Integer orderAmount) {OrderRequest request = new OrderRequest();request.setGoodsId(goodsId);request.setUserId(userId);request.setOrderAmount(orderAmount);return getData(orderService.createPreOrderDetail(request));}@Overridepublic List<OrderListDTO> queryOrderList(List<Long> ids) {OrderIdsRequest request = new OrderIdsRequest();request.setIds(ids);return getData(orderService.queryOrderList(request));}@Overridepublic Long createOrder(Long goodsId, Long userId, Integer orderAmount) {OrderRequest request = new OrderRequest();request.setGoodsId(goodsId);request.setUserId(userId);request.setOrderAmount(orderAmount);return getData(orderService.createOrder(request));}@Overridepublic Boolean pay(Long orderId) {OrderIdRequest request = new OrderIdRequest();request.setOrderId(orderId);return getData(orderService.pay(request));}
}
文件: api/src/main/java/com/newbie/api/manager/OrderManager.java
(修改)
package com.newbie.api.manager;import com.newbie.contract.dto.OrderDetailDTO;
import com.newbie.contract.dto.OrderListDTO;
import com.newbie.contract.dto.PreOrderDTO;
import com.newbie.contract.request.OrderIdRequest;
import com.newbie.contract.request.OrderRequest;
import com.newbie.integration.service.OrderIntegrationService;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;@Component
public class OrderManager {@Resourceprivate OrderIntegrationService orderIntegrationService;public OrderDetailDTO queryOrderDetail(Long id) {return orderIntegrationService.queryOrderDetail(id);}public PreOrderDTO preOrderDetail(OrderRequest request) {return orderIntegrationService.preOrderDetail(request.getGoodsId(), request.getUserId(), request.getOrderAmount());}public List<OrderListDTO> queryOrderList(List<Long> ids) {return orderIntegrationService.queryOrderList(ids);}public Long createOrder(OrderRequest request) {return orderIntegrationService.createOrder(request.getGoodsId(), request.getUserId(), request.getOrderAmount());}public Boolean pay(OrderIdRequest request) {return orderIntegrationService.pay(request.getOrderId());}
}
- 解释:以上就是贯穿三层(Controller -> Manager -> Integration)的完整实现,它将前端的HTTP请求,最终转换为对
order
微服务的Dubbo RPC调用。
4、配置与运行
- 数据库: 在MySQL中执行
orders.sql
。 - Dubbo配置:
order
服务:- 在
goodsth-newbie-order-ms
的rpc-server.xml
中,添加对OrderServiceImpl
的<dubbo:service>
暴露,端口设为20882
。 - 确认
goodsth-newbie-order-ms
的rpc-client.xml
已配置对UserService
和GoodsService
的引用。
- 在
api
服务:- 在
api
服务的rpc-client.xml
中,添加对OrderService
的<dubbo:reference>
引用。
- 在
- 启动: 依次启动Zookeeper、MySQL、
user
服务、goods
服务、order
服务和api
服务。 - 测试: 使用Postman测试
api
服务中/api/newbie/order/
下的各个接口。