史上最完整的《苍穹外卖》项目实操笔记系列【中篇】,跟视频的每一P对应,全系列10万字,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。
《苍穹外卖》项目实操笔记【上】:P1~P65《苍穹外卖》项目实操笔记【上】
《苍穹外卖》项目实操笔记【下】:P123~P189《苍穹外卖》项目实操笔记【下】
本章思路:HttpClient -> 微信小程序开发 -> 微信登录 -> 导入商品浏览功能代码
在微信登录实现的时候需要HttpClient。要开发用户端的微信小程序,方便用户点餐。
介绍:在Java中通过编码的方式发送HTTP请求。
maven坐标:
org.apache.httpcomponents
httpclient
4.5.13
要保证当前项目已经提前启动好了。
在key-server/src/test/java/com/sky/test下面创建HttpClientTest类然后写入如下代码:
public class HttpClientTest{
@Test
public void testGET() throws Exception {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求
CloseableHttpResponse response = httpClient.execute(httpGet);
//获取服务端返回的状态码
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();
}
}
效果图被删了。
要保证当前项目已经提前启动好了。POST需要提前传入参数。
在上一节的HttpClientTest类中写入如下的代码:
@Test
public void testPOST() throws Exception{
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
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);
//发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+statusCode);
HttpEntity entity1 = response.getEntity();
String body = EntityUtils.toString(entity1);
System.out.println("响应数据为:"+body);
//关闭资源
response.close();
httpClient.close();
}
与Get不同的是,这里添加了登录的参数以及请求编码的方式和数据的格式,其它的基本都是相同的。
这个其实是调用了登录的API,最后会返回一个Token。
首先要注册,个人方式注册无法开通支付权限。小程序信息完善。开发小程序。提交审核和发布。
开发支持:开发文档,开发者工具,设计指南,小程序体验DEMO。
准备工作:①注册小程序,②完善小程序信息,③下载开发者工具。
小程序 (qq.com)
然后在设置里的基本设置栏输入小程序基本的信息。
在开发管理菜单可以获取APPID和小程序秘钥:
AppID(小程序ID):wx3910b10fd7db38d1
AppSecret(小程序密钥):f274884c2a56466016ebbcf009404e63
下载微信开发者工具。
也可以直接使用资料中的安装包。
扫一扫登录,点击+号创建小程序,注意填写AppId:
下面是开发界面:
可以点击选择上面的面板,点击详情-本地设置-勾选不校验合法域名(因为在开发阶段不勾选会导致请求发送不出去):
app.js存放的是业务代码,小程序的逻辑。
app.json存放的是配置文件,小程序的公共配置。
app.wxss是小程序的公共样式表。
上述这些会放在pages目录下面。
wxml是小程序的页面结构。
效果:点击获取用户信息就会弹出申请框,点击允许之后,就可以显示昵称和头像图片:
首先要清空pages/index下面的index.js和index.wxml页面中的内容。
index.wxml中的代码如下:
{{msg}}
昵称: {{nickName}}
这个文件主要是负责定义前端展示的元素,比如{{}}是文本内插的符号,按钮用
type为default显示的是绿色。primary为绿色外框白色文字。warn为白色外框红色文字。
index.js中的代码如下:
Page({
data:{
msg:'hello world',
nickName: '',
url:''
},
//获取微信用户的头像和昵称
getUserInfo(){
wx.getUserProfile({
desc: '获取用户信息',
success: (res)=>{
console.log(res.userInfo);
//为数据赋值
this.setData({
nickName: res.userInfo.nickName,
url: res.userInfo.avatarUrl
})
}
})
}
})
这个文件就是具体逻辑代码,比如data用于定于变量,然后getUserInfo就是上面定义的触发事件。
前端的效果:点击微信登录,会显示授权码。在控制台会输出每次的授权码。
index.mxml代码如下:
{{msg}}
昵称: {{nickName}}
授权码:{{code}}
index.js代码如下:
Page({
data:{
msg:'hello world',
nickName: '',
url:'',
code:''
},
//获取微信用户的头像和昵称
getUserInfo(){
wx.getUserProfile({
desc: '获取用户信息',
success: (res)=>{
console.log(res.userInfo);
//为数据赋值
this.setData({
nickName: res.userInfo.nickName,
url: res.userInfo.avatarUrl
})
}
})
},
//微信登录,获取微信用户的授权码
wxLogin(){
wx.login({
success : (res)=>{
console.log(res.code)
this.setData({
code:res.code
})
}
})
}
})
点击发送请求会向http://localhost:8080/user/shop/status发送查询,返回店铺状态。
index.js完整代码如下:
Page({
data:{
msg:'hello world',
nickName: '',
url:'',
code:''
},
//获取微信用户的头像和昵称
getUserInfo(){
wx.getUserProfile({
desc: '获取用户信息',
success: (res)=>{
console.log(res.userInfo);
//为数据赋值
this.setData({
nickName: res.userInfo.nickName,
url: res.userInfo.avatarUrl
})
}
})
},
//微信登录,获取微信用户的授权码
wxLogin(){
wx.login({
success : (res)=>{
console.log(res.code)
this.setData({
code:res.code
})
}
})
},
//发送请求
sendRequest(){
wx.request({
url: 'http://localhost:8080/user/shop/status',
method:'GET',
success:(res)=>{
console.log(res.data) //data代表整个响应回来的数据
}
})
}
})
index.wxml完整代码如下:
{{msg}}
昵称: {{nickName}}
授权码:{{code}}
点击右上角的微信开发者工具:
点击上传按钮:
然后可以在版本管理界面看到,开发版本多了一项:
小程序代码是在day06文件夹中,需要事先解压一下:
然后在小程序开发助手点击导入:
这里要确保AppID是自己的:
下面是微信登录的官方文档:
开放能力 / 用户信息 / 小程序登录 (qq.com)
点击上面的链接可以进入到如下界面:
下面是请求的参数:
使用PostMan进行测试,成功:
也可以直接在浏览器地址栏中构建:
微信登录流程:小程序要调用wx.login获取code授权码,然后要调研wx.request()发送code授权码。开发者服务器要向微信接口服务提交appid+appsecret+code,然后就可以获得session_key(会话密钥)和openid。然后开发者服务器可以自定义登录状态,产生一个token令牌,然后返回自定义登录状态。小程序可以把token令牌进行存储。然后wx.request可以法器业务请求,开发者服务器可以解析token,最终返回数据。
小程序给后端发送授权码code,然后后端去调微信的接口服务,后端再给用户返回令牌,令牌里包含用户的唯一标识。
数据库设计如下:
在applicaton-dev.yml中写入如下代码:
sky:
wechat:
appid: wx3910b10fd7db38d1
secret: f274884c2a56466016ebbcf009404e63
在application.yml中写入如下代码:
sky:
wechat:
appid: ${sky.wechat.appit}
secret: ${sky.wechat.secret}
在sky: jwt:下面加上如下代码:
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication
在sky-server的controller下的user下创建UserController,写入如下代码:
@RestController
@RequestMapping("/user/user")
@Api(tags="C端用户相关接口")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
//微信登录
@PostMapping("/login")
@ApiOperation("微信登录")
public Result login(@RequestBody UserLoginDTO userLoginDTO){
log.info("微信用户登录;{}",userLoginDTO.getCode());
//微信登录
User user = userService.wxLogin(userLoginDTO);
//为微信用户生成jwt令牌
Map 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);
}
}
然后在sky-server的service下创建UserService,写入如下代码:
public interface UserService {
//微信登录
User wxLogin(UserLoginDTO userLoginDTO);
}
本节是在上节的基础上继续对service的实现类进行具体的实现:
在service曾的impl包下创建UserServiceImpl实现类,写入如下的代码:
@Service
@Slf4j
public class UserServiceImpl implements UserService {
//微信服务接口地址
public static final String WX_LOGIN="http://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties; //配置文件中传入的值会被放入配置类,所以只需要自动注入然后获取即可
@Autowired
private UserMapper userMapper;
//微信登录
public User wxLogin(UserLoginDTO userLoginDTO){
String openid = getOpenid(userLoginDTO.getCode());
//判断openid是否为空,如果为空登录失败,抛出业务异常
if(openid==null){
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
//判断当前用户是否为新用户(拿openid到用户表里查)
User user = userMapper.getByOpenid(openid);
//如果是新用户,自动完成注册
if(user == null){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
//返回这个用户对象
return user;
}
private String getOpenid(String code){
//调用微信接口服务,来获得当前微信用户的openid
Map 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;
}
}
主要的功能是:获取当前微信用户的openId,然后判断当前用户是否为新用户,如果是新用户就自动调用Mapper层完成注册。
在mapper层的UserMapper类中写入如下代码:
//插入数据
void insert(User user);
在resources下的mapper下的UserMapper.xml中写入Insert的具体代码:
insert into user(openid,name,phone,sex,id_number,avatar,create_time)
values (#{openid},#{name},#{phone},#{sex},#{idNumber},#{avatar},#{createTime})
在UserController中的log.info处加一个断点,然后点击小虫,进行断点调试。
然后在微信开发助手点击编译,然后确定授权微信,然后点击允许获得昵称和头像,此时控制台会获得一个code,然后到controller传入的参数DTO看看是否也获得到了code。
在service的UserServiceImpl中的log上加入一个断点。因为是新用户,所以会执行插入操作,插入openid和时间。
然后取消所有断点,重新运行。看开发者工具可以看到正常获得authentication。
现在需要配置一个拦截器来识别authentication实现拦截放行。
在sky-server下的interceptor将JwtTokenAdminInterceptor复制一个然后粘贴到该目录的下面,重新命名为JwtTokenUserInterceptor:
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
//校验jwt
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());
BaseContext.setCurrentId(userId);
log.info("当前员工id:", userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
然后要让这个拦截器生效:
在sky-server下的config包中的WebMvcConfiguration中写入如下代码:
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
在addInterceptors中写入如下代码(注意下面addInterceptor里面是jwtTokenUserInterceptor):
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
【如果有需要现成代码欢迎点赞+收藏后私信我】
四个接口:查询分类;根据分类id查询菜品;根据分类id查询套餐;根据套餐id查询包含的菜品。
这里尤其要注意,要把day04中项目实战-套餐管理-参考答案全部拷到controller层的admin下,注意一定要是admin包下(不能是user包)的对应controller类中。
这个参考答案是关于套餐的代码,对后面的操作很重要!(需要导入的代码略多,但请耐心!)
当我全部把代码导入后,发现SetmealMapper.xml下好像少了一个update,代码我补充在下面:
update setmeal
category_Id = #{categoryId},
name = #{name},
price = #{price},
status = #{status},
description = #{description},
image = #{image},
where id = #{id}
然后像我一样随便设置一个套餐。
然后检查一下起售停售、新增、删除、修改、批量删除、查询这些功能是否正常,我这里是都正常。
然后在资料包中day06/代码导入/商品浏览中有9个文件:3个controller,2个service,2个serviceImpl,1个mapper,1个mapper.xml。
注意下面2点:
1.上面的controller文件都要放在controller的user这个包下面。
2.有些已有的service或者mapper仅仅只需要拷贝方法,有些缺少的需要新建。(这里list可能会报错看所给资料的day04中有项目实战-套餐管理 - 参考答案,把list的mapper层的代码拷贝过来即可。)
拷贝完在IDEA编译一下,然后到小程序再编译一下。可以看到小程序中已经出现各类菜品(图片没显示属于正常,下面我详细说),然后会有选规格。
然后点击套餐,可以看到套餐里面包含的菜品。
出现问题:
1.如果微信前端没有显示任何东西,看微信开发助手控制台的list是401的问题:一般是拦截器的设置有问题,首先把拦截器给注视掉(可以参考前面的1.18 (微信登录)功能测试+拦截器 P83)。重新启动看能不能访问到。如果能访问到再参考P83的拦截器重新进行设置。
2.如果微信前端的图片是403的问题:这里先说明,图片不显示是正常的——因为像这些图片都是存储在老师阿里云的oss上的,可能老师更改了权限,所以我们没有权限访问图片。
参考下面的URL可以发现,微信开发助手的图片链接不是我当前的oss服务器地址。
解决方法很简单,在菜品修改界面,修改菜品的图片即可(相当于重新上传到你自己的oss服务器上)。如下图左图是修改前,右图是修改后。
菜品和套餐存储在数据库中,如果短期内有大量的人查询会导致数据库压力过大,用户体验不佳。现在把商品数据缓存到Redis中。
加入购物车,查看购物车,可以看到购物车中的商品,可以清空购物车,还可以添加商品到购物车。
缓存菜品 -> 缓存套餐 -> 添加购物车 -> 查看购物车 -> 清空购物车,从购物车中减去某个商品。
问题说明:小程序菜品数据是通过数据库获得,如果用户端访问量过大,数据库的压力会增加。
实现思路:通过Redis来缓存菜品数据,减少数据库查询操作。内存操作的性能比磁盘IO性能更高。
每个分类下的菜品保存一份缓存数据。
数据库中菜品数据有变更时要清理缓存数据。
在sky-server下的controller/user下的DishController类中写入如下代码:
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
// 根据分类id查询菜品
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result> list(Long categoryId) {
//构造redis中的key,规则:dish_分类id
String key = "dish_" + categoryId;
//查询redis中是否存在菜品数据
List list = (List)redisTemplate.opsForValue().get(key);
if(list != null && list.size()>0){//如果存在,直接返回,无须查询数据库
return Result.success(list);
}
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
//如果不存在,查询数据库,将查询到的数据放入redis中
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
}
然后在小程序助手中编译,然后在不同的菜类间切换(比如蜀味烤鱼和蜀味牛蛙),效果是第一次IDEA的控制台会输出SQL语句,然后去看Redis会发现已经有了缓存。然后继续在前面已缓存的菜类间切换,会发现控制台不再输出SQL语句,说明走的是Redis缓存。
不清理可能出现的问题:比如菜品的价格如果被修改,如果继续从Redis从取数据,会导致数据的不一致。
新增菜品、修改菜品、批量删除菜品、起售和停售菜品的时候需要清理缓存。
所以需要在controller下的admin中的DishController中修改代码:
@Autowired
private RedisTemplate redisTemplate;
//清理缓存数据
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
在update、delete方法中调用完service的方法后:
cleanCache("dish_*");
在save方法中调用service的方法后:
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);//清理缓存数据
首先把所有的菜类都缓存到Redis中(在小程序助手里把所有菜类都点一遍),然后在电脑管理端的前端平台修改某一道菜,然后再看Redis的可视化面板,看看是不是所有的Redis都被清空。
SpringCache是Spring提供的缓存框架。提供了基于注解的缓存功能。
SpringCache提供了一层抽象,底层可以切换不同的缓存实现(只需要导入不同的Jar包即可),如EHCache,Caffeine,Redis。
首先打开下面这个项目,然后配置一下配置文件:
找到项目提供的.sql文件,然后在Navicat中新建一个spring_cache_demo的数据库,然后用查询语句查询即可:
下面2个重要依赖已经导入:
org.springframework.boot
spring-boot-starter-cache
org.springframework.boot
spring-boot-starter-data-redis
在启动类上加@EnableCaching
在controller层的UserController下的save方法上写入如下的代码:
@PostMapping
@CachePut(cacheNames="userCache",key="#user.id") //如果使用spring Cache缓存数据,key的生成:userCache::1。user是从参数取到的。
//@CachePut(cacheNames="userCache",key="#result.id") //result是从返回值return取到的
//@CachePut(cacheNames="userCache",key="#p0.id")
//@CachePut(cacheNames="userCache",key="#a0.id")
//@CachePut(cacheNames="userCache",key="#root.args[0].id")
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
注意key="#result.id"中的result取的是返回值返回的那个结果。 key="#user.id"的user取的是传入的参数。p0,a0,root.args[0]表示取的都是第1个参数。
现在进入localhost:8888/doc.html中对save方法发送请求:
然后可以看到缓存中有了数据:
可以看到目录是树状结构,树状结构用:号分隔。
在controller层的UserController下的getById方法上写入如下的代码:
@Cacheable(cacheNames="userCache",key="#id")
然后在方法体的第1行打上断点,点击小虫,然后到localhost:8888/doc.html中对get方法进行测试,发送1,效果是:直接从Redis中返回数据,压根不会触发断点。
现在手动删除id为1的数据,然后重新在doc.html中发送数据,然后会执行断点,放心后控制台输出SQL语句,然后Redis缓存中也有数据。
在controller层的UserController下的deleteById和deleteAll方法上加入如下注解:
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key="#id") //key的形式 userCache::10
public void deleteById(Long id){userMapper.deleteById(id);}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames="userCache",allEntries = true)
public void deleteAll(){
userMapper.deleteAll();
}
测试的话可以在两个方法内的第1行打上断点,点击小虫,然后到localhost:8888/doc.html中对delete和deleteAll方法进行测试,先通过getById方法增加几个数据,然后再逐一删除。
可以这么理解cacheNames里的参数就是指定key的名字,只会删除相应的key。
首先要在sky-server包下导入下面两个坐标:
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-cache
然后要在sky-server包下的启动类SkyApplication类上加如下注解:
@EnableCaching
首先是在controller/user包下的SetmealController类中的list方法上加如下注解:
@Cacheable(cacheNames="setmealCache",key="#categoryId") //key: setmealCache::100
然后是在controller/admin包下的SetmealController类中的save方法上加如下注解:
@CacheEvict(cacheNames="setmealCache",key="#setmealDTO.categoryId")
最后是在controller/admin包下的SetmealController类中的update、delete、startOrStop方法上加如下注解:
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
首先IDEA和小程序助手都要启动。然后先在小程序助手点套餐,然后Redis会把数据缓存。
这个时候到管理端前端界面(电脑网页端),尝试起售和禁售,看看Redis缓存会不会被清空,然后再尝试修改和删除同样也会触发Redis清空。
购物车:暂时存放所选商品的地方。需要记录:选的是什么商品,选购商品的个数。不同用户有不同购物车。
没设置口味数据直接点击+号就可以加入购物车;如果有口味数据,就点击选择规格然后选择口味加入购物车。
如下图有许多冗余字段,目的是为了提高数据的显示速度,不必去连接其它表查询。
冗余字段不能太多,也不能经常变动。
老师在这里说了一下:返回类型Result
在sky-server的controller的user中创建一个名为ShoppingCartController类,写入如下代码:
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags="C端购物车相关接口")
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);
return Result.success();
}
}
然后在sky-server的service下创建ShoppingCartService类,写入如下代码:
public interface ShoppingCartService {
//添加购物车
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
然后在sky-server的service的Impl下创建ShoppingCartServiceImpl类:
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
//判断当前加入购物车中的商品是否已经存在了
//如果已经存在,只需要数量+1
//如果不存在,则需要插入一条购物车数据
}
}
更多细节放在下节完善。
完善sky-server的service的Impl下的ShoppingCartServiceImpl类:
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
//判断当前加入购物车中的商品是否已经存在了
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//userid暂时还不知道,从ThreadLocal取
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
List list = shoppingCartMapper.list(shoppingCart);
//如果已经存在,只需要数量+1
if(list != null && list.size()>0) {
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber()+1);
shoppingCartMapper.updateNumberById(cart);
}
//如果不存在,则需要插入一条购物车数据
}
}
在sky-server的mapper下的ShoppingCartMapper类中写入如下代码:
@Mapper
public interface ShoppingCartMapper {
List list(ShoppingCart shoppingCart);
//根据id修改商品数量
@Update("update shopping_cart set number = #{number} where id = #{id}")
void updateNumberById(ShoppingCart shoppingCart);
}
在sky-server的resources下的mapper下的ShoppingCartMapper.xml类中写入如下代码:
完善sky-server的service的Impl下的ShoppingCartServiceImpl类:
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
//判断当前加入购物车中的商品是否已经存在了
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//userid暂时还不知道,从ThreadLocal取
Long userId = BaseContext.getCurrentId();
shoppingCart.setUserId(userId);
List list = shoppingCartMapper.list(shoppingCart);
//如果已经存在,只需要数量+1
if(list != null && list.size()>0) {
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber()+1);
shoppingCartMapper.updateNumberById(cart);
}else {//如果不存在,则需要插入一条购物车数据
//判断本次添加到购物车的是菜品还是套餐,因为要查询不同表
Long dishId = shoppingCartDTO.getDishId();
if(dishId!=null){
//本次添加到购物车的是菜品
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
}else{
//本次添加到购物车的是套餐
Long setmealId = shoppingCartDTO.getSetmealId();
Setmeal setmeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}
在shoppingCartMapper中添加insert方法:
@Insert("insert into shopping_cart(name,user_id,dish_id,setmeal_id,dish_flavor,number,amount,image,create_time)" +
"value (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
void insert(ShoppingCart shoppingCart);
可能会遇到2个问题:1.数据没插入到数据库,2.userid为空。
先说1,数据没插入到数据库的根本原因在于userid为空,因为如下图user_id默认不为空,假如userid为空则无法正常插入数据, 而userid为空的根源在于拦截器的问题。
拦截器如果有问题可以参考我前面1.18 (微信登录)功能测试+拦截器 P83进行自查,特别注意下面这个地方(可能大概率写错在这个地方):
registry.addInterceptor(jwtTokenUserInterceptor)
额外说一点,如果有出现下面问题
2024-01-25 01:42:12.490 ERROR 44380 --- [nio-8080-exec-7] c.a.druid.pool.DruidAbstractDataSource : discard long time none received connection. , jdbcUrl : jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true, jdbcUrl : jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true, lastPacketReceivedIdleMillis : 97208
可以在启动类中加入如下静态代码:
static {
System.setProperty("druid.mysql.usePingMethod","false");
}
如果传入的参数为空重点看一下有没有在传入的参数前加@RequestBody注解。
在sky-server的controller下的ShoppingCartController类中加入如下代码:
@ApiOperation("查看购物车")
@GetMapping("/list")
public Result> list(){
List list = shoppingCartService.showShoppingCart();
return Result.success(list);
}
在sky-server的service下的ShoppingCartService类中加入如下代码:
List showShoppingCart();
在sky-server的service的Impl下的ShoppingCartServiceImpl类中加入如下代码:
@Override
public List showShoppingCart() {
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = ShoppingCart.builder()
.userId(userId)
.build();
List list = shoppingCartMapper.list(shoppingCart);//只需要传userid即可
return list;
}
大致的效果如下图:
在sky-server的controller下的ShoppingCartController类中加入如下代码:
@ApiOperation("清空购物车")
@DeleteMapping("/clean")
public Result clean(){
shoppingCartService.clean();
return Result.success();
}
在sky-server的service下的ShoppingCartService类中加入如下代码:
//清空购物车
void clean();
在sky-server的service的Impl下的ShoppingCartServiceImpl类中加入如下代码:
//清空购物车
public void clean() {
Long userId = BaseContext.getCurrentId();
shoppingCartMapper.deleteByUserId(userId);
}
在sky-server的mapper的ShoppingCartMapper类中加入如下代码:
@Delete("delete from shopping_cart where user_id=#{userId}")
void deleteByUserId(Long userId);
到这里如果有同学需要项目代码的话,欢迎【点赞+收藏后私信我领取】
本章完成的是用户下单和订单支付功能。
导入地址簿功能代码 -> 用户下单 ->订单支付。
一个用户可以有多个收货地址,只能有一个默认地址。
点击新增收货地址可以新增一个地址。
还可以修改和删除收货地址。
直接把day08/地址簿模块功能代码导入即可,注意要把AddressController放到user包下,其它很简单。
通过小程序助手前端测试一下新增、修改、删除、设置默认地址等功能。
接口设计:
数据库设计无需多言。
在sky-server的controller下创建OrderController类中加入如下代码:
@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags="用户端订单相关接口")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result submit(@RequestBody OrdersSubmitDTO orderSubmitDTO){
log.info("用户下单,参数为:{}",orderSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submitOrder(orderSubmitDTO);
return Result.success(orderSubmitVO);
}
}
在sky-server的service下创建OrderService类中加入如下代码:
public interface OrderService {
//用户下单
OrderSubmitVO submitOrder(OrdersSubmitDTO orderSubmitDTO);
}
在sky-server的service的Impl下创建OrderServiceImpl类中加入如下代码:
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
public OrderSubmitVO submitOrder(OrdersSubmitDTO orderSubmitDTO){
return null;
}
}
在sky-server的mapper下创建OrderMapper类中加入如下代码:
@Mapper
public interface OrderMapper {
}
在sky-server的mapper下创建OrderDetailMapper类中加入如下代码:
@Mapper
public interface OrderDetailMapper {
}
本节主要是对OrderService进行完善,主要完善了处理各种业务异常的情况
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
public OrderSubmitVO submitOrder(OrdersSubmitDTO orderSubmitDTO){
//处理各种业务异常(地址簿为空,购物车数据为空)
//地址簿为空
AddressBook addressBook = addressBookMapper.getById(orderSubmitDTO.getAddressBookId());
if(addressBook == null){
//抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//查询当前用户的购物车数据
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List shoppingCartList = shoppingCartMapper.list(shoppingCart);
if(shoppingCartList == null || shoppingCartList.size()==0){
throw new ShoppingCartBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//向订单表插入1条数据
//向订单明细表插入n条数据
//清空当前用户的购物车数据
//封装VO返回结束
return null;
}
}
本节主要是对OrderService继续进行完善,然后创建orderMapper类,实现insert方法。
下面是对OrderService的完善:
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
public OrderSubmitVO submitOrder(OrdersSubmitDTO orderSubmitDTO){
//1.处理各种业务异常(地址簿为空,购物车数据为空)
//地址簿为空
AddressBook addressBook = addressBookMapper.getById(orderSubmitDTO.getAddressBookId());
if(addressBook == null){
//抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//查询当前用户的购物车数据
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List shoppingCartList = shoppingCartMapper.list(shoppingCart);
if(shoppingCartList == null || shoppingCartList.size()==0){
throw new ShoppingCartBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//2.向订单表插入1条数据
Orders orders = new Orders();
BeanUtils.copyProperties(orderSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userId);
orderMapper.insert(orders);
//3.向订单明细表插入n条数据
//4.清空当前用户的购物车数据
//5.封装VO返回结果
}
}
创建OrderMapper类,写入如下代码:
@Mapper
public interface OrderMapper {
void insert(Orders orders);
}
在resources的mapper包下创建OrderMapper.xml,写入如下代码:
insert into orders (number,status,user_id,address_book_id,order_time,
checkout_time,pay_method,pay_status,amount,remark,phone,address,consignee,
estimated_delivery_time,delivery_status,pack_amount,tableware_number,
tableware_status)
values
(#{number},#{status},#{userId},#{addressBookId},#{orderTime}
,#{checkoutTime},#{payMethod},#{payStatus},#{amount},#{remark}
,#{phone},#{address},#{consignee},#{estimatedDeliveryTime}
,#{deliveryStatus},#{packAmount},#{tablewareNumber},#{tablewareStatus})
本节主要是对OrderService的最终完善,然后创建orderDetailMapper类,实现insertBatch方法:
下面是OrderService的最终代码:
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Transactional
public OrderSubmitVO submitOrder(OrdersSubmitDTO orderSubmitDTO){
//1.处理各种业务异常(地址簿为空,购物车数据为空)
//地址簿为空
AddressBook addressBook = addressBookMapper.getById(orderSubmitDTO.getAddressBookId());
if(addressBook == null){
//抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//查询当前用户的购物车数据
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List shoppingCartList = shoppingCartMapper.list(shoppingCart);
if(shoppingCartList == null || shoppingCartList.size()==0){
throw new ShoppingCartBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
//2.向订单表插入1条数据
Orders orders = new Orders();
BeanUtils.copyProperties(orderSubmitDTO,orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userId);
orderMapper.insert(orders);
List orderDetailList = new ArrayList<>();
//3.向订单明细表插入n条数据
for(ShoppingCart cart: shoppingCartList){
OrderDetail orderDetail = new OrderDetail();//订单明细
BeanUtils.copyProperties(cart,orderDetail);
orderDetail.setOrderId(orders.getId());//设置当前订单明细关联的订单id
orderDetailList.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetailList);
//4.清空当前用户的购物车数据
shoppingCartMapper.deleteByUserId(userId);
//5.封装VO返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderAmount(orders.getAmount())
.build();
return orderSubmitVO;
}
}
创建orderDetailMapper类
@Mapper
public interface OrderDetailMapper {
//批量插入订单明细数据
void insertBatch(List orderDetailList);
}
在resources的mapper下创建orderDetailMapper类,实现insertBatch方法:
insert into order_detail(name,image,order_id,dish_id,setmeal_id,
dish_flavor,number,amount)
values
(#{od.name},#{od.image},#{od.orderId},#{od.dishId},
#{od.setmealId},#{od.dishFlavor},#{od.number},#{od.amount})
在serviceImpl上加了@Transactional注解,所以Insert方法执行完后数据库还不会有数据,只有等方法执行完数据库才有数据。
如下图在小程序助手模拟下单,可以看到数据库里order_detail表会出现具体菜的细节。
orders表里会有相关的信息:
简单测试一下能获得相应信息即算成功。
到这里如果有同学需要项目代码的话,欢迎【点赞+收藏后私信我领取】
大致讲了一下微信支付接入流程:
本节主要讲微信支付的时序图:
mchid是商户号,out_trade_no是订单号,appid是应用的id,notify_url是回调地址,amount是总金额,payer是支付者,openid是当前付款用户的openid。
本节主要介绍微信支付的相关接口,详见视频,在此不过多赘述。
这节涉及到2个问题:1.调用过程如何保证数据安全?2.微信后台如何调用到商户系统?
1.保证数据安全:微信支付涉及到数据的传输,为了保证数据的安全,所以需要对数据进行加密和解密,需要用到证书。
需要下面2个文件(要求小程序是企业认证,没有的话也没关系,对后面没太大影响):获取微信微信支付平台证书文件:apiclient_key.pem。商户私钥文件:wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
2.微信后台调用商户系统:当前商户系统的ip是笔记本的ip,仅仅只是局域网内的ip地址,要求获得公网ip,可以使用内网穿透技术解决。
获取临时域名
安装包在给的第8天资料里,这个软件是为了给当前局域网内的ip一个公网的域名:
在下面的地址栏输入cmd:
到官网获取隧道Authtoken:
输入如下代码,粘贴token即可:
会生成一个配置文件:
输入下面的代码:
cpolar.exe http 8080
获得临时的域名:
可以通过域名+doc.html进行访问:
在配置项application.yml中加入如下代码:
sky:
wechat:
appid: ${sky.wechat.appit}
secret: ${sky.wechat.secret}
mchid: ${sky.wechat.mchid}
mchSerialNo: ${sky.wechat.mchid}
privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
apiV3Key: ${sky.wechat.apiV3Key}
weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
notifyUrl: ${sky.wechat.notifyUrl}
refundNotifyUrl: ${sky.wechat.refundNotifyUrl}
仔细看下面参数及其对应的含义:
在application-dev.yml文件中写入如下的代码(要注意notifyUrl和refundNotifyUrl,这两个url的前半部分都是cpolar临时生成的公网ip,因为是临时域名,所以每次生成的都会变化,以后要注意修改):
sky:
wechat:
appid: wx3910b10fd7db38d1
secret: f274884c2a56466016ebbcf009404e63
mchid: 1561414331
mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
privateKeyFilePath: C:\software\apiclient_key.pem
apiV3Key: CZBK51236435wxpay435434323FFDuv3
weChatPayCertFilePat: C:\software\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
notifyUrl: https://9ea0754.r19.cpolar.top/notify/paySuccess
refundNotifyUrl: https://9ea0754.r19.cpolar.top/notify/refundSuccess
notifyUrl和refundNotifyUrl这俩地址是由微信后台请求的。
1. 将OrderController下的payment方法(订单支付)代码,复制到sky-server的controller的user下的OrderController中。
2. 将OrderService下的payment和paySuccess方法(订单支付,支付成功修改订单状态)代码,复制到sky-server的service下。
3. 将OrderServiceImpl下的payment和paySuccess方法(订单支付,支付成功修改订单状态)代码,复制到sky-server的service下的Impl中。还有注入下面的工具类。
@Autowired
private WeChatPayUtil weChatPayUtil;
4. 将OrderMapper下的getByNumber和update方法代码,复制到sky-server的Mapper下。
这个update方法一定要复制,后面章节频繁用到这个方法。
5. 直接把我下面完整版的update代码复制到sky-server的resources的mapper下的OrderMapper.xml中。
update orders
number=#{number},
status=#{status},
address_book_id=#{addressBookId},
order_time=#{orderTime},
checkout_time=#{checkoutTime},
pay_method=#{payMethod},
pay_status=#{payStatus},
amount=#{amount},
remark=#{remark},
phone=#{phone},
address=#{address},
user_name=#{userName},
consignee=#{consignee} ,
cancel_reason=#{cancelReason},
rejection_reason=#{rejectionReason},
cancel_time=#{cancelTime},
estimated_delivery_time=#{estimatedDeliveryTime},
delivery_status=#{deliveryStatus},
delivery_Time=#{deliveryTime},
pack_amount=#{packAmount},
tableware_number=#{tablewareNumber},
tableware_status=#{tablewareStatus},
where id=#{id}
6.将下面代码,复制到sky-server的Mapper下的UserMapper中:
@Select("select * from user where id=#{id}")
User getById(Long userId);
7. 最后在sky-server的controller下创建notify包,将PayNotifyController类复制到notify包下。
直接参考视频,在此不过多赘述。
支付成功效果如下:
因为没有营业执照没办法真正支付,所以下面打算直接绕过支付,直接支付成功。
需要修改2个地方:1个是微信小程序,点击支付按钮后直接跳转支付成功。另1个是后端,要求在收到前端支付操作后,不进行任何判断,直接给数据库设置已支付状态。
下面是修改过程:
首先在微信小程序里的pay包下的index.js中将如下的代码注释掉:
然后把原先注释掉的重定向解除:
然后在IDEA中,把service/impl下的OrderServiceImpl中的如下代码注释掉:
同样在OrderServiceImpl中,写入如下代码,用于设置参数:
完整的订单支付代码如下:
/**
* 订单支付
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
/* //调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}
*/
JSONObject jsonObject = new JSONObject();
jsonObject.put("code","ORDERPAID");
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付
Integer OrderStatus = Orders.TO_BE_CONFIRMED; //订单状态,待接单
LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间
orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
return vo;
}
在OrderMapper中写入如下代码:
@Update("update orders set status = #{orderStatus},pay_status = #{orderPaidStatus} ,checkout_time = #{check_out_time} where id = #{id}")
void updateStatus(Integer orderStatus, Integer orderPaidStatus, LocalDateTime check_out_time, Long id);
现在还有一个问题就是orderId怎么获取?方法如下:
在OrderServiceImpl里加一个全局变量orders(如下图),在submitOrder方法(提交订单)中的如下位置给全局变量赋值:
简单解释下,在如下页面点击去支付后就会调用submitOrder方法,将订单数据写入数据库,所以可以在submitOrder方法中获取订单的id。
下面是我刚刚下的一单,status==2代表待派送,pay_status==1代表已支付,测试没问题。
管理端显示也没问题:
【看到这里如果觉得有帮助点个赞吧据说点赞的uu都会收获好运哦!】
这部分在视频中没有,需要参考所给资料的Day09将代码导入到对应文件中。
需要导入如下(注意区分用户端和商家端):
1.查询历史订单(用户端);2.查询订单详情(用户端);3.取消订单(用户端);4.再来一单(用户端)。
1.订单搜索(商家端);2.各个状态的订单数量统计(商家端)。3.查询订单详情(商家端)。4.接单(商家端);5.拒单(商家端);6.取消订单(商家端);7.派送订单(商家端);8.完成订单(商家端)
校验收货地址是否超出配送范围可以选择导入,对后面项目无影响。
在OrderMapper.xml中可能缺少一个update,我补充在下面,需要的话自取
update orders
number=#{number},
status=#{status},
address_book_id=#{addressBookId},
order_time=#{orderTime},
checkout_time=#{checkoutTime},
pay_method=#{payMethod},
pay_status=#{payStatus},
amount=#{amount},
remark=#{remark},
phone=#{phone},
address=#{address},
user_name=#{userName},
consignee=#{consignee} ,
cancel_reason=#{cancelReason},
rejection_reason=#{rejectionReason},
cancel_time=#{cancelTime},
estimated_delivery_time=#{estimatedDeliveryTime},
delivery_status=#{deliveryStatus},
delivery_Time=#{deliveryTime},
pack_amount=#{packAmount},
tableware_number=#{tablewareNumber},
tableware_status=#{tablewareStatus},
where id=#{id}
用户端项目测试(微信小程序助手):
1.查询历史订单
点击左上角个人中心,然后点击历史订单。
2.查询订单详情
点击历史订单中的订单项。
3.取消订单
下完一单,不支付,到订单详情界面,取消订单。
4.再来一单
点击再来一单,该单所有菜品重新加入购物车。
管理端项目测试(电脑界面):
事先说明一下,虽然暂时无法实现支付功能,但可以人为去修改订单的状态,一共有6种状态,如下图:
可以通过修改数据库的staus字段进行更改:
显示到网页端如下图:
1.订单搜索
根据订单号、手机号、下单时间进行查找,发现没问题。
2.各个状态的订单数量统计
看不同状态订单的数量是不是正确的,我这里正常:
3.查看订单详情
点击查看可以看到订单详情
4.接单
对于待接单的项可以点击接单,
然后接单会变成派送,这里没问题:
5.拒单
只有已支付且尚未派送的订单可以拒单,status==2。
6.取消订单
点击取消按钮后,选择取消原因,订单状态变成已取消,取消按钮消失。
7.派送订单
对于待派送的订单可以点击派送:
然后派送按钮会变成完成,状态变成派送中
8.完成订单
点击完成,然后订单会进入已完成:
【如果有需要现成代码欢迎点赞+收藏后私信我】