13 尚品甄选-前台系统-商品详情和注册登录
1 商品详情
1.1 需求分析
需求说明:当点击某一个商品的时候,此时就需要在商品详情页面展示出商品的详情数据,商品详情页所需数据:
1、商品的基本信息
2、当前商品sku的基本信息
3、商品轮播图信息
4、商品详情(详细为图片列表)
5、商品规格信息
6、当前商品sku的规格属性
如下所示:

整体的访问流程如下所示:

查看接口文档:
商品详情接口地址及示例数据
get /api/product/item/{skuId}
返回结果:
{
"code": 200,
"message": "成功",
"data": {
"productSku": {
"id": 1,
"createTime": "2023-05-25 22:21:07",
"skuCode": "1_0",
"skuName": "小米 红米Note10 5G手机 颜色:白色 内存:8G",
"productId": 1,
"thumbImg": "http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1 (1).jpg",
"salePrice": 1999.00,
"marketPrice": 2019.00,
"costPrice": 1599.00,
"stockNum": 99,
"saleNum": 1,
"skuSpec": "颜色:白色,内存:8G",
"weight": "1.00",
"volume": "1.00",
"status": null,
"skuSpecList": null
},
"product": {
"id": 1,
"createTime": "2023-05-25 22:21:07",
"name": "小米 红米Note10 5G手机",
"brandId": 1,
"category1Id": 1,
"category2Id": 2,
"category3Id": 3,
"unitName": "个",
"sliderUrls": "",
"specValue": "[{\"key\":\"颜色\",\"valueList\":[\"白色\",\"红色\",\"黑色\"]},{\"key\":\"内存\",\"valueList\":[\"8G\",\"18G\"]}]",
"status": 1,
"auditStatus": 1,
"auditMessage": "审批通过",
"brandName": null,
"category1Name": null,
"category2Name": null,
"category3Name": null,
"productSkuList": null,
"detailsImageUrls": null
},
"specValueList": [
{
"valueList": [
"白色",
"红色",
"黑色"
],
"key": "颜色"
},
{
"valueList": [
"8G",
"18G"
],
"key": "内存"
}
],
"detailsImageUrlList": [
"http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-6_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-4_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-1_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1 (1).jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-3_u_1.jpg"
],
"skuSpecValueMap": {
"白色 + 12G": 13,
"白色 + 8G": 12
},
"sliderUrlList": [
"http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-6_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-4_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-1_u_1.jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-5_u_1 (1).jpg",
"http://139.198.127.41:9000/spzx/20230525/665832167-3_u_1.jpg"
]
}
}
1.2 接口开发
操作模块:service-product
1.2.1 ProductItemVo
封装接口返回的数据对象:
@Data
@Schema(description = "商品详情对象")
public class ProductItemVo {
@Schema(description = "商品sku信息")
private ProductSku productSku;
@Schema(description = "商品信息")
private Product product;
@Schema(description = "商品轮播图列表")
private List<String> sliderUrlList;
@Schema(description = "商品详情图片列表")
private List<String> detailsImageUrlList;
@Schema(description = "商品规格信息")
private JSONArray specValueList;
@Schema(description = "商品规格对应商品skuId信息")
private Map<String,Object> skuSpecValueMap;
}
1.2.2 ProductController
表现层代码:
@Operation(summary = "商品详情")
@GetMapping("item/{skuId}")
public Result<ProductItemVo> item(@Parameter(name = "skuId", description = "商品skuId", required = true) @PathVariable Long skuId) {
ProductItemVo productItemVo = productService.item(skuId);
return Result.build(productItemVo , ResultCodeEnum.SUCCESS);
}
1.2.3 ProductService
业务层代码实现
// 业务接口
ProductItemVo item(Long skuId);
// 接口实现类
@Autowired
private ProductMapper productMapper;
@Autowired
private ProductDetailsMapper productDetailsMapper;
@Override
public ProductItemVo item(Long skuId) {
//当前sku信息
ProductSku productSku = productSkuMapper.getById(skuId);
//当前商品信息
Product product = productMapper.getById(productSku.getProductId());
//同一个商品下面的sku信息列表
List<ProductSku> productSkuList = productSkuMapper.findByProductId(productSku.getProductId());
//建立sku规格与skuId对应关系
Map<String,Object> skuSpecValueMap = new HashMap<>();
productSkuList.forEach(item -> {
skuSpecValueMap.put(item.getSkuSpec(), item.getId());
});
//商品详情信息
ProductDetails productDetails = productDetailsMapper.getByProductId(productSku.getProductId());
ProductItemVo productItemVo = new ProductItemVo();
productItemVo.setProductSku(productSku);
productItemVo.setProduct(product);
productItemVo.setDetailsImageUrlList(Arrays.asList(productDetails.getImageUrls().split(",")));
productItemVo.setSliderUrlList(Arrays.asList(product.getSliderUrls().split(",")));
productItemVo.setSpecValueList(JSON.parseArray(product.getSpecValue()));
productItemVo.setSkuSpecValueMap(skuSpecValueMap);
return productItemVo;
}
1.2.4 根据skuId获取ProductSku
ProductSkuMapper
ProductSku getById(Long id);
ProductSkuMapper.xml
在映射文件中定义对应的sql语句
<select id="getById" resultMap="productSkuMap">
select <include refid="columns" />
from product_sku
where
id = #{id}
</select>
1.2.5 根据商品id获取Product
ProductMapper
@Mapper
public interface ProductMapper {
Product getById(Long id);
}
ProductMapper.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.atguigu.spzx.product.mapper.ProductMapper">
<resultMap id="productMap" type="com.atguigu.spzx.model.entity.product.Product" autoMapping="true">
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,name,brand_id,category1_id,category2_id,category3_id,unit_name,slider_urls,spec_value,status,audit_status,audit_message,create_time,update_time,is_deleted
</sql>
<select id="getById" resultMap="productMap">
select <include refid="columns" />
from product
where
id = #{id}
</select>
</mapper>
1.2.6 根据商品id获取ProductSku列表
ProductSkuMapper
List<ProductSku> findByProductId(Long productId);
ProductSkuMapper.xml
在映射文件中定义对应的sql语句
<select id="findByProductId" resultMap="productSkuMap">
select <include refid="columns" />
from product_sku
where
product_id = #{productId}
</select>
1.2.7 根据商品id获取ProductDetails
ProductDetailsMapper
@Mapper
public interface ProductDetailsMapper {
ProductDetails getByProductId(Long productId);
}
ProductDetailsMapper.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.atguigu.spzx.product.mapper.ProductDetailsMapper">
<resultMap id="productDetailsMap" type="com.atguigu.spzx.model.entity.product.ProductDetails" autoMapping="true">
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,product_id,image_urls,create_time,update_time,is_deleted
</sql>
<select id="getByProductId" resultMap="productDetailsMap">
select <include refid="columns" />
from product_details
where
product_id = #{productId}
</select>
</mapper>
2 用户注册
在完成购物车模块之前需要先完成注册和登录功能,尚品甄选项目只允许用户在登录状态下把商品添加到购物车。
2.1 需求分析
需求说明:用户注册可采用手机号码或邮箱注册,当前我们使用手机号码注册
- 所涉及数据:
1、用户名(当前只做手机号码注册)
2、手机验证码
3、密码与确认密码
4、昵称
- 所涉及2个接口:
1、获取手机验证码
2、提交注册
如图所示:

查看接口文档:
获取手机验证码接口地址
get /api/user/sms/{phone}
注册接口地址
post /api/user/userInfo/register
参数:
{
"username": "15019685678",
"password": "111111",
"nickName": "晴天",
"code": "6799"
}
2.2 手机验证
2.2.1 云市场-短信API
开通三网106短信
在阿里云云市场搜索“短信”,一般都可用,选择一个即可,例如如下:点击“立即购买”开通
这里开通的是【短信验证码- 快速报备签名】

获取开发参数
登录云市场控制台,在已购买的服务中可以查看到所有购买成功的API商品情况,下图红框中的就是AppKey/AppSecret,AppCode的信息。

API方式使用云市场服务
参考如下例子,复制代码在test目录进行测试

2.2.2 发送短信流程说明
发送短信验证码的流程如下所示:

查看接口文档:
get /api/user/sms/{phone}
2.2.3 user微服务环境搭建
实现步骤:
- 1、在spzx-service模块下创建对应的service-user微服务,并加入如下的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
- 2、在service-user服务的src/resources目录下创建application.yml、application-dev.yml文件,如下所示:
application.yml
spring:
profiles:
active: dev
application-dev.yml
server:
port: 8512
spring:
application:
name: service-user
cloud:
nacos:
discovery:
server-addr: 192.168.136.142:8848
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.136.142:3306/db_spzx?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
username: root
password: 1234
data:
redis:
host: 192.168.136.142
port: 6379
password: 1234
mybatis:
config-location: classpath:mybatis-config.xml
mapper-locations: classpath:mapper/*/*.xml
- 3、导入课程资料中提供的:mybatis-config.xml以及logback-spring.xml配置文件,修改输出路径:
<property name="log.path" value="D://logs//service-user//logs" />
- 4、启动类创建
package com.atguigu.spzx.user;
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
2.2.4 发送短信接口开发
SmsController
表现层代码:
// com.atguigu.spzx.user.controller;
@RestController
@RequestMapping("api/user/sms")
public class SmsController {
@Autowired
private SmsService smsService ;
@GetMapping(value = "/sendCode/{phone}")
public Result sendValidateCode(@PathVariable String phone) {
smsService.sendValidateCode(phone);
return Result.build(null , ResultCodeEnum.SUCCESS) ;
}
}
SmsService
- 复制http工具类到common-utils模块
common-utils引入依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.3.7.v20160115</version>
</dependency>
common-utils复制下面代码
package com.atguigu.spzx.utils;
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
Map<String, String> bodys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
String body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys,
byte[] body)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method,
Map<String, String> headers,
Map<String, String> querys)
throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] { tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
- 业务层接口方法
package com.atguigu.spzx.user.service.impl;
@Service
public class SmsServiceImpl implements SmsService {
@Autowired
private RedisTemplate<String , String> redisTemplate ;
@Override
public void sendValidateCode(String phone) {
String code = redisTemplate.opsForValue().get("phone:code:" + phone);
if(StringUtils.hasText(code)) {
return;
}
String validateCode = RandomStringUtils.randomNumeric(4); // 生成验证码
redisTemplate.opsForValue().set("phone:code:" + phone , validateCode , 5 , TimeUnit.MINUTES);
sendSms(phone , validateCode) ;
}
// 发送短信方法
public void sendSms(String phone, String validateCode) {
String host = "https://dfsns.market.alicloudapi.com";
String path = "/data/send_sms";
String method = "POST";
String appcode = "938a8491cea74e43a48edf0a67070051";
Map<String, String> headers = new HashMap<String, String>();
//最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
headers.put("Authorization", "APPCODE " + appcode);
//根据API的要求,定义相对应的Content-Type
headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
Map<String, String> querys = new HashMap<String, String>();
Map<String, String> bodys = new HashMap<String, String>();
bodys.put("content", "code:"+validateCode);
bodys.put("template_id", "CST_ptdie100");
bodys.put("phone_number", phone);
try {
/**
* 重要提示如下:
* HttpUtils请从
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
* 下载
*
* 相应的依赖请参照
* https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
*/
HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
System.out.println(response.toString());
//获取response的body
//System.out.println(EntityUtils.toString(response.getEntity()));
} catch (Exception e) {
e.printStackTrace();
throw new GuiguException(ResultCodeEnum.SYSTEM_ERROR);
}
}
}
spzx-server-gateway
网关配置user微服务的路由规则:
spring:
cloud:
gateway:
routes:
- id: service-user
uri: lb://service-user
predicates:
- Path=/*/user/**