苍穹外卖开发日志Day07——HttpClient及微信小程序开发


2025.07.24

一、HttpClient

1.1 HttpClient简介

  • HttpClient是Apache提供的一个开源的HTTP客户端库,用于发送HTTP请求和处理HTTP响应。它提供了一个简单的API来执行HTTP请求,支持多种HTTP方法(如GET、POST、PUT、DELETE等),并且可以处理Cookies、重定向、代理等功能。简言之就是通过编码的方式来发送HTTP请求。
  • 核心API:
    • HttpClient:用于发送HTTP请求和接收响应。
    • HttpClients:用于创建HttpClient实例。
    • CloseableHttpClient:一个可关闭的HTTP客户端,用于发送请求和接收响应。
    • HttpGetHttpPost等:用于构建不同类型的HTTP请求。
  • 发送请求步骤:
    1. 创建HttpClient实例。
    2. 创建Http请求对象(如HttpGetHttpPost)。
    3. 调用HttpClient的execute方法发送请求。

1.2 HttpClient使用示例

  • 导入依赖:(实际上AliyunOSS的SDK已经包含了HttpClient,所以不需要单独添加依赖)
1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
  • 发送GET请求示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@SpringBootTest
public class HttpClientTest {

/**
* 通过HttpClient发送GET方式请求
*/
@Test
public void testGET() throws IOException {
// 1. 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

// 2. 创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8082/user/shop/status");

// 3. 发送请求
CloseableHttpResponse response = httpClient.execute(httpGet);

// 4. 获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码是:" + statusCode);

HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据是:" + body);

// 关闭资源
response.close();
httpClient.close();
}
}
  • 测试结果:

    出现问题的原因:Redis服务未启动

  • 解决方法:启动Redis服务后再次测试

  • 发送POST请求示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@SpringBootTest
public class HttpClientTest {

/**
* 通过HttpClient发送POST方式请求
*/
@Test
public void testPost() throws IOException {
// 1. 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

// 2. 创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");

StringEntity entity = new StringEntity(jsonObject.toString());
// 指定请求编码方式
entity.setContentEncoding("utf-8");
// 制定传输数据格式
entity.setContentType("application/json");

httpPost.setEntity(entity);

// 3. 发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);

// 4. 解析返回结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("服务端返回的状态码是:" + statusCode);

HttpEntity httpEntity = response.getEntity();
String body = EntityUtils.toString(httpEntity);
System.out.println("服务端返回的数据是:" + body);

// 关闭资源
response.close();
httpClient.close();
}
}
  • 测试结果:

    出现问题的原因:之前使用了反向代理单独设置了登陆端口为8080,而不是8082
    1
    2
    3
    4
    5
    6
    7
    8
    server {
    listen 8082;
    server_name localhost;

    location /api/ {
    proxy_pass http://localhost:8080/admin/; # 反向代理
    }
    }
  • 解决方法:修改端口号后再次测试

二、微信小程序开发

2.1 小程序简介

  • 微信小程序是腾讯公司推出的一种轻量级应用,用户可以通过微信直接使用,无需下载安装。小程序具有快速加载、易于分享和使用等特点,适用于各种场景,如电商、社交、工具等。
  • 微信小程序公众平台:https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN
  • 开放注册范围:个人、企业、政府、媒体等均可注册小程序。
  • 开发支持:
    • 开发文档
    • 开发者工具
    • 设计指南
    • 小程序体现DEMO
  • 接入流程:
    1. 注册:在微信公众平台注册小程序账号,完成注册后可以同步进行信息完善和开发
    2. 小程序信息完善:填写小程序基本信息,包括名称、头像、介绍及服务范围等
    3. 开发小程序:完成小程序开发者绑定,开发信息配置后,开发者可下载开发工具、参考开发文档进行小程序的开发和测试
    4. 提交审核和发布:完成小程序开发后,提交代码至微信团队审核,审核通过后即可发布(公测期间不能发布)

2.2 准备工作

2.3 入门案例

  • 操作步骤

    • 了解小程序目录结构
    • 编写小程序代码
    • 编译小程序
  • 小程序目录结构

    • 小程序包含一个描述整体程序的app和多个描述各自页面的page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下:
      小程序目录结构
    • 一个小程序页面由四个文件组成:
      小程序页面组成

三、微信登录

3.1 导入小程序

  • 导入小程序:
    小程序导入
  • 更改端口号:修改common/vendor.js文件中的端口号为8082
    小程序端口号修改

3.2 微信登录流程

  • 微信登录流程:
    小程序登录流程
  • 说明:
    • 调用wx.login()获取 临时登录凭证code ,并回传到开发者服务器。
    • 调用auth.code2Session接口,换取 用户唯一标识 OpenID、用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号)和会话密钥 session_key。
      之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
  • 注意事项:
    • 会话密钥session_key是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
    • 临时登录凭证code只能使用一次
  • 登录测试:
    小程序登录测试

3.3 需求分析和设计

  • 产品原型
  • 业务规则
    • 基于微信登录实现小程序的登录功能
    • 如果是新用户需要自动完成注册
  • 接口设计
  • 数据库设计(user表):

3.4 代码开发

  • 配置配置项:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 配置微信登录所需配置项 -->
    sky:
    wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}

    <!-- 配置为微信用户生成jwt令牌时使用的配置项 -->
    sky:
    jwt:
    user-secrt-key: itheima
    user-ttl: 7200000
    user-token-name: authentication

3.4.1 UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {

@Autowired
private UserService userService;

@Autowired
private JwtProperties jwtProperties;

/**
* 微信登录
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}", userLoginDTO.getCode());
// 微信登录
User user = userService.wxLogin(userLoginDTO);

// 为用户生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID, user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);

UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}

3.4.2 UserService

1
2
3
4
5
6
7
8
9
public interface UserService {

/**
* 微信登录
* @param userLoginDTO
* @return
*/
User wxLogin(UserLoginDTO userLoginDTO);
}

3.4.3 UserServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Service
@Slf4j
public class UserServiceImpl implements UserService {

// 微信接口服务地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

@Autowired
private WeChatProperties weChatProperties;

@Autowired
private UserMapper userMapper;

/**
* 微信登录
* @param userLoginDTO
* @return
*/
@Override
public User wxLogin(UserLoginDTO userLoginDTO) {
String openid = getOpenid(userLoginDTO.getCode());

// 校验openid是否为空,若为空则登录失败,抛出业务异常
if (openid == null || openid.equals("")) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}

// 判断当前用户是否为新用户
User user = userMapper.getByOpenId(openid);

// 如果是新用户,自动完成注册
if (user == null) {
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}

// 返回这个用户对象
return user;
}

/**
* 调用微信接口服务获取当前微信用户的openid
* @param code
* @return
*/
private String getOpenid(String code) {
// 调用微信接口服务获取当前微信用户的openid
HashMap<String, String> map = new HashMap<>();
map.put("appid", weChatProperties.getAppid());
map.put("secret", weChatProperties.getSecret());
map.put("js_code", code);
map.put("grant_type", "authorization_code");
String json = HttpClientUtil.doGet(WX_LOGIN, map);

JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");

return openid;
}
}

3.4.4 UserMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Mapper
public interface UserMapper {

/**
* 根据openId查询用户是否为新用户
* @param openId
* @return
*/
@Select("select * from user where openid = #{openid}")
User getByOpenId(String openid);

/**
* 存储用户信息
* @param user
*/
void insert(User user);
}

3.4.5 UserMapper.xml

1
2
3
4
5
6
7
8
9
10
<?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.sky.mapper.UserMapper">

<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user (openid, name, phone, sex, id_number, avatar, create_time)
values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
</insert>
</mapper>

3.4.6 JwtTokenUserInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户id:", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

3.4.7 WebMvcConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;

/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");

registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}

/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}

/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

/**
* 扩展Spring MVC框架的消息转换器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
// 需要为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json对象
converter.setObjectMapper(new JacksonObjectMapper());
// 将自己的消息转换器加入到容器中,并设置优先级(索引)
converters.add(0, converter);
}
}

3.5 功能测试

  • 测试微信登录功能:
    微信登录功能测试

四、商品浏览

4.1 商品浏览——功能开发

  • 产品原型
  • 接口设计
    • 查询分类
    • 根据分类id查询菜品
    • 根据分类id查询套餐
    • 根据套餐id查询包含的菜品

4.2 商品浏览——代码实现

4.2.1 CategoryController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController("userCategoryController")
@RequestMapping("/user/category")
@Api(tags = "C端-分类接口")
public class CategoryController {

@Autowired
private CategoryService categoryService;

/**
* 查询分类
* @param type
* @return
*/
@GetMapping("/list")
@ApiOperation("查询分类")
public Result<List<Category>> list(Integer type) {
List<Category> list = categoryService.list(type);
return Result.success(list);
}
}

4.2.2 DishController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;

/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

List<DishVO> list = dishService.listWithFlavor(dish);

return Result.success(list);
}
}

4.2.3 DishService

1
2
3
4
5
6
7
8
9
public interface DishService {

/**
* 条件查询菜品和口味
* @param dish
* @return
*/
List<DishVO> listWithFlavor(Dish dish);
}

4.2.4 DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
public class DishServiceImpl implements DishService {

@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private SetmealMapper setmealMapper;


/**
* 条件查询菜品和口味
* @param dish
* @return
*/
public List<DishVO> listWithFlavor(Dish dish) {
List<Dish> dishList = dishMapper.list(dish);

List<DishVO> dishVOList = new ArrayList<>();

for (Dish d : dishList) {
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(d,dishVO);

//根据菜品id查询对应的口味
List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());

dishVO.setFlavors(flavors);
dishVOList.add(dishVO);
}

return dishVOList;
}
}

4.2.5 SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
@Autowired
private SetmealService setmealService;

/**
* 条件查询
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
public Result<List<Setmeal>> list(Long categoryId) {
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setStatus(StatusConstant.ENABLE);

List<Setmeal> list = setmealService.list(setmeal);
return Result.success(list);
}

/**
* 根据套餐id查询包含的菜品列表
*
* @param id
* @return
*/
@GetMapping("/dish/{id}")
@ApiOperation("根据套餐id查询包含的菜品列表")
public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
List<DishItemVO> list = setmealService.getDishItemById(id);
return Result.success(list);
}
}

4.2.6 SetmealService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface SetmealService {

/**
* 条件查询
* @param setmeal
* @return
*/
List<Setmeal> list(Setmeal setmeal);

/**
* 根据id查询菜品选项
* @param id
* @return
*/
List<DishItemVO> getDishItemById(Long id);

}

4.2.7 SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 套餐业务实现
*/
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;

/**
* 条件查询
* @param setmeal
* @return
*/
public List<Setmeal> list(Setmeal setmeal) {
List<Setmeal> list = setmealMapper.list(setmeal);
return list;
}

/**
* 根据id查询菜品选项
* @param id
* @return
*/
public List<DishItemVO> getDishItemById(Long id) {
return setmealMapper.getDishItemBySetmealId(id);
}
}

4.2.8 SetmealMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Mapper
public interface SetmealMapper {

/**
* 动态条件查询套餐
* @param setmeal
* @return
*/
List<Setmeal> list(Setmeal setmeal);

/**
* 根据套餐id查询菜品选项
* @param setmealId
* @return
*/
@Select("select sd.name, sd.copies, d.image, d.description " +
"from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
"where sd.setmeal_id = #{setmealId}")
List<DishItemVO> getDishItemBySetmealId(Long setmealId);

}

4.2.9 SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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.sky.mapper.SetmealMapper">

<select id="list" parameterType="Setmeal" resultType="Setmeal">
select * from setmeal
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
</mapper>

4.3 商品浏览——功能测试

  • 测试商品浏览功能:
    商品浏览功能测试