企业WEB项目商品管理&图片上传

前端 同时被 2 个专栏收录
3 篇文章 0 订阅
27 篇文章 2 订阅

一、商品基本信息录入

1.电商概念SPU与SKU

SPU = Standard Product Unit (标准产品单位)
SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。

例如:iphone7就是一个SPU,与商家,与颜色、款式、套餐都无关。

SKU=stock keeping unit(库存量单位)
SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。
SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍。

例如:纺织品中一个SKU通常表示:规格、颜色、款式。

2.商品分类

2.1需求分析

实现三级商品分类列表查询功能

进入页面首先显示所以一级分类,效果如下:image-20200705001309486

点击列表行的查询下级按钮,进入下级分类列表,同时更新面包屑导航

再次点击表行的查询下级按钮,进入三级分类列表,因为三级分类属于最后一级,所以在列表中不显示查询下级按钮,同时更新面包屑导航。

点击面包屑导航,可以进行返回操作。

这里后端和淘淘商城差不多,主要提供前端代码。

2.2前端

列表实现:

(1)修改itemCatService.js

//根据上级ID查询下级列表
this.findByParentId=function(parentId){
    return $http.get('../itemCat/findByParentId.do?parentId='+parentId);	
}

(2)修改itemCatController.js

//根据上级ID显示下级列表 
$scope.findByParentId=function(parentId){
    itemCatService.findByParentId(parentId).success(
        function(response){
            $scope.list=response;
        }			
    );
}   

面包屑导航:

我们需要返回上级列表,需要通过点击面包屑来实现,修改itemCatController.js

$scope.grade=1;//默认为1级	
//设置级别
$scope.setGrade=function(value){
    $scope.grade=value;
}		
//读取列表
$scope.selectList=function(p_entity){			
    if($scope.grade==1){//如果为1级
        $scope.entity_1=null;	
        $scope.entity_2=null;
    }		
    if($scope.grade==2){//如果为2级
        $scope.entity_1=p_entity;	
        $scope.entity_2=null;
    }		
    if($scope.grade==3){//如果为3级
        $scope.entity_2=p_entity;		
    }		
    $scope.findByParentId(p_entity.id);	//查询此级下级列表
}

修改列表的查询下级按钮,设定级别值后 显示列表

<span ng-if="grade!=3">	                                     
    <button type="button" class="btn bg-olive btn-xs" ng-click="setGrade(grade+1);selectList(entity)">查询下级</button> 		                                     
</span>                                  	

这里我们使用了ng-if指令,用于条件判断,当级别不等于3的时候才显示“查询下级”按钮

绑定面包屑:

<ol class="breadcrumb">	                        	
  <li><a href="#" ng-click="grade=1;selectList({id:0})">顶级分类列表</a></li>
  <li><a href="#" ng-click="grade=2;selectList(entity_1)">{{entity_1.name}}</a></li>
  <li><a href="#" ng-click="grade=3;selectList(entity_2)">{{entity_2.name}}</a></li>
</ol>

3.商品介绍

实现商品介绍的录入,要求使用富文本编辑器

3.1富文本编辑器介绍

富文本编辑器,Rich Text Editor, 简称 RTE, 它提供类似于 Microsoft Word 的编辑功能。常用的富文本编辑器:

KindEditor http://kindeditor.net/

UEditor http://ueditor.baidu.com/website/

CKEditor http://ckeditor.com/

3.2使用kindeditor

在页面中添加JS代码,用于初始化kindeditor

<script type="text/javascript">
	var editor;
	KindEditor.ready(function(K) {
		editor = K.create('textarea[name="content"]', {
			allowFileManager : true
		});
	});
</script>

allowFileManager 【是否允许浏览服务器已上传文件】 默认值是:false

提取kindeditor编辑器的内容:

$scope.entity.goodsDesc.introduction=editor.html();

清空kindeditor编辑器的内容:

editor.html('');//清空富文本编辑器

4.选择商品分类

在商品录入界面实现商品分类的选择(三级分类)效果如下:

image-20200705102621309

当用户选择一级分类后,二级分类列表要相应更新,当用户选择二级分类后,三级列表要相应更新。三级分类选好后,模板Id要同时更新

4.1一级分类下拉选择框

在goodsController增加代码

//读取一级分类
$scope.selectItemCat1List=function(){
    itemCatService.findByParentId(0).success(
        function(response){
            $scope.itemCat1List=response; 
        }
    );
}

页面加载调用该方法

<body class="hold-transition skin-red sidebar-mini" ng-app="pinyougou" ng-controller="goodsController" ng-init="selectItemCat1List()">

修改goods_edit.html一级分类下拉选择框

<select class="form-control" ng-model="entity.goods.category1Id" ng-options="item.id as item.name for item in itemCat1List"></select>

ng-options语法:作为表单传递的值 as 显示的内容 for 别名 in List

ng-options属性可以在表达式中使用数组或对象来自动生成一个select中的option列表。ng-options与ng-repeat很相似,很多时候可以用ng-repeat来代替ng-options。但是ng-options提供了一些好处,例如减少内存提高速度,以及提供选择框的选项来让用户选择。

4.2二级分类下拉选择框

在goodsController增加代码:

//读取二级分类
$scope.$watch('entity.goods.category1Id', function(newValue, oldValue) {          
    //根据选择的值,查询二级分类
    itemCatService.findByParentId(newValue).success(
        function(response){
            $scope.itemCat2List=response; 	    			
        }
    );    	
}); 

$watch方法用于监控某个变量的值,当被监控的值发生变化,就自动执行相应的函数。

修改goods_edit.html中二级分类下拉框

<select class="form-control select-sm" ng-model="entity.goods.category2Id" ng-options="item.id as item.name for item in itemCat2List"></select>

4.3三级分类下拉选择框

在goodsController增加代码:

//读取三级分类
$scope.$watch('entity.goods.category2Id', function(newValue, oldValue) {          
    //根据选择的值,查询二级分类
    itemCatService.findByParentId(newValue).success(
        function(response){
            $scope.itemCat3List=response; 	    			
        }
    );    	
});

修改goods_edit.html中三级分类下拉框

<select class="form-control select-sm" ng-model="entity.goods.category3Id" ng-options="item.id as item.name for item in itemCat3List"></select>

4.4读取模板ID

在goodsController增加代码:

//三级分类选择后  读取模板ID
$scope.$watch('entity.goods.category3Id', function(newValue, oldValue) {    
    itemCatService.findOne(newValue).success(
        function(response){
            $scope.entity.goods.typeTemplateId=response.typeId; //更新模板ID    
        }
    );    
}); 

在goods_edit.html显示模板ID

模板ID:{{entity.goods.typeTemplateId}}

5.品牌选择

在用户选择商品分类后,品牌列表要根据用户所选择的分类进行更新。具体的逻辑是根据用户选择的三级分类找到对应的商品类型模板,商品类型模板中存储了品牌的列表json数据。

在goodsController引入typeTemplateService 并新增代码

//模板ID选择后  更新品牌列表
$scope.$watch('entity.goods.typeTemplateId', function(newValue, oldValue) {    
    typeTemplateService.findOne(newValue).success(
        function(response){
            $scope.typeTemplate=response;//获取类型模板
            $scope.typeTemplate.brandIds= JSON.parse( $scope.typeTemplate.brandIds);//品牌列表
        }
    );    
}); 

添加品牌选择框

<select class="form-control" ng-model="entity.goods.brandId" ng-options="item.id as item.text for item in typeTemplate.brandIds"></select>

成果:

image-20200705105410209

6.扩展属性

修改goodsController.js ,在用户更新模板ID时,读取模板中的扩展属性赋给商品的扩展属性。

//模板ID选择后  更新模板对象
$scope.$watch('entity.goods.typeTemplateId', function(newValue, oldValue) {    
    typeTemplateService.findOne(newValue).success(
        function(response){
            $scope.typeTemplate=response;//获取类型模板
            $scope.typeTemplate.brandIds= JSON.parse( $scope.typeTemplate.brandIds);//品牌列表
            $scope.entity.goodsDesc.customAttributeItems=JSON.parse( $scope.typeTemplate.customAttributeItems);//扩展属性
        }
    );    
});

修改goods_edit.html

<!--扩展属性-->
<div class="tab-pane" id="customAttribute">
    <div class="row data-type">                                
        <div ng-repeat="pojo in entity.goodsDesc.customAttributeItems">
            <div class="col-md-2 title">{{pojo.text}}</div>
            <div class="col-md-10 data">
                <input class="form-control" ng-model="pojo.value" placeholder="{{pojo.text}}">	            </div>
        </div>  				
    </div>
</div>

二、图片上传

1.分布式文件服务器FastDFS

1.1什么是FastDFS

FastDFS 是用 c 语言编写的一款开源的分布式文件系统。FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统 的文件系统来管理文件。可以将storage称为存储服务器。

image-20200705001948345

服务端两个角色:

Tracker:管理集群,tracker 也可以实现集群。每个 tracker 节点地位平等。收集 Storage 集群的状态。

Storage:实际保存文件 Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

1.2文件上传流程

image-20200705002021009

客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

image-20200705002041185

组名:文件上传后所在的 storage 组名称,在文件上传成功后有 storage 服务器返回,需要客户端自行保存。

虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项 store_path*对应。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。

数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。

文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

1.3文件下载流程

image-20200705002138617

最简单的 FastDFS 架构:

image-20200705002210178

1.4FastDFS入门小Demo

需求:将本地图片上传至图片服务器,再控制台打印url

创建Maven工程fastDFSdemo

由于FastDFS客户端jar包并没有在中央仓库中,所以需要使用下列命令手动安装jar包到Maven本地仓库(将jar包放到d盘setup目录)

mvn install:install-file -DgroupId=org.csource.fastdfs -DartifactId=fastdfs  -Dversion=1.2 -Dpackaging=jar -Dfile=d:\setup\fastdfs_client_v1.20.jar

pom.xml中引入

<dependency>
    <groupId>org.csource.fastdfs</groupId>
    <artifactId>fastdfs</artifactId>
    <version>1.2</version>
</dependency>

(2)添加配置文件fdfs_client.conf ,将其中的服务器地址设置为192.168.25.133

# connect timeout in seconds
# default value is 30s
connect_timeout=30

# network timeout in seconds
# default value is 30s
network_timeout=60

# the base path to store log files
base_path=/home/fastdfs

# tracker_server can ocur more than once, and tracker_server format is
#  "host:port", host can be hostname or ip address
tracker_server=192.168.25.133:22122

#standard log level as syslog, case insensitive, value list:
### emerg for emergency
### alert
### crit for critical
### error
### warn for warning
### notice
### info
### debug
log_level=info

# if use connection pool
# default value is false
# since V4.05
use_connection_pool = false

# connections whose the idle time exceeds this time will be closed
# unit: second
# default value is 3600
# since V4.05
connection_pool_max_idle_time = 3600

# if load FastDFS parameters from tracker server
# since V4.05
# default value is false
load_fdfs_parameters_from_tracker=false

# if use storage ID instead of IP address
# same as tracker.conf
# valid only when load_fdfs_parameters_from_tracker is false
# default value is false
# since V4.05
use_storage_id = false

# specify storage ids filename, can use relative or absolute path
# same as tracker.conf
# valid only when load_fdfs_parameters_from_tracker is false
# since V4.05
storage_ids_filename = storage_ids.conf


#HTTP settings
http.tracker_server_port=80

#use "#include" directive to include HTTP other settiongs
##include http.conf

(3)创建java类,main方法代码如下:

// 1、加载配置文件,配置文件中的内容就是 tracker 服务的地址。
ClientGlobal.init("D:/maven_work/fastDFS-demo/src/fdfs_client.conf");
// 2、创建一个 TrackerClient 对象。直接 new 一个。
TrackerClient trackerClient = new TrackerClient();
// 3、使用 TrackerClient 对象创建连接,获得一个 TrackerServer 对象。
TrackerServer trackerServer = trackerClient.getConnection();
// 4、创建一个 StorageServer 的引用,值为 null
StorageServer storageServer = null;
// 5、创建一个 StorageClient 对象,需要两个参数 TrackerServer 对象、StorageServer 的引用
StorageClient storageClient = new StorageClient(trackerServer, storageServer);
// 6、使用 StorageClient 对象上传图片。
//扩展名不带“.”
String[] strings = storageClient.upload_file("D:/pic/benchi.jpg", "jpg",null);
// 7、返回数组。包含组名和图片的路径。
for (String string : strings) {
    System.out.println(string);
}

控制台输出如下结果:

group1

M00/00/00/wKgZhVkMP4KAZEy-AAA-tCf93Fo973.jpg

在浏览器输入:

http://192.168.25.133/group1/M00/00/00/wKgZhVkMP4KAZEy-AAA-tCf93Fo973.jpg即可查看刚刚上传的图片

2.商品图片上传

2.1后端

(1)pinyougou-common工程pom.xml引入依赖

<!-- 文件上传组件 -->
<dependency>
    <groupId>org.csource.fastdfs</groupId>
    <artifactId>fastdfs</artifactId>
</dependency>
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
</dependency>	

(2)pinyougou-common工程创建FastDFSClient.java工具类

package util;

import org.csource.common.NameValuePair;
import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.StorageClient1;
import org.csource.fastdfs.StorageServer;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerServer;

public class FastDFSClient {

	private TrackerClient trackerClient = null;
	private TrackerServer trackerServer = null;
	private StorageServer storageServer = null;
	private StorageClient1 storageClient = null;
	
	public FastDFSClient(String conf) throws Exception {
		if (conf.contains("classpath:")) {
			conf = conf.replace("classpath:", this.getClass().getResource("/").getPath());
		}
		ClientGlobal.init(conf);
		trackerClient = new TrackerClient();
		trackerServer = trackerClient.getConnection();
		storageServer = null;
		storageClient = new StorageClient1(trackerServer, storageServer);
	}
	
	/**
	 * 上传文件方法
	 * <p>Title: uploadFile</p>
	 * <p>Description: </p>
	 * @param fileName 文件全路径
	 * @param extName 文件扩展名,不包含(.)
	 * @param metas 文件扩展信息
	 * @return
	 * @throws Exception
	 */
	public String uploadFile(String fileName, String extName, NameValuePair[] metas) throws Exception {
		String result = storageClient.upload_file1(fileName, extName, metas);
		return result;
	}
	
	public String uploadFile(String fileName) throws Exception {
		return uploadFile(fileName, null, null);
	}
	
	public String uploadFile(String fileName, String extName) throws Exception {
		return uploadFile(fileName, extName, null);
	}
	
	/**
	 * 上传文件方法
	 * <p>Title: uploadFile</p>
	 * <p>Description: </p>
	 * @param fileContent 文件的内容,字节数组
	 * @param extName 文件扩展名
	 * @param metas 文件扩展信息
	 * @return
	 * @throws Exception
	 */
	public String uploadFile(byte[] fileContent, String extName, NameValuePair[] metas) throws Exception {
		
		String result = storageClient.upload_file1(fileContent, extName, metas);
		return result;
	}
	
	public String uploadFile(byte[] fileContent) throws Exception {
		return uploadFile(fileContent, null, null);
	}
	
	public String uploadFile(byte[] fileContent, String extName) throws Exception {
		return uploadFile(fileContent, extName, null);
	}
}

(3)fdfs_client.conf 拷贝到pinyougou-shop-web工程config文件夹

(4)在pinyougou-shop-web工程springmvc.xml添加配置:

<!-- 配置多媒体解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<property name="defaultEncoding" value="UTF-8"></property>
		<!-- 设定文件上传的最大值5MB,5*1024*1024 -->
		<property name="maxUploadSize" value="5242880"></property>
</bean>

(5)新建UploadController.java

/**
 * 文件上传Controller
 * @author Administrator
 *
 */
@RestController
public class UploadController {
	
	@Value("${FILE_SERVER_URL}")
	private String FILE_SERVER_URL;//文件服务器地址

	@RequestMapping("/upload")
	public Result upload( MultipartFile file){				
		//1、取文件的扩展名
		String originalFilename = file.getOriginalFilename();
		String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
		try {
//2、创建一个 FastDFS 的客户端
			FastDFSClient fastDFSClient  
= new FastDFSClient("classpath:config/fdfs_client.conf");
			//3、执行上传处理
			String path = fastDFSClient.uploadFile(file.getBytes(), extName);
			//4、拼接返回的 url 和 ip 地址,拼装成完整的 url
			String url = FILE_SERVER_URL + path;			
			return new Result(true,url);			
		} catch (Exception e) {
			e.printStackTrace();
			return new Result(false, "上传失败");
		}		
	}	
}

2.2前端

(1)创建uploadService.js

//文件上传服务层
app.service("uploadService",function($http){
	this.uploadFile=function(){
		var formData=new FormData();
	    formData.append("file",file.files[0]);   
		return $http({
            method:'POST',
            url:"../upload.do",
            data: formData,
            headers: {'Content-Type':undefined},
            transformRequest: angular.identity
        });		
	}	
});

anjularjs对于post和get请求默认的Content-Type header 是application/json。通过设置‘Content-Type’: undefined,这样浏览器会帮我们把Content-Type 设置为 multipart/form-data.

通过设置 transformRequest: angular.identity ,anjularjs transformRequest function 将序列化我们的formdata object.

(2)将uploadService服务注入到goodsController 中,同时记住引入js

//商品控制层(商家后台)
app.controller('goodsController' ,function($scope,$controller   ,goodsService,itemCatService,uploadService){
  
<script type="text/javascript" src="../js/service/uploadService.js">  </script>
/**
	 * 上传图片
	 */
$scope.uploadFile=function(){	  
    uploadService.uploadFile().success(function(response) {        	
        if(response.success){//如果上传成功,取出url
            $scope.image_entity.url=response.message;//设置文件地址
        }else{
            alert(response.message);
        }
    }).error(function() {           
        alert("上传发生错误");
    });        
};    

(3)修改图片上传窗口,调用上传方法,回显上传图片

<img  src="{{image_entity.url}}" width="200px" height="200px">

2.3图片列表

(1)在goodsController.js增加方法

$scope.entity={goods:{},goodsDesc:{itemImages:[]}};//定义页面实体结构
//添加图片列表
$scope.add_image_entity=function(){    	
    $scope.entity.goodsDesc.itemImages.push($scope.image_entity);
}

(2)修改上传窗口的保存按钮

<button class="btn btn-success" ng-click="add_image_entity()" data-dismiss="modal" aria-hidden="true">保存</button>

(3)遍历图片列表

<tr ng-repeat="pojo in entity.goodsDesc.itemImages">
	 <td>{{pojo.color}}</td>
	 <td><img alt="" src="{{pojo.url}}" width="100px" height="100px"></td>
	<td><button type="button" class="btn btn-default" title="删除" ><i class="fa fa-trash-o"></i> 删除</button></td>
</tr>

2.4移除图片

在goodsController.js增加代码

//列表中移除图片
$scope.remove_image_entity=function(index){
    $scope.entity.goodsDesc.itemImages.splice(index,1);
}

修改列表中的删除按钮

<button type="button" class="btn btn-default" title="删除" ng-click="remove_image_entity($index)"><i class="fa fa-trash-o"></i> 删除</button>

三、商品规格

1.规格选择

显示规格及选项列表(复选框)如下图,并保存用户选择的结果

image-20200705162659246

1.1显示规格选项列表

由于我们的模板中只记录了规格名称,而我们除了显示规格名称还是显示规格下的规格选项,所以我们需要在后端扩充方法。

(1)在pinyougou-sellergoods-interface的TypeTemplateService.java新增方法定义

/**
	 * 返回规格列表
	 * @return
	 */
public List<Map> findSpecList(Long id);

(2)在pinyougou-sellergoods-service的TypeTemplateServiceImpl.java新增方法

@Autowired
private TbSpecificationOptionMapper specificationOptionMapper;

@Override
public List<Map> findSpecList(Long id) {
    //查询模板
    TbTypeTemplate typeTemplate = typeTemplateMapper.selectByPrimaryKey(id);

    List<Map> list = JSON.parseArray(typeTemplate.getSpecIds(), Map.class)  ;
    for(Map map:list){
        //查询规格选项列表
        TbSpecificationOptionExample example=new TbSpecificationOptionExample();
        com.pinyougou.pojo.TbSpecificationOptionExample.Criteria criteria = example.createCriteria();
        criteria.andSpecIdEqualTo( new Long( (Integer)map.get("id") ) );
        List<TbSpecificationOption> options = specificationOptionMapper.selectByExample(example);
        map.put("options", options);
    }		
    return list;
}

(3)在pinyougou-shop-web的TypeTemplateController.java新增方法

@RequestMapping("/findSpecList")
public List<Map> findSpecList(Long id){
    return typeTemplateService.findSpecList(id);
}

测试后端代码:

image-20200705133302807

(4)前端代码:修改typeTemplateService.js

//查询规格列表
this.findSpecList=function(id){
    return $http.get('../typeTemplate/findSpecList.do?id='+id);
}

(5)修改goodsController.js

//查询规格列表
typeTemplateService.findSpecList(newValue).success(
    function(response){
        $scope.specList=response;
    }
);    	

(6)修改goods_edit.html页面

<div ng-repeat="pojo in specList">
    <div class="col-md-2 title">{{pojo.text}}</div>
    <div class="col-md-10 data">         
        <span ng-repeat="option in pojo.options">
            <input  type="checkbox" >{{option.optionName}}	     
        </span>  
    </div>
</div>   

1.2保存选中规格选项

我们需要将用户选中的选项保存在tb_goods_desc表的specification_items字段中,定义json格式如下:

[{“attributeName”:”规格名称”,”attributeValue”:[“规格选项1”,“规格选项2”… ] } , … ]

(1)在baseController.js增加代码

//从集合中按照key查询对象
$scope.searchObjectByKey=function(list,key,keyValue){
    for(var i=0;i<list.length;i++){
        if(list[i][key]==keyValue){
            return list[i];
        }			
    }		
    return null;
}

(2)在goodsController.js增加代码

$scope.updateSpecAttribute=function($event,name,value){
    var object=$scope.searchObjectByKey($scope.entity.goodsDesc.specificationItems,"attributeName",name);
    if(object!=null){//有值说明这个规格名称中已有被选中的
        //判断是否勾选
        if($event.target.checked){
            object.attributeValue.push(value);//在这个规格名称中添加规格参数
        }else{//取消勾选	
            object.attributeValue.splice(object.attributeValue.indexOf(value),1);//移除选项
            //如果选项都取消了,将此条记录移除
            if(object.attributeValue.length==0){
                $scope.entity.goodsDesc.specificationItems.splice(
                    $scope.entity.goodsDesc.specificationItems.indexOf(object),1);
            }
        }
    }else{
        $scope.entity.goodsDesc.specificationItems.push(
            {"attributeName":name,"attributeValue":[value]});//添加新的记录
    }
}

(3)在goods_edit.html调用方法

<input  type="checkbox" ng-click="updateSpecAttribute($event,pojo.text,option.optionName)">{{option.optionName}}		

image-20200705165709305

2.SKU商品信息

基于上一步我们完成的规格选择,根据选择的规格录入商品的SKU信息,当用户选择相应的规格,下面的SKU列表就会自动生成,如下图:image-20200705194152148

2.1实现思路

(1)我们先定义一个初始的不带规格名称的集合,只有一条记录。

(2)循环用户选择的规格,根据规格名称和已选择的规格选项对原集合进行扩充,添加规格名称和值,新增的记录数与选择的规格选项个数相同

image-20200705194225690

2.2克隆

常规上我们把克隆分为浅克隆和深克隆。浅克隆是克隆结果随着被克隆的那个发生变化,而深克隆恰恰相反。

  1. 浅克隆:var a={}; b=a;
  2. 深克隆:var a={name:‘abc’} var b={name:‘abc’}

技巧:var b = JSON.parse(JSON.stringify(a));

后端同理

2.3生成SKU列表(深克隆)

(1)在goodsController.js实现创建sku列表的方法

//创建SKU列表	
$scope.createItemList=function(){
    $scope.entity.itemList=[{spec:{},price:0,num:99999,status:'0',isDefault:'0' } ];//初始化
    var items = $scope.entity.goodsDesc.specificationItems;	
    for(var i=0;i<items.length;i++){
        $scope.entity.itemList = addColumn( $scope.entity.itemList,items[i].attributeName,items[i].attributeValue );
    }
}

//添加列值——在原有记录上出现了新的规格名称,需要添加
//list——每条记录		columnName——规格名称 	conlumnValues——规格值
addColumn=function(list,columnName,conlumnValues){
    var newList=[];//新的集合
    for(var i=0;i<list.length;i++){//先遍历原有的
        var oldRow=list[i];
        for(var j=0;j<conlumnValues.length;j++){
            var newRow= JSON.parse(JSON.stringify(oldRow));//深克隆
            newRow.spec[columnName]=conlumnValues[j];
            newList.push(newRow);
        }
    }
    return newList;
}

(2)在更新规格属性后调用生成SKU列表的方法

<input  type="checkbox" ng-click="updateSpecAttribute($event,pojo.text,option.optionName);createItemList()">{{option.optionName}}	

效果如下:

image-20200705194740112

2.4显示SKU列表

goods_edit.html页面上绑定SKU列表

<table class="table table-bordered table-striped table-hover dataTable">
    <thead>
        <tr>					                          
            <th class="sorting" ng-repeat="item in entity.goodsDesc.specificationItems">{{item.attributeName}}</th>
            <th class="sorting">价格</th>
            <th class="sorting">库存</th>
            <th class="sorting">是否启用</th>
            <th class="sorting">是否默认</th>
        </tr>
    </thead>
    <tbody>
        <tr ng-repeat="pojo in entity.itemList">					                           
            <td ng-repeat="item in entity.goodsDesc.specificationItems">
                {{pojo.spec[item.attributeName]}}
            </td>													
            <td>
                <input class="form-control" ng-model="pojo.price"  placeholder="价格">
            </td>
            <td>
                <input class="form-control" ng-model="pojo.num" placeholder="库存数量">
            </td>
            <td>
                <input type="checkbox" ng-model="pojo.status" ng-true-value="1" ng-false-value="0" >
            </td>
            <td>
                <input type="checkbox" ng-model="pojo.isDefault" ng-true-value="1" ng-false-value="0">									             	
            </td>
        </tr>
    </tbody>
</table>

成果:

image-20200705195913791

2.5后端

(1)在GoodsServiceImpl添加属性

@Autowired
private TbItemMapper itemMapper;

@Autowired
private TbBrandMapper brandMapper;

@Autowired
private TbItemCatMapper itemCatMapper;

@Autowired
private TbSellerMapper sellerMapper;

(2)修改GoodsServiceImpl的add方法,增加代码,实现对SKU商品信息的保存

/**
 * 增加
 */
@Override
public void add(Goods goods) {
    goods.getGoods().setAuditStatus("0");		
    goodsMapper.insert(goods.getGoods());	//插入商品表
    goods.getGoodsDesc().setGoodsId(goods.getGoods().getId());
    goodsDescMapper.insert(goods.getGoodsDesc());//插入商品扩展数据
    for(TbItem item :goods.getItemList()){
        //标题
        String title= goods.getGoods().getGoodsName();
        Map<String,Object> specMap = JSON.parseObject(item.getSpec());
        for(String key:specMap.keySet()){
            title+=" "+ specMap.get(key);
        }
        item.setTitle(title);		
        item.setGoodsId(goods.getGoods().getId());//商品SPU编号
        item.setSellerId(goods.getGoods().getSellerId());//商家编号
        item.setCategoryid(goods.getGoods().getCategory3Id());//商品分类编号(3级)
        item.setCreateTime(new Date());//创建日期
        item.setUpdateTime(new Date());//修改日期 
        //品牌名称
        TbBrand brand = brandMapper.selectByPrimaryKey(goods.getGoods().getBrandId());
        item.setBrand(brand.getName());
        //分类名称
        TbItemCat itemCat = itemCatMapper.selectByPrimaryKey(goods.getGoods().getCategory3Id());
        item.setCategory(itemCat.getName());		
        //商家名称
        TbSeller seller = sellerMapper.selectByPrimaryKey(goods.getGoods().getSellerId());
        item.setSeller(seller.getNickName());		
        //图片地址(取spu的第一个图片)
        List<Map> imageList = JSON.parseArray(goods.getGoodsDesc().getItemImages(), Map.class) ;
        if(imageList.size()>0){
            item.setImage ( (String)imageList.get(0).get("url"));
        }		
        itemMapper.insert(item);
    }		
}

3.是否启用规格

在规格面板添加是否启用规格,当用户没有选择该项,将原来的规格面板和SKU列表隐藏,用户保存商品后只生成一个SKU.

3.1前端

goods_add.html添加复选框

<div class="col-md-2 title">是否启用规格</div>
<div class="col-md-10 data">
    <input type="checkbox"  ng-model="entity.goods.isEnableSpec" ng-true-value="1" ng-false-value="0">
</div>
</div>

用if指令控制规格面板与SKU列表的显示与隐藏

<div ng-if="entity.goods.isEnableSpec==1">
    ......SKU表格部分
</div>

3.2后端

修改GoodsServiceImpl的add方法

/**
	 * 增加
	 */
@Override
public void add(Goods goods) {
    goods.getGoods().setAuditStatus("0");		
    goodsMapper.insert(goods.getGoods());	//插入商品表
    goods.getGoodsDesc().setGoodsId(goods.getGoods().getId());
    goodsDescMapper.insert(goods.getGoodsDesc());//插入商品扩展数据
    if("1".equals(goods.getGoods().getIsEnableSpec())){
        for(TbItem item :goods.getItemList()){
            //标题
            String title= goods.getGoods().getGoodsName();
            Map<String,Object> specMap = JSON.parseObject(item.getSpec());
            for(String key:specMap.keySet()){
                title+=" "+ specMap.get(key);
            }
            item.setTitle(title);
            setItemValus(goods,item);
            itemMapper.insert(item);
        }		
    }else{					
        TbItem item=new TbItem();
        item.setTitle(goods.getGoods().getGoodsName());//商品KPU+规格描述串作为SKU名称
        item.setPrice( goods.getGoods().getPrice() );//价格			
        item.setStatus("1");//状态
        item.setIsDefault("1");//是否默认			
        item.setNum(99999);//库存数量
        item.setSpec("{}");			
        setItemValus(goods,item);					
        itemMapper.insert(item);
    }	
}

private void setItemValus(Goods goods,TbItem item) {
    item.setGoodsId(goods.getGoods().getId());//商品SPU编号
    item.setSellerId(goods.getGoods().getSellerId());//商家编号
    item.setCategoryid(goods.getGoods().getCategory3Id());//商品分类编号(3级)
    item.setCreateTime(new Date());//创建日期
    item.setUpdateTime(new Date());//修改日期 

    //品牌名称
    TbBrand brand = brandMapper.selectByPrimaryKey(goods.getGoods().getBrandId());
    item.setBrand(brand.getName());
    //分类名称
    TbItemCat itemCat = itemCatMapper.selectByPrimaryKey(goods.getGoods().getCategory3Id());
    item.setCategory(itemCat.getName());

    //商家名称
    TbSeller seller = sellerMapper.selectByPrimaryKey(goods.getGoods().getSellerId());
    item.setSeller(seller.getNickName());

    //图片地址(取spu的第一个图片)
    List<Map> imageList = JSON.parseArray(goods.getGoodsDesc().getItemImages(), Map.class) ;
    if(imageList.size()>0){
        item.setImage ( (String)imageList.get(0).get("url"));
    }		
}

四、商品管理

1.商品列表

在商家后台,显示该商家的商品列表信息,如下图:

image-20200706123556595

1.1后端

修改pinyougou-shop-web工程的GoodsController.java的search方法

修改pinyougou-shop-web工程的GoodsController.java的search方法
    @RequestMapping("/search")
    public PageResult search(@RequestBody TbGoods goods, int page, int rows  ){
    //获取商家ID
    String sellerId = SecurityContextHolder.getContext().getAuthentication().getName();
    //添加查询条件 
    goods.setSellerId(sellerId);		
    return goodsService.findPage(goods, page, rows);		
}

修改pinyougou-sellergoods-service 工程com.pinyougou.sellergoods.service.impl 的findPage方法,修改条件构建部分代码,将原来的模糊匹配修改为精确匹配

if(goods.getSellerId()!=null && goods.getSellerId().length()>0){
    //criteria.andSellerIdLike("%"+goods.getSellerId()+"%");
    criteria.andSellerIdEqualTo(goods.getSellerId());
}

1.2前端

1、循环列表:

<tr ng-repeat="entity in list">
    <td><input  type="checkbox"></td>			                              
    <td>{{entity.id}}</td>
    <td>{{entity.goodsName}}</td>
    <td>{{entity.price}}</td>
    <td>{{entity.category1Id}}</td>
    <td>{{entity.category2Id}}</td>
    <td>{{entity.category3Id}}</td>
    <td>
        {{entity.auditStatus}}
    </td>		                                  
    <td class="text-center">                                          
        <button type="button" class="btn bg-olive btn-xs">修改</button>                  
    </td>
</tr>

2、显示状态:

修改goodsController.js,添加state数组

$scope.status=['未审核','已审核','审核未通过','关闭'];//商品状态

修改列表显示

{{status[entity.auditStatus]}}

3、显示分类:

我们现在的列表中的分类仍然显示ID

如何才能显示分类的名称呢?

方案一:在后端代码写关联查询语句,返回的数据中直接有分类名称。

方案二:在前端代码用ID去查询后端,异步返回商品分类名称。

我们目前采用方案二,因为商品分类并不多,我们可以直接通过前端存入内存,这样效率高很多,而不是每次都要去后端查一下

(1)修改goodsController

$scope.itemCatList=[];//商品分类列表
//加载商品分类列表
$scope.findItemCatList=function(){		
    itemCatService.findAll().success(
        function(response){							
            for(var i=0;i<response.length;i++){
                $scope.itemCatList[response[i].id]=response[i].name;
            }
        }
    );
}

代码解释:因为我们需要根据分类ID得到分类名称,所以我们将返回的分页结果以数组形式再次封装。

(2)修改goods.html ,增加初始化调用

<body class="hold-transition skin-red sidebar-mini" ng-app="pinyougou" ng-controller="goodsController" ng-init="findItemCatList()">
    
<td>{{itemCatList[entity.category1Id]}}</td>
<td>{{itemCatList[entity.category2Id]}}</td>
<td>{{itemCatList[entity.category3Id]}}</td>

4、条件查询:

根据状态和商品名称进行查询,修改goods.html

<div class="has-feedback">
    状态:<select ng-model="searchEntity.auditStatus">
    <option value="">全部</option>      
    <option value="0">未审核</option>    
    <option value="1">已审核</option>    
    <option value="2">审核未通过</option>    
    <option value="3">关闭</option>                                     
    </select>
    商品名称:<input ng-model="searchEntity.goodsName">						
    <button class="btn btn-default" ng-click="reloadList()">查询</button>                                    
</div>

2.商品修改

在商品列表页面点击修改,进入商品编辑页面,并传递参数商品ID,商品编辑页面接受该参数后从数据库中读取商品信息,用户修改后保存信息。

2.1基本信息读取

我们首选读取商品分类、商品名称、品牌,副标题,价格,商品介绍等信息

读这类信息需要我们知道商品ID,而我们知道一般.html后面是不好带参数的,这里我们用到AngularJS内置的./html可以传参的方法$location,后端代码这里很简单,不过多赘述

(1)在goodsController中引入$location服务

(2)修改goodsController 添加代码:

//查询实体 
$scope.findOne=function(){			
    var id= $location.search()['id'];//获取参数值
    if(id==null){
        return ;
    }
    goodsService.findOne(id).success(
        function(response){
            $scope.entity= response;	
            //向富文本编辑器添加商品介绍
            editor.html($scope.entity.goodsDesc.introduction);
        }
    );				
}

$location.search()其实就是把页面所有的变量都集合到一个数组中

测试:地址栏输入

http://localhost:9102/admin/goods_edit.html#?id=商品ID

注意: ?前要加# ,则是angularJS的地址路由的书写形式

2.2读取商品图片和扩展属性

修改goodsController.js 的findOne

//显示图片列表
$scope.entity.goodsDesc.itemImages= JSON.parse($scope.entity.goodsDesc.itemImages);
//显示扩展属性
$scope.entity.goodsDesc.customAttributeItems=  JSON.parse($scope.entity.goodsDesc.customAttributeItems);	

经过测试,我们发现扩展属性值并没有读取出来,这是因为与之前读取扩展属性名称发生冲突,读出来后被初始化了。我们需要改写代码, 添加判断,当用户没有传递id参数时再执行此逻辑

//监控模板ID ,读取品牌列表
$scope.$watch('entity.goods.typeTemplateId',function(newValue,oldValue){
    //读取品牌列表和扩展属性
    typeTemplateService.findOne(newValue).success(
        function(response){
            .......
            //如果没有ID,则加载模板中的扩展数据
            if($location.search()['id']==null){
                $scope.entity.goodsDesc.customAttributeItems = JSON.parse($scope.typeTemplate.customAttributeItems);//扩展属性	
            }				
        }
    );
    .......
});

2.3读取商品规格

修改findOne

//规格				$scope.entity.goodsDesc.specificationItems=JSON.parse($scope.entity.goodsDesc.specificationItems);		

这里我们要把复选框状态显示出来,用到了ng-checked,方法返回true则勾选。

<input  type="checkbox"                          		
       ng-click="updateSpecAttribute($event,pojo.text,p.optionName);createSKUTable()"          		
       ng-checked="checkAttributeValue(pojo.text,p.optionName)">{{p.optionName}}
//根据规格名称和选项名称返回是否被勾选
$scope.checkAttributeValue=function(specName,optionName){
    var items= $scope.entity.goodsDesc.specificationItems;
    var object= $scope.searchObjectByKey(items,'attributeName',specName);
    if(object==null){
        return false;
    }else{
        if(object.attributeValue.indexOf(optionName)>=0){
            return true;
        }else{
            return false;
        }
    }			
}

2.4读取SKU数据

在GoodsServiceImpl的findOne方法中加载SKU商品数据

//查询SKU商品列表
TbItemExample example=new TbItemExample();
com.pinyougou.pojo.TbItemExample.Criteria criteria = example.createCriteria();
criteria.andGoodsIdEqualTo(id);//查询条件:商品ID
List<TbItem> itemList = itemMapper.selectByExample(example);		
goods.setItemList(itemList);

在goodsController.js修改findOne方法的代码

//SKU列表规格列转换——集合数组需要遍历转换			
for( var i=0;i<$scope.entity.itemList.length;i++ ){
    $scope.entity.itemList[i].spec = 
        JSON.parse( $scope.entity.itemList[i].spec);		
}			

成果:

image-20200706151011518

2.5保存数据

修改pinyougou-sellergoods-service的GoodsServiceImpl ,将SKU列表插入的代码提取出来,封装到私有方法中

/**
	 * 插入SKU列表数据
	 * @param goods
	 */
private void saveItemList(Goods goods) {
    if("1".equals(goods.getGoods().getIsEnableSpec())){
        for(TbItem item :goods.getItemList()){
            //标题
            String title= goods.getGoods().getGoodsName();
            Map<String,Object> specMap = JSON.parseObject(item.getSpec());
            for(String key:specMap.keySet()){
                title+=" "+ specMap.get(key);
            }
            item.setTitle(title);
            setItemValus(goods,item);
            itemMapper.insert(item);
        }		
    }else{					
        TbItem item=new TbItem();
        item.setTitle(goods.getGoods().getGoodsName());//商品KPU+规格描述串作为SKU名称
        item.setPrice( goods.getGoods().getPrice() );//价格			
        item.setStatus("1");//状态
        item.setIsDefault("1");//是否默认			
        item.setNum(99999);//库存数量
        item.setSpec("{}");			
        setItemValus(goods,item);					
        itemMapper.insert(item);
    }
}

接下来,我们修改update方法,实现修改

public void update(Goods goods){
    goods.getGoods().setAuditStatus("0");//设置未申请状态:如果是经过修改的商品,需要重新设置状态
    goodsMapper.updateByPrimaryKey(goods.getGoods());//保存商品表
    goodsDescMapper.updateByPrimaryKey(goods.getGoodsDesc());//保存商品扩展表
    //删除原有的sku列表数据		
    TbItemExample example=new TbItemExample();
    com.pinyougou.pojo.TbItemExample.Criteria criteria = example.createCriteria();
    criteria.andGoodsIdEqualTo(goods.getGoods().getId());	
    itemMapper.deleteByExample(example);
    //添加新的sku列表数据
    saveItemList(goods);//插入商品SKU列表数据	
}	

修改GoodsController.java

@RequestMapping("/update")
public Result update(@RequestBody Goods goods){
    //校验是否是当前商家的id		
    Goods goods2 = goodsService.findOne(goods.getGoods().getId());
    //获取当前登录的商家ID
    String sellerId = SecurityContextHolder.getContext().getAuthentication().getName();
    //如果传递过来的商家ID并不是当前登录的用户的ID,则属于非法操作
    if(!goods2.getGoods().getSellerId().equals(sellerId) ||  !goods.getGoods().getSellerId().equals(sellerId) ){
        return new Result(false, "操作非法");		
    }		
    try {
        goodsService.update(goods);
        return new Result(true, "修改成功");
    } catch (Exception e) {
        e.printStackTrace();
        return new Result(false, "修改失败");
    }
}

代码解释:出于安全考虑,在商户后台执行的商品修改,必须要校验提交的商品属于该商户

修改goodsController.js ,新增保存的方法

//保存 
$scope.save=function(){			
    //提取文本编辑器的值
    $scope.entity.goodsDesc.introduction=editor.html();	
    var serviceObject;//服务层对象  				
    if($scope.entity.goods.id!=null){//如果有ID
        serviceObject=goodsService.update( $scope.entity ); //修改  
    }else{
        serviceObject=goodsService.add( $scope.entity  );//增加 
    }				
    serviceObject.success(
        function(response){
            if(response.success){
                alert('保存成功');					
                $scope.entity={};
                editor.html("");
            }else{
                alert(response.message);
            }
        }		
    );				
}

2.6页面跳转

(1)由商品列表页跳转到商品编辑页

<a href="goods_edit.html#?id={{entity.id}}" class="btn bg-olive btn-xs">修改</a>

(2)由商品编辑页跳转到商品列表

<a href="goods.html" class="btn btn-default">返回列表</a>

(3)保存成功后返回列表页面

if(response.success){					
    location.href="goods.html";//跳转到商品列表页
}

3.商品审核

待审核商品列表:

<body class="hold-transition skin-red sidebar-mini" ng-app="pinyougou" ng-controller="goodsController" ng-init="searchEntity={auditStatus:'0'};findItemCatList()">

需求:商品审核的状态值为1,驳回的状态值为2 。用户在列表中选中ID后,点击审核或驳回,修改商品状态,并刷新列表。

后端略

(1)修改pinyougou-manager-web的goodsService.js ,增加方法

//更改状态
this.updateStatus=function(ids,status){
    return $http.get('../goods/updateStatus.do?ids='+ids+"&status="+status);
}  

(2)修改pinyougou-manager-web的goodsController.js ,增加方法

//更改状态
$scope.updateStatus=function(status){		
    goodsService.updateStatus($scope.selectIds,status).success(
        function(response){
            if(response.success){//成功
                $scope.reloadList();//刷新列表
                $scope.selectIds=[];//清空ID集合
            }else{
                alert(response.message);
            }
        }
    );		
}

(3)修改pinyougou-manager-web的goods.html 页面,为复选框绑定事件指令

<input  type="checkbox" ng-click="updateSelection($event,entity.id)" >

<button type="button" class="btn btn-default" title="审核通过" ng-click="updateStatus('1')"><i class="fa fa-check"></i> 审核通过</button>
<button type="button" class="btn btn-default" title="驳回" ng-click="updateStatus('2')" ><i class="fa fa-ban"></i> 驳回</button>

4.商品删除

我们为商品管理提供商品删除功能,用户选中部分商品,点击删除按钮即可实现商品删除。注意,这里的删除并非是物理删除,而是修改tb_goods表的is_delete字段为1 ,我们可以称之为“逻辑删除”

修改pinyougou-sellergoods-service工程的GoodsServiceImpl.java的delete方法

/**
	 * 批量删除
	 */
@Override
public void delete(Long[] ids) {
    for(Long id:ids){
        TbGoods goods = goodsMapper.selectByPrimaryKey(id);
        goods.setIsDelete("1");
        goodsMapper.updateByPrimaryKey(goods);
    }		
}

排除已删除记录——修改pinyougou-sellergoods-service工程GoodsServiceImpl.java的findPage方法,添加以下代码:

criteria.andIsDeleteIsNull();//非删除状态

结语

不知道从什么时候开始,效率越来越差了இ௰இ

品优购系列可能要鸽几天了,建模搞得头疼🤕

//更改状态
$scope.updateStatus=function(status){		
    goodsService.updateStatus($scope.selectIds,status).success(
        function(response){
            if(response.success){//成功
                $scope.reloadList();//刷新列表
                $scope.selectIds=[];//清空ID集合
            }else{
                alert(response.message);
            }
        }
    );		
}

(3)修改pinyougou-manager-web的goods.html 页面,为复选框绑定事件指令

<input  type="checkbox" ng-click="updateSelection($event,entity.id)" >

<button type="button" class="btn btn-default" title="审核通过" ng-click="updateStatus('1')"><i class="fa fa-check"></i> 审核通过</button>
<button type="button" class="btn btn-default" title="驳回" ng-click="updateStatus('2')" ><i class="fa fa-ban"></i> 驳回</button>

4.商品删除

我们为商品管理提供商品删除功能,用户选中部分商品,点击删除按钮即可实现商品删除。注意,这里的删除并非是物理删除,而是修改tb_goods表的is_delete字段为1 ,我们可以称之为“逻辑删除”

修改pinyougou-sellergoods-service工程的GoodsServiceImpl.java的delete方法

/**
	 * 批量删除
	 */
@Override
public void delete(Long[] ids) {
    for(Long id:ids){
        TbGoods goods = goodsMapper.selectByPrimaryKey(id);
        goods.setIsDelete("1");
        goodsMapper.updateByPrimaryKey(goods);
    }		
}

排除已删除记录——修改pinyougou-sellergoods-service工程GoodsServiceImpl.java的findPage方法,添加以下代码:

criteria.andIsDeleteIsNull();//非删除状态

结语

不知道从什么时候开始,效率越来越差了இ௰இ

品优购系列可能要鸽几天了,建模搞得头疼🤕

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页

打赏作者

「已注销」

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值