本篇对苍穹外卖后半部分进行介绍,重点是redis缓存的使用以及微信小程序客户端开发。
目录
一、菜品管理
1.1新增菜品
1.2菜品的分页查询
1.3删除菜品
1.4修改菜品
1.5设置业务状态
二、微信小程序客户端的开发
三、Redis的根本使用
常用命令:
缓存套餐:
一、菜品管理
1.1新增菜品
起首进行需求分析:

注意口胃属性也是一个单独的表,要与这个菜品相对应,纪录菜品的id,以是涉及到多个表的操纵,一个是菜品表一个是口胃表,以是要使用@Transactonal开启事务管理
团体流程如下:
在dishcontroller中还是正常的使用dishDTO来担当前端传过来的数据,dishDTO中有flavors数组,前端传过来的口胃数组可以用这个数组来接收,但是dish中没有这个属性,然后在service中因为要涉及到两个表的操纵,一个是dish表(dish表用于存储菜品)一个是dish_flavor表(这个表用于存放口胃,其中有dish_id属性),以是此时要保证事务的原子性,要么都成功,要么都失败以是要在前面添加上@Transactional注解,然后向dish表中插入数据,插入之后再向dish_flavor表中插入数据(创建一个dishflavormapper),dish_flavor表中有dish_id,因为dish_id是自增的,前端没有传过来,那如何得到这个dish_id呢,可以想到前面刚插入一个dish数据,可以通过主键回显来得到这个dish_id,然后在dishMapper.xml文件中是使用usegeneratedkeys然后使用keyproperty=id,赋值给id,然后通过dish。getid即可得到这个值,如果不消主键回显是得不到这个值的。由于flavors是一个数组,以是要给这个数组中每一个元素赋值dish_id,然后再进行批量插入即可,使用foreach。最终完成操纵。
界面如下:

团体代码如下:
- //Controller:
- @PostMapping
- @ApiOperation("新增菜品")
- public Result save(@RequestBody DishDTO dishDTO){
- log.info("新增菜品:"+dishDTO);
- dishService.saveWithFlavor(dishDTO);
- //清理缓存数据:
- String key="dish_"+dishDTO.getCategoryId();
- redisTemplate.delete(key);
- return Result.success();
- }
- //Service:
- @Transactional//需要开启事务管理,可以看到启动类那里已经开启了,添加了这个注解:@EnableTransactionManagement //开启注解方式的事务管理
- public void saveWithFlavor(DishDTO dishDTO) {
- //向菜品表添加一条数据
- //Dish类中没有falvor属性flavor类中有dish_id属性,dishdto类中有List<DishFlavor> flavors属性
- Dish dish=new Dish();
- BeanUtils.copyProperties(dishDTO,dish);
- dish.setStatus(StatusConstant.DISABLE);
- dishMapper.insert(dish);
- //向dish_flavor表中插入数据需要用到dish_id,但是怎么得到dish_id,这时候要用到主键回显,在dishMapper中使用到了usergeneratedkeys
- // 然后返回给这个对象的id属性,然后这个时候使用dish.getId才会有值,如果不使用主键回显是得不到的。
- Long dish_id = dish.getId();
- //向口味表添加n条数据,因为口味可能有多个,也可能没有
- List<DishFlavor> flavors = dishDTO.getFlavors();
- if(flavors.size()>0&&flavors!=null){
- //这里要进行赋值,dish_id:
- flavors.forEach(dish_flavor->{
- dish_flavor.setDishId(dish_id);
- });
- //注意此处flavors是一个数组,想进行插入操作可以批量插入:
- dishFlavorMapper.insertBach(flavors);
- }
- }
- Mapper:
- <insert id="insert" useGeneratedKeys="true" keyProperty="id">
- insert into dish(name, category_id, price, image, status,description, create_time, update_time, create_user, update_user)
- values(#{name},#{categoryId},#{price},#{image},#{status},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser})
- </insert>
- !--注意此处虽然dish_flavor中有dish_id属性,但是前端还没有把这个dish_id传过来,因为主键一般是自增的,
- 并且还在上传菜品中还没有提交此时还没有值呢,那如何获取dish_id呢,可以想到DishServiceImpl中在dish表插入了一条数据,可以进行主键回显,然后得到这个dish_id这个值,-->
- <insert id="insertBach">
- insert into dish_flavor(dish_id,name,value)values
- <foreach collection="flavors" item="df" separator=",">
- (#{df.dishId},#{df.name},#{df.value})
- </foreach>
- </insert>
复制代码 1.2菜品的分页查询
需求分析:须要返回以下数据,须要返回分类名称,以是还须要前端传递分类id,根据分类id去查询分类表得到分类名称,须要连表进行查询,最终计划一个VO封装返回数据。
团体代码如下:
须要注意的就是须要连合查询返回分类名称,这一步容易忽略,此处采用左外连接以及动态查询的方式来获取到categoryname然后封装到dishVO中;注意此处采用左外连接查询,查询dish表的所有加category表中的name,使用category的id雷同;
别的就是使用分页插件,PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()) 这一步的作用就是:
- 设置分页参数:设置了分页的参数,包罗当前请求的页码(getPage())和每页表现的纪录数(getPageSize())。
- 拦截后续查询:在调用 startPage 方法后,PageHelper 会拦截紧随其后的第一次 MyBatis 查询操纵,并自动为其添加分页的 SQL 语句。
- 自动分页:不须要手动编写分页的 SQL 语句,PageHelper 会自动处理分页逻辑,包罗计算总页数、总纪录数等。
- //Controller
- /**
- * 分类查询
- * @param dishPageQueryDTO
- * @return
- */
- @ApiOperation("菜品分页查询:")
- @GetMapping("/page")
- public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
- PageResult pageResult=dishService.page(dishPageQueryDTO);
- return Result.success(pageResult);
- }
- //Service
- public PageResult page(DishPageQueryDTO dishPageQueryDTO) {
- PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
- Page<DishVO> page=dishMapper.select(dishPageQueryDTO);
- long total = page.getTotal();
- List result = page.getResult();
- return new PageResult(total,result);
- }
- //DAO:
- Page<DishVO> select(DishPageQueryDTO dishPageQueryDTO);
- <select id="select" resultType="com.sky.vo.DishVO">
- select d.*,c.name as categoryName from dish d left outer join category c on d.category_id=c.id
- <where>
- <if test="name!=null">
- and d.name like concat('%',#{name},'%')
- </if>
- <if test="status!=null">
- and d.status=#{status}
- </if>
- <if test="categoryId!=null">
- and d.category_id=#{categoryId}
- </if>
- </where>
- order by d.create_time desc
- </select>
复制代码 1.3删除菜品
需求分析:需求分析是很重要的,须要考虑的点也许多,业务层须要做许多的逻辑判定,而且还须要连带口胃表中的数据也须要删除。

须要判定菜品是否起售,须要根据id去查询菜品的状态,如果起售状态无法删除。然后须要去判定菜品是否被套餐关联,可以根据菜品id去套餐表中查询数量,如果数量大于零阐明已经被关联,无法删除。
须要注意的是前端可以传递多个菜品id批量删除,以是须要用一个LIst接收参数,而且使用@RequestParam注解,前端传过来的是String范例的,1,2,3这种范例的,以是正常来说要用String来担当,然后split分隔成数组,但是现在可以用springmvc来自动解析,然后转换成一个聚集。
别的,也须要操纵多个表,菜品表和口胃表都须要进行删除操纵,以是须要开启事务。
团体代码如下:
- //Controller
- @DeleteMapping
- @ApiOperation("删除菜品")
- public Result delete(@RequestParam List<Long> ids){//注意此处前端传过来的是String类型的,1,2,3这种类型的,所以正常来说要用String来接受,然后split分隔成数组,但是现在可以用springmvc来自动解析,然后转换成一个集合,如果这样的话,需要用一个注解!
- dishService.deleteBatch(ids);
- //清理缓存:此处将所有缓存数据都要删除,首先要获取所有的key
- Set keys = redisTemplate.keys("dish_*");//此处表示获取所有以dish_开头的key
- redisTemplate.delete(keys);//支持集合参数
- return Result.success();
- }
- //Service:
- @Transactional
- public void deleteBatch(List<Long> ids) {
- //首先要判断能否删除,起售中的菜品无法被删除:
- for(Long id:ids){
- Dish dish=dishMapper.getById(id);
- if(dish.getStatus()== StatusConstant.ENABLE){
- throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
- }
- }
- //被套餐关联的菜品不能被删除:
- int count=setmealDishMapper.getCount(ids);
- if(count>0){
- throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
- }
- dishMapper.delete(ids);
- dishFlavorMapper.delete(ids);
- }
- //DAO:
- <delete id="delete">
- delete from dish where id in
- <foreach collection="ids" item="id" separator="," open="(" close=")">
- #{id}
- </foreach>
- </delete>
- <delete id="delete">
- delete from dish_flavor where dish_id in
- <foreach collection="ids" item="id" separator="," open="(" close=")">
- #{id}
- </foreach>
- </delete>
复制代码 1.4修改菜品
起首须要进行查询操纵回显,然后再进行更新操纵。须要注意的是回显的时间会涉及到多个表,例如口胃表等 。
但是此处口胃表的修改非常复杂,因为修改的时间可能添加可能删除可能修改,以是一个方法就是直接删撤除该菜品连合的口胃,然后再重新新增。

- @ApiOperation("根据id查询菜品用于修改菜品回显")
- @GetMapping("/{id}")
- public Result<DishVO> getById(@PathVariable Long id){
- log.info("根据id查询菜品:"+id);
- DishVO dishvo=dishService.getById(id);
- return Result.success(dishvo);
- }
- @ApiOperation("修改菜品")
- @PutMapping
- public Result update(@RequestBody DishDTO dishDTO){
- log.info("修改菜品"+dishDTO);
- dishService.update(dishDTO);
- //清理缓存:此处将所有缓存数据都要删除,首先要获取所有的key
- Set keys = redisTemplate.keys("dish_*");//此处表示获取所有以dish_开头的key
- redisTemplate.delete(keys);//支持集合参数
- return Result.success();
- }
- //根据id查询菜品
- public DishVO getById(Long id) {
- Dish dish=dishMapper.getById(id);
- List<DishFlavor> flavors=dishFlavorMapper.getById(id);
- DishVO dishVO=new DishVO();
- BeanUtils.copyProperties(dish,dishVO);
- dishVO.setFlavors(flavors);
- return dishVO;
- }
- //修改菜品
- public void update(DishDTO dishDTO) {
- Dish dish=new Dish();
- BeanUtils.copyProperties(dishDTO,dish);
- dishMapper.update(dish);
- //此处口味的修改非常复杂,因为可能会删除可能会新增还可能不修改,所以一种方法是直接都删掉,然后新增:
- List<Long> list=new ArrayList();
- list.add(dishDTO.getId());
- dishFlavorMapper.delete(list);
- List<DishFlavor> flavors = dishDTO.getFlavors();
- if(flavors.size()>0&&flavors!=null){
- //注意此处flavors是一个数组,想进行插入操作可以批量插入:
- flavors.forEach(dish_flavor->{
- //此处仍然需要设置dish_id,原来的都删除掉了,然后新增之后没有dish_id了,还需要在此处设置一下
- dish_flavor.setDishId(dishDTO.getId());
- });
- dishFlavorMapper.insertBach(flavors);
- }
- }
复制代码 1.5设置业务状态
本来可以使用一个设置接口就可以,管理端和用户端共用这个接口,但是在本项目中用户端的发送请求都是/admin为前缀,用户端都是/user为前缀,以是此处写了两个查询接口。
需求分析:
因为定义了两个shopcontroller,会有冲突,以是可以分别在user和admin中的RestController注解中写上对应名称。
二、微信小程序客户端的开发
起首介绍一下HttpClient:
其中有几个核心API:

httpclient是一个接口可以发送http请求,httpclients可以创建一个httpclient,CloseableHttpClient是一个实现类,实现了HttpClient接口。HttpGet请求和HttpPost请求。
测试get和post请求,调用我们的接口:
- @SpringBootTest
- public class HttpClientTest {
- /**
- * 通过HttpClient发送get方式请求
- */
- @Test
- public void testGet() throws IOException {
- //创建httpclient对象
- CloseableHttpClient httpClient= HttpClients.createDefault();
- //创建请求对象
- HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");//去访问user端的接口
- //发送请求,接收响应结果
- CloseableHttpResponse response=httpClient.execute(httpGet);
- //获取服务端响应过来的状态码
- int statusCode = response.getStatusLine().getStatusCode();
- System.out.println("服务端发送过来的状态码:"+statusCode);
- //获取响应过来的值:
- HttpEntity entity = response.getEntity();
- String s = EntityUtils.toString(entity);
- System.out.println("服务端响应回来的值:"+s);
- //关闭资源
- response.close();
- httpClient.close();
- }
- /**
- * 通过HttpClient发送post方式请求
- */
- @Test
- public void testPost() throws IOException {
- CloseableHttpClient httpClient = HttpClients.createDefault();
- HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");//这是一个post请求,请求的是管理端的登录接口,需要传过去json格式的数据
- 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();
- }
- }
复制代码 
微信登录即可完成登录,如果是新用户,自动注册将用户生存到数据库中,要实现微信登岸,就须要获取授权码,小程序先wx.login获取到授权码(code) 然后发送请求到服务端,服务端接收code后发送http请求到微信接口服务,然后微信接口返回session_key和openid。然后服务端接收之后,须要给用户返回令牌,令牌中包含用户的唯一标识。
同样须要设置客户端的jwt登录校验:
- @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");
- }
复制代码 接口计划:
注意路径中第一个user是user表现用户端,然后第二个user代表的是用户模块。
返回的数据中data中id指的是数据库中这个用户的id,openid表现这个微信用户在小程序中的唯一标识。
起首是controller层:
须要写好接口,小程序端发送请求到这个接口,发送了授权码(code),然后服务端接收,接收之后调用service中的代码,通过httpclient发送请求到微信接口服务,使用封装好的httpclientutil发送get请求并携带appid、secret以及授权码等参数返回的结果是String范例的json数据,然后须要解析json数据得到openid,判定openid是否为空,为空登岸失败,不为空继承调用mapper中的根据openid查询代码,看是否为新用户,如果是新用户自动注册,调用insert方法添加到数据库,最终返回给controller层这个对象。然后controller层接收这个对象之后,须要给这个用户天生jwt令牌:
创建好jwt令牌之后,须要添加拦截器进行jwt验证,末了设置在webmvcconfiguration中。
- @ApiOperation(value="员工登录")//添加说明
- @PostMapping("/login")
- public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
- log.info("员工登录:{}", employeeLoginDTO);
- Employee employee = employeeService.login(employeeLoginDTO);
- //登录成功后,生成jwt令牌
- Map<String, Object> claims = new HashMap<>();
- claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
- String token = JwtUtil.createJWT(
- jwtProperties.getAdminSecretKey(),
- jwtProperties.getAdminTtl(),
- claims);
- EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
- .id(employee.getId())
- .userName(employee.getUsername())
- .name(employee.getName())
- .token(token)
- .build();
- return Result.success(employeeLoginVO);
- }
复制代码 三、Redis的根本使用
本项目中使用到了redis的根本用法,主要是进行增删改查操纵,并没有实现setnx加锁等稍微困难的操纵,练手还是挺可以的。
Redis是一个基于内存的key-value结构数据库,其直接存储到内存中;mysql数据库是存储到磁盘中,查询操纵是磁盘io操纵。
下面是一些基础的启动操纵:
启用通过以下命令即可,这个是server端:
然后再打开一个命令行窗口,client端:使用这个命令会自动连接到本地的redis,如果想连接其他地方的redis,可以用下面的命令,其中-h表现ip地址,-p表现端口号:
下面介绍Redis中五种根本数据范例
字符串 string
哈希hash
列表 list
聚集set
有序聚集zset(一般可以用来做排行榜相关)
常用命令:
字符串范例:
注意setnx一般当作锁来使用
哈希范例:
列表范例:
lpush lpop rpush rpop既可以模仿栈也可以模仿队列
聚集范例:
有序聚集:
通用命令:
测试如下:
Redis在java中的使用:
Spring Data Redis是Spring的一部分,对Redis底层开发进行了高度封装,在spring项目中,可以使用spring data redis来简化redis操纵。
使用的操纵步调如下:
1.导入spring data redis的maven坐标:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
复制代码 2.设置redis数据源
application-dev.yml中设置:
- sky:
- datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver
- host: localhost
- port: 3306
- database: sky_take_out
- username: root
- password: dir99
- redis:
- host: Localhost
- port: 6379
- password: dir99
- database: 1
- wechat:
- appid: wx02bddf1e8f6f1036
- secret: d3ed0a340ec836f1131f8f1b582531edy
- 引用配置:
复制代码
3.编写设置类,创建RedisTemplate对象
- @Configuration
- @Slf4j
- public class RedisConfiguration {
- @Bean
- public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
- log.info("开始创建redis模板对象...");
- RedisTemplate redisTemplate=new RedisTemplate();
- //设置redis的连接工厂对象:
- redisTemplate.setConnectionFactory(redisConnectionFactory);
- //设置redis key的序列化器,可以将key在redis可视化界面中显示出正常的字符串类型,不会出现看起来乱码的情况
- redisTemplate.setKeySerializer(new StringRedisSerializer());
- return redisTemplate;
- }
- }
复制代码 下面是具体的使用:
如果一段时间内大量用户访问而且点菜,菜品都是存放在数据库中的,就要频繁访问数据库,会导致性能下降,卡顿,点击一个分类后可能要几秒钟之后才气表现出数据。以是可以使用缓存,把这些商品数据缓存到redis中,提高性能。
那么一个菜品参加一个缓存数据还是多个呢?可以想到小程序展示菜品的时间是根据分类来展示的,以是每一类生存一个缓存数据,每一个类构建一个key。
实现思路如下:
每一类可以生存一个缓存,在redis中是通过key-value来生存数据的,以是可以通过以下方式来生存,dish背面加一个动态数值,表现分类id,然后将java中list聚集,转换为String范例(注意,此处java中的范例和redis中的范例不太一样,以是对list进行序列化然后转换成redis中的String范例)生存到redis中。但是如果菜品有变更,例如,管理端更改代价,要实时清算缓存数据,因为缓存没有同步更新过来,不清算的话会导致数据不一致性,以是要清算缓存数据。
- @GetMapping("/list")
- @ApiOperation("根据分类id查询菜品")
- public Result<List<DishVO>> list(Long categoryId) {
- /**
- * 使用缓存:
- */
- //先构造key进行查询:dish_分类id格式:
- String key="dish_"+categoryId;
- //查询缓存中是否存在菜品数据 注意此处下方放进去是什么类型取出来就是什么类型,下面用的List<DishVO>,这里强转以下就行
- List<DishVO> list = (List<DishVO>)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);//查询起售中的菜品
- list = dishService.listWithFlavor(dish);
- redisTemplate.opsForValue().set(key,list);
- return Result.success(list);
- }
复制代码 更改完之后,须要再继承更改,添加清算缓存操纵,保证数据一致性:
分析当删除,新增,修改,起售停售菜品操纵都须要清算缓存:
以上操纵都在管理端进行,以是要在管理端进行修改:
清算缓存并不须要删撤除所有的缓存,只须要对更改的进行清算即可,哪一个分类被修改,清算这一个类即可。像新增操纵改的哪个类删除哪个缓存数据即可,但是删除操纵,前端传过来的是一个数组,内里可能包含多个菜品,这些菜品可能属于一个类也可能属于多个类,以是简单来看直接将所有的缓存删除即可。
注意修改操纵:平凡的修改像改名字,代价只计划一个分类,但是如果要更改分类的话,就要涉及两个分类,这一个类少一个那一个类多一个以是此处直接同样删除所有类的缓存数据。
起售停售菜品也是,前端传过来的有status以及id,如果想删除单个类的缓存也可以,须要根据菜品id查询分类进行查询操纵然后得到这个类的key然后进行删除,但是须要进行查询操纵有点浪费资源以是不如直接删除。
缓存套餐:
Spring cache(重要!!!)
有些雷同AOP,只须要在须要缓存的方法加上注解即可。
在启动类上加上@EnableCaching注解来开启缓存注解功能。
如果想换为Caffeine只须要导入这个坐标,删除redis坐标即可。
后三个注解都是加在方法上的,cachePut与Cacheable的区别就是cacheable既可以取也可以存,另一个只能存不能取。
spring cache demo:
起首创建一个user表:
然后在pom文件中设置坐标,上图有;
@CachePut注解的使用
对于这个save方法,也就是插入操纵,新增一个新用户,想插入数据库的同时生存到redis中一份,之前是通过redisTemplete然后set方法来实现,用了springcache之后,内里有一个注解就是@CachePut注解,可以直接将方法返回值加到缓存中。
#user.id,这个#背面的须要和方法中的形参名一致。但是要在mapper中使用主键回显!
在redis可视化软件中:
是根据冒号来确定的树形结构,完备的key是a:b:c:d
@Cacheable注解使用
对于这个getbyid方法,想在调用mapper之前先判定缓存中是否有这个数据,如果有的话直接返回缓存中的数据,如果没有的话,再去查询数据库返回。之前也是用的redisTemplete中get方法进行判定。先设置key然后get。使用SpringCache之后可以用@Cacheable注解。
加上注解之后:和上面的putCache注解雷同,存到redis中的key为cacheNames::key,#
背面还须要和方法参数一致。
加上这个注解之后,实在在controller层的getById方法执行之前先有一个署理对象去缓存中查找,如果查找到了直接就返回缓存中的数据。这个方法实在没有被调用。如果没有查到通过反射来调用controller层中的这个方法。末了将返回结果放到redis中。
@CacheEvict注解的使用
这样可以删除一个缓存数据
这种方式将userCache下的键值缓存数据对全部都删除
得不偿失以是直接都删撤除。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |