dotnetcore+vue+elementUI 前后端分离架 二(后端篇)
前言
最近几年前后端分离架构大行其道,而且各种框架也是层出不穷。本文通过dotnetcore +vue 来介绍 前后端分离架构实战。
涉及的技术栈
服务端技术
mysql
本项目使用mysql 作为持久化层
本项目采用了 mysql 的示例 employees 数据库, 需要的朋友可以自行下载 。
http://www3.ntu.edu.sg/home/ehchua/programming/sql/SampleDatabases.html
orm
dapper 短小精悍,被称为orm中的瑞士军刀。作者之前使用EF 比较多,总感觉 EF 对一些复杂查询需要原生sql支持的不是那么好,EF 生成sql 不好控制,涉及对性能要求比较高的地方,优化起来不够理想。作者在最近的几个项目中接触到dapper,它对原生sql查询支持的相当给力,使用起来也比较简便,感觉真是爱不释手,嘿嘿。。。
https://github.com/StackExchange/Dapper架构
服务端整体架构采用仓储模式
Repository是一个独立的层,介于领域层与数据映射层(数据访问层)之间。它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问。Repository是仓库管理员,领域层需要什么东西只需告诉仓库管理员,由仓库管理员把东西拿给它,并不需要知道东西实际放在哪。
此处照搬了dudu同学对仓储模式的理解。
仓储模式的好处:领域依赖接口与DB进行交互,解除了二者之间的耦合,如果更换orm,DB层,采用这种模式,更换的代价最低。
前端技术
整体上SPA 结构
Vue:
渐进式 JavaScript 框架.
三大特点:易用,灵活, 高效。
详细信息见 https://cn.vuejs.org/VueRouter
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。
axios
axios 开始崛起, 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端。
ElementUI
网站快速成型工具
Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库
详细信息见 http://element-cn.eleme.io/#/zh-CN
服务端项目结构
服务端代码地址:https://github.com/hbb0b0/Hbb0b0.CMS/tree/master/Hbb0b0.CMS.WebAPI
摘取部分类予以说明。
CMS.Common
通用工具的封装
配置工具类
DB配置
Cors配置
WebApi配置
DB工具类
DapperContext
IRespository
ResponsitoryBase
包含获取列表,获取分页列表,增删改的通用操作的封装
SimpleCRUD
github上对Dapper的进一步分装
消息
数据转换
entity --> Dto
DTObase
PageList
CMS.DTO
返回给前端的数据对象,隔离DB model变化直接映射前端对象,使View与Model解耦。
CMS.Model
DB 中的表映射到 Model
CMS.IResponsity
DB 操作接口
CMS.Responsity
DB 操作接口的实现
CMS.IService
Serive接口,可以供给第三方调用,也是第三方调用的规范,同时也约束着服务实现。
目前提供给API层调用。
CMS.Service
Serive接口的实现
CMS.API
以WebAPI 形式提供给前台调用,并不是严格按照 WebApi 规范 post,get,put,delete 调用.使用了 core中的 mvc 方式。
CMS.API 调用 IService 接口
CMS.Service 调用 IResponsitory接口
各接口与实现之间通过Core中的构造函数注入方式联接
服务端代码说明:
配置
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Information"
}
},
"Console": {
"LogLevel": {
"Default": "Information"
}
}
},
"WebAPI": {
"DB": {
"ConnectionString": "server=localhost;database=employees;uid=root;pwd=sqlsa;charset='utf8';persistsecurityinfo=True;SslMode=none;Allow User Variables=True;",
"ReadingConnectionString": "server=localhost;database=employees;uid=root;pwd=sqlsa;charset='utf8';persistsecurityinfo=True;SslMode=none;Allow User Variables=True;",
"WritingConnectionString": "server=localhost;database=employees;uid=root;pwd=sqlsa;charset='utf8';persistsecurityinfo=True;SslMode=none;Allow User Variables=True;"
},
"Cors": {
"Name": "CMSCorsConfig",
"Original": "http://localhost:8080"
}
}
}
APIStartup
配置读取与接口实现配置
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CMS.Common;
using CMS.Common.Config;
using CMS.Common.DB;
using CMS.IRepository;
using CMS.IService;
using CMS.Repository;
using CMS.Service;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace CMS.WebApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
//序列化设置
.AddJsonOptions(options =>
{
//忽略循环引用
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
//不使用驼峰样式的key
//options.SerializerSettings.ContractResolver = new DefaultContractResolver();
//设置时间格式
options.SerializerSettings.DateFormatString = "yyyy-MM-dd hh:mm:ss";
}
);
services.AddOptions();
//读取配置
services.Configure<WebApiOption>(Configuration.GetSection("WebAPI"));
IServiceProvider serviceProvider = services.BuildServiceProvider();
//获取配置
WebApiOption config = serviceProvider.GetService<IOptions<WebApiOption>>().Value;
//Cors配置
AddCorsService(services,config);
//DB Service
AddDBService(services,config);
}
/// <summary>
/// AddDBService
/// </summary>
/// <param name="services"></param>
/// <param name="config"></param>
private void AddDBService(IServiceCollection services,WebApiOption config)
{
//设置全局配置
services.AddSingleton<IDapperContext>(_ => new DapperContext(
config
));
services.AddScoped<IDepartmentRep, DepartmentRep>();
services.AddScoped<IDepartmentService, DepartmentService>();
services.AddScoped<IEmployeeRep, EmployeeRep>();
services.AddScoped<IEmployeeService, EmployeeService>();
services.AddScoped<ITitleRep, TitleRep>();
services.AddScoped<ISalaryRep, SalaryRep>();
}
/// <summary>
/// AddCorsService
/// </summary>
/// <param name="services"></param>
/// <param name="config"></param>
private void AddCorsService(IServiceCollection services, WebApiOption config)
{
//添加cors 服务
services.AddCors(options =>
options.AddPolicy(WebApiOption.CORS_POLICY_NAME, p => p.WithOrigins(config.Cors.Original)
.AllowAnyMethod().AllowAnyHeader()));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{no?}",
defaults: new { controller = "Home", action = "Index" });
});
//配置Cors
app.UseCors(WebApiOption.CORS_POLICY_NAME);
}
}
}
ControllerBase
using CMS.Common.Config;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace CMS.WebApi.Common
{
[Route("api/[controller]")]
[EnableCors(WebApiOption.CORS_POLICY_NAME)]
public abstract class BaseController: Controller
{
protected readonly ILogger m_Logger;
public BaseController(ILogger<BaseController> logger)
{
m_Logger = logger;
}
}
}
EmployeeController
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CMS.Common;
using CMS.Common.Config;
using CMS.Common.Message;
using CMS.DTO;
using CMS.Entity;
using CMS.IService;
using CMS.WebApi.Common;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace CMS.WebApi.Controllers
{
public class EmployeeController : BaseController
{
private DBOption m_CmsOptions;
private IEmployeeService m_Service;
public EmployeeController(IOptions<DBOption> option, IEmployeeService employeeService, ILogger<EmployeeController> logger) : base(logger)
{
this.m_CmsOptions = option.Value;
m_Service = employeeService;
}
[Route("[action]/{pageNumber}/{rowsPerPage}")]
[HttpGet]
public ReponseMessage<PagedList<EmployeeDTO>> GetPagedList(int pageNumber, int rowsPerPage)
{
var result = m_Service.GetPagedList(pageNumber, rowsPerPage);
return m_Service.GetPagedList(pageNumber, rowsPerPage);
}
[Route("[action]")]
[HttpPost]
public ReponseMessage<PagedList<EmployeeDTO>> Query([FromBody]QueryCondition<EmployeeQuery> condition)
{
var result = m_Service.Query(condition);
m_Logger.LogInformation("GetPagedList:{0}", JsonConvert.SerializeObject(result));
return result;
}
[Route("[action]/{no}")]
[HttpGet]
public ReponseMessage<EmployeeDetailDto> GetDetail(string no)
{
var result = m_Service.GetDetail(no);
return result;
}
[Route("[action]")]
[HttpPost]
public ReponseMessage<int> Add([FromBody]EmployeeDTO dto)
{
var result = m_Service.Add(dto);
return result;
}
[Route("[action]/{empNo}")]
[HttpPost]
public ReponseMessage<int> Delete(int empNo)
{
var result = m_Service.Delete(empNo);
return result;
}
[Route("[action]/")]
[HttpPost]
public ReponseMessage<int> Update([FromBody]EmployeeDTO dto)
{
var result = m_Service.Update(dto);
return result;
}
}
}
IService
using CMS.Common;
using CMS.Common.Message;
using CMS.DTO;
using CMS.Entity;
using CMS.Model;
using System;
using System.Collections.Generic;
namespace CMS.IService
{
public interface IEmployeeService
{
ReponseMessage<PagedList<EmployeeDTO>> GetPagedList(int pageNumber, int rowsPerPage);
ReponseMessage<PagedList<EmployeeDTO>> Query(QueryCondition<EmployeeQuery> condition);
ReponseMessage<EmployeeDetailDto> GetDetail(string emp_no);
ReponseMessage<int> Add(EmployeeDTO dto);
ReponseMessage<int> Update(EmployeeDTO dto);
ReponseMessage<int> Delete(int emp_No);
}
}
Service 实现
using CMS.Common;
using CMS.Common.Message;
using CMS.DTO;
using CMS.Entity;
using CMS.IRepository;
using CMS.IService;
using CMS.Model;
using CMS.Utility;
using Dapper;
using System;
using System.Collections.Generic;
using System.Linq;
namespace CMS.Service
{
public class EmployeeService : IEmployeeService
{
private IEmployeeRep m_Rep;
private ITitleRep m_TitleRep;
private ISalaryRep m_SalaryRep;
public EmployeeService(IEmployeeRep rep,ITitleRep titleRep, ISalaryRep salaryRep)
{
m_Rep = rep;
m_TitleRep = titleRep;
m_SalaryRep = salaryRep;
}
public ReponseMessage<EmployeeDetailDto> GetDetail(string emp_no)
{
ReponseMessage<EmployeeDetailDto> result = new ReponseMessage<EmployeeDetailDto>();
EmployeeDetailDto data = new EmployeeDetailDto();
var emp = m_Rep.Get(emp_no);
data.Info = emp.JTransformTo<EmployeeDTO>();
var titleList = m_TitleRep.GetList(string.Format($"where emp_no = {emp_no}"));
var salaryList = m_SalaryRep.GetList(string.Format($"where emp_no = {emp_no} order by from_date "));
result.Data = data;
result.IsSuccess = true;
if (titleList != null)
{
result.Data.TitleList = titleList.JTransformTo<TitleDto>();
}
if (salaryList!=null)
{
result.Data.SalaryList = salaryList.JTransformTo<SalaryDto>();
}
return result;
}
public ReponseMessage<PagedList<EmployeeDTO>> GetPagedList(int pageNumber, int rowsPerPage)
{
int total = 0;
ReponseMessage<PagedList<EmployeeDTO>> result = new ReponseMessage<PagedList<EmployeeDTO>>();
result.Data = new PagedList<EmployeeDTO>();
var modelResult= m_Rep.GetPagedList(pageNumber, rowsPerPage, null, null, out total);
result.Data.TotalCount = total;
result.Data.Items=modelResult.Items.JTransformTo<EmployeeDTO>();
result.IsSuccess = true;
return result;
}
public ReponseMessage<PagedList<EmployeeDTO>> Query(QueryCondition<EmployeeQuery> condition)
{
ReponseMessage<PagedList<EmployeeDTO>> result = new ReponseMessage<PagedList<EmployeeDTO>>();
result.Data = new PagedList<EmployeeDTO>();
var modelResult = m_Rep.Query(condition);
result.Data.TotalCount = modelResult.TotalCount;
if (modelResult.TotalCount > 0)
{
result.Data.Items = modelResult.TotalCount>0 ? modelResult.Items.JTransformTo<EmployeeDTO>():new List<EmployeeDTO>();
}
result.IsSuccess = true;
return result;
}
public ReponseMessage<int> Add(EmployeeDTO dto)
{
ReponseMessage<int> result = new ReponseMessage<int>();
if (dto==null )
{
result.IsSuccess = false;
result.MessageInfo = new MessageInfo() { ErrorCode = -1, Message ="无效的参数" };
}
EmployeeModel entitity = dto.JTransformTo<EmployeeModel>();
int? opResult = m_Rep.CustomerAdd(entitity);
if(opResult.HasValue && opResult>0)
{
result.IsSuccess = true;
result.Data = opResult.Value;
}
return result;
}
public ReponseMessage<int> Update(EmployeeDTO dto)
{
ReponseMessage<int> result = new ReponseMessage<int>();
if (dto == null)
{
result.IsSuccess = false;
result.MessageInfo = new MessageInfo() { ErrorCode = -1, Message = "无效的参数" };
}
EmployeeModel entitity = dto.JTransformTo<EmployeeModel>();
int? opResult = m_Rep.Update(entitity);
if (opResult.HasValue && opResult > 0)
{
result.IsSuccess = true;
result.Data = opResult.Value;
}
return result;
}
public ReponseMessage<int> Delete(int empNo)
{
ReponseMessage<int> result = new ReponseMessage<int>();
int? opResult = m_Rep.Delete(empNo);
if (opResult.HasValue && opResult ==1)
{
result.IsSuccess = true;
result.Data = opResult.Value;
}
return result;
}
}
}
IResponsitory
using CMS.Common;
using CMS.Common.DB;
using CMS.DTO;
using CMS.Entity;
using CMS.Model;
using System;
using System.Collections.Generic;
using System.Text;
namespace CMS.IRepository
{
public interface IEmployeeRep: IRepository<EmployeeModel>
{
PagedList<EmployeeDTO> Query(QueryCondition<EmployeeQuery> query);
int? Add(EmployeeModel entity);
int? CustomerAdd(EmployeeModel entity);
int? Delete(int empNo);
int? Update(EmployeeModel entity);
}
}
-Responsitory 实现
using CMS.Common;
using CMS.Common.DB;
using CMS.DTO;
using CMS.IRepository;
using CMS.Model;
using System;
using System.Collections.Generic;
using System.Text;
using Dapper;
namespace CMS.Repository
{
public class EmployeeRep : ReponsitoryBase, IEmployeeRep
{
private static object m_sync_Object = new object();
public EmployeeRep(IDapperContext dapper) : base(dapper)
{
}
public int? Add(EmployeeModel entity)
{
return this.Insert(entity);
}
new public int? Update(EmployeeModel entity)
{
return base.Update(entity);
}
public int? CustomerAdd(EmployeeModel entity)
{
int cmdResult = -1;
using (var connection = this.GetWritingConnection())
{
var sql = new StringBuilder(" set @maxNo=(select max(emp_no)+1 from employees) ;");
sql.AppendLine("insert into employees(emp_no,first_name,last_name,gender,birth_date,hire_date) values(@maxNo,@first_name,@last_name,@gender,@birth_date,@hire_date); ");
sql.AppendLine("select @maxNo;");
cmdResult = connection.ExecuteScalar<int>(sql.ToString(), entity);
}
return cmdResult;
}
public int? Delete(int empNo)
{
int cmdResult = -1;
using (var connection = this.GetWritingConnection())
{
cmdResult = connection.Execute("delete from employees where emp_no = @emp_no ;", new { emp_no = empNo });
}
return cmdResult;
}
public PagedList<EmployeeDTO> Query(QueryCondition<EmployeeQuery> query)
{
PagedList<EmployeeDTO> pagedList = new PagedList<EmployeeDTO>();
#region sql
var sql = new StringBuilder("SELECT SQL_CALC_FOUND_ROWS * from employees ");
#endregion
sql.AppendLine(" Where 1=1");
if (!string.IsNullOrEmpty(query.Param.First_Name))
{
sql.AppendLine(string.Format(" and First_Name like '{0}'", query.GetLikeValue(query.Param.First_Name)));
}
if (!string.IsNullOrEmpty(query.Param.Last_Name))
{
sql.AppendLine(string.Format(" and last_Name like '{0}'", query.GetLikeValue(query.Param.Last_Name)));
}
if (!string.IsNullOrEmpty(query.Param.Emp_No))
{
sql.AppendLine(string.Format(" and emp_no = @Emp_No"));
}
if (!string.IsNullOrEmpty(query.Param.Gender))
{
sql.AppendLine(string.Format(" and gender = @Gender"));
}
DateTime? hire_date_start = null;
DateTime? hire_date_end = null;
if (query.Param.Hire_Date_Range != null)
{
if (query.Param.Hire_Date_Range[0].HasValue)
{
hire_date_start = query.Param.Hire_Date_Range[0];
sql.AppendLine(string.Format(" and hire_date >= @Hire_Date_Range_Start"));
}
if (query.Param.Hire_Date_Range[1].HasValue)
{
hire_date_end = query.Param.Hire_Date_Range[1];
sql.AppendLine(string.Format(" and hire_date <= @Hire_Date_Range_End"));
}
}
DateTime? birth_date_start = null;
DateTime? birth_date_end = null;
if (query.Param.Birth_Date_Range != null)
{
if (query.Param.Birth_Date_Range[0].HasValue)
{
birth_date_start = query.Param.Birth_Date_Range[0];
sql.AppendLine(string.Format(" and birth_date >= @Birth_Date_Range_Start"));
}
if (query.Param.Birth_Date_Range[1].HasValue)
{
birth_date_end = query.Param.Birth_Date_Range[1];
sql.AppendLine(string.Format(" and birth_date <= @Birth_Date_Range_End"));
}
}
sql.AppendLine(" order by emp_no desc");
sql.AppendLine($" LIMIT {(query.pageInfo.PageIndex - 1) * query.pageInfo.PageSize},{query.pageInfo.PageSize}");
sql.Append(";");
using (var connection = this.GetReadingConnection())
{
var result = connection.Query<EmployeeDTO>(sql.ToString(),
new
{
Emp_No = query.Param.Emp_No,
Hire_Date_Range_Start = hire_date_start,
Hire_Date_Range_End = hire_date_end,
Birth_Date_Range_Start = birth_date_start,
Birth_Date_Range_End = birth_date_end,
Gender = query.Param.Gender
}).AsList();
pagedList.Items = result;
pagedList.TotalCount = connection.ExecuteScalar<int>("SELECT FOUND_ROWS();");
}
return pagedList;
}
}
目前测试 empoyee 30万条分页数据大致在400ms左右
相关文章:
浅谈开发模式及架构发展
dotnet core webapi +vue 搭建前后端完全分离web架构(一)
原文地址:http://www.cnblogs.com/hbb0b0/p/8391598.html
.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com
dotnetcore+vue+elementUI 前后端分离架 二(后端篇)相关推荐
- dotnetcore+vue+elementUI 前后端分离 三(前端篇)
说明: 本项目使用了 mysql employees数据库,使用了vue + axois + element UI 2.0 ,演示了 单页程序 架构 ,vue router 的使用,axois 使用, ...
- 鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离构建工程项目管理系统
鸿鹄工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离构建工程项目管理系统 1. 项目背景 一.随着公司的快速发展,企业人员和经 ...
- springboot flask php,使用Vue,Spring Boot,Flask,Django 完成Vue前后端分离开发(二)
使用Vue完成前后端分离开发(二) Bravery never goes out of fashion. 勇敢永远不过时. 前面简单说了一下 Vue 项目的搭建和项目的大致页面,这里讲一下 Djang ...
- 基于SSM+SpringBoot+Vue+ElementUI前后端分离的校园岗位招聘就业管理系统
运行视频 基于SSM+SpringBoot+Vue+ElementUI前后端分离的校园岗位招聘就业管理系统 项目运行截图 学生管理 添加学生 学生信息 教师管理 教师信息 实习基地 公告信息 公司管理 ...
- spring boot+iview 前后端分离架构之前后端交互的实现(六)
spring boot 与 iview 前后端分离架构之前后端交互的实现(六) axios的跨域访问工具的封装实现 实现前后端交互-后端的实现 实现前后端交互-前端的实现 bug修复 mysql的ma ...
- 短视频社交|电影点播平台Springboot+vue+ElementUI前后端分离
感谢您的关注,请收藏以免忘记,点赞以示鼓励,评论给以建议,爱你哟 项目编号:BS-PT-071 一,项目简介 本项目基于Springboot+vue开发实现了一个电影点播和短视频分享平台,名为爱奇艺影 ...
- springboot+vue +ElementUI前后端分离框架搭建教程
项目介绍 一款 Java 语言基于 SpringBoot2.x.MybatisPlus.Vue.ElementUI.MySQL等框架精心打造的一款前后端分离框架,致力于实现模块化.组件化.可插拔的前后 ...
- 最棒的Vue+ElementUI前后端分离框架
项目介绍 一款 Java 语言基于 SpringBoot2.x.MybatisPlus.Vue.ElementUI.MySQL等框架精心打造的一款前后端分离框架,致力于实现模块化.组件化.可插拔的前后 ...
- 全新一代Vue+ElementUI前后端分离框架
项目介绍 一款 Java 语言基于 SpringBoot2.x.MybatisPlus.Vue.ElementUI.MySQL等框架精心打造的一款前后端分离框架,致力于实现模块化.组件化.可插拔的前后 ...
最新文章
- FFT IP核调用与仿真之SCALE压缩因子设置
- 【Java 并发编程】线程池机制 ( 测试线程开销 | 启动线程分析 | 用户态 | 内核态 | 用户线程 | 内核线程 | 轻量级进程 )
- linux开启内部路由转发功能
- Java中JSON字符串与java对象的互换实例详解
- Spring AOP capabilities and goals
- 韩顺平循序渐进学java 第13讲 抽象类.接口
- androidrunnable使用_Android Runnable 运行在那个线程
- jqGrid数据增删查改
- 下载百度地图瓦片和获取瓦片经纬度
- md5加密算法使用流程
- 我的服务器开发之路-服务器SSH默认端口22和ftp默认端口21修改
- PowerApps 的最近更新
- C++编程-牛客网-雀魂启动
- 文件不能设置默认打开方式怎么办?
- JVM概览:内存空间与数据存储
- 维护站点和建站时遇到的一些问题
- 关于阿里巴巴回购雅虎所持有股票的一些探讨
- js把string转化为json
- Java Transaction silently rolled back because it has been marked as rollback-only问题解决
- 国际自愿减排标准介绍:CDM、GS、VCS 、ACR、CCER等国内外自愿减排类型介绍