利用ASP.NET MVC+EasyUI+SqlServer搭建企业开发框架
我们要搭建的框架是企业级开发框架,适用用企业管理信息系统的开发,如:oa、hr等
1、框架名称:sampleframe。
2、搭建原则:最少知识原则。
3、先定义一个简单的编码规范:除类名和接口名外,其余首字母小写。
4、准备开发环境:vs2010及以上(mvc4)、sqlserver、jquery2.0和easyui1.4.5
首先来看一下列表页面的效果图:
我们期望简洁带前台代码,如下:
<table id="datagrid" class="easyui-datagrid" url="getlist" toolbar="#toolbar"> <thead> <tr> <th field="customerid" sortable="true" data-options="sortable:true" width="50"> customerid </th> <th field="companyname" data-options="sortable:true" width="50"> companyname </th> <th field="phone" width="50"> phone </th> <th field="address" width="50"> address </th> </tr> </thead> </table> <div id="toolbar"> <div> <a class="easyui-linkbutton" iconcls="icon-add" onclick="add();">添加</a> <a class="easyui-linkbutton" iconcls="icon-edit" onclick="edit();">修改</a> <a class="easyui-linkbutton" iconcls="icon-remove" onclick="del();">删除</a> </div> <div> <input id="querybox" class="easyui-textbox" buttonicon="icon-search" style="width: 200px;" data-options="onclickbutton:function(){loadlist(loadlistsettings);}" /> <a class="easyui-linkbutton" iconcls="icon-find" onclick="showquerywindow();"> 详细查询</a> </div> </div> <script type="text/javascript"> var loadlistsettings = { searchfields: "customerid,companyname", prompt: "请输入客户id或公司名称" }; </script> <script type="text/javascript"> pageinit(); </script>
为了这样简洁的前台,我们需要准备:
1、通用的布局模板_layout.cshtml。base.css、base.js、defaultsettings.js、tool.js和private.js见后面
<!doctype html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <meta http-equiv="x-ua-compatible" content="ie=edge,chrome=1" /> <link href='/webresource/easyui/themes/default/easyui.css' rel='stylesheet' type='text/css' /> <link href='/webresource/easyui/themes/icon.css' rel='stylesheet' type='text/css' /> <link href='/webresource/base.css' rel='stylesheet' type='text/css' /> <script src="/webresource/jquery/jquery.min.js" type="text/javascript"></script> <script src="/webresource/jquery/json2.js" type="text/javascript"></script> <script src="/webresource/easyui/jquery.easyui.min.js" type="text/javascript"></script> <script src="/webresource/easyui/locale/easyui-lang-zh_cn.js" type="text/javascript"></script> <script src="/webresource/tool.js" type="text/javascript"></script> <script src="/webresource/base.js" type="text/javascript"></script> <script src="/webresource/defaultsettings.js" type="text/javascript"></script> <script src="/webresource/private.js" type="text/javascript"></script> @rendersection("scripts", required: false) </head> <body style="width: 100%; height: 100%; margin: 0px;"> @renderbody() </body> </html>
2、平台样式表base.css
.icon-find { background: url('icons/find.png') no-repeat center center; } .datagrid-toolbar div:nth-child(1) { float: left; width: 60%; } .datagrid-toolbar div:nth-child(2) { margin-left: 60%; margin-right: 10px; width: auto; text-align: right; }
3、基本方法脚本base.js
//取消自动渲染 $.parser.auto = false; function pageinit() { //改变控件默认值 $.fn.linkbutton.defaults.plain = true; $.fn.datagrid.defaults.fit = true; $.fn.datagrid.defaults.fitcolumns = true; $.fn.datagrid.defaults.pagination = true; //显示ajax异常信息 $(document).ajaxerror(function (event, xhr, options, exc) { $.messager.alert({ title: '异步请求出错', msg: xhr.responsetext, icon: "error" }); }); //地址栏传参允许中文 jquery(document).ajaxsend(function (event, request, options) { options.url = encodeuri(options.url); }); $.parser.parse(); } function loadlist(settings) { var settings = $.extend(true, {}, loadlistdefaultsettings, settings); if ($("#" + settings.gridid).length == 0) $.messager.alert({ title: "系统异常", msg: "datagrid:" + settings.gridid + "不存在!", icon: "error" }); var quickquerydata = []; if ($("#" + settings.queryboxid).length > 0) { var val = $("#" + settings.queryboxid).textbox("getvalue"); if (settings.searchfields && val) { var keys = settings.searchfields.split(','); for (i = 0, len = keys.length; i < len; i++) { quickquerydata.push({ field: keys[i], method: 'inlike', value: val }); } } } var querydata = []; //详细查询预留 //加载数据 $("#" + settings.gridid).datagrid("load", { quickquerydata: json.stringify(quickquerydata), querydata: json.stringify(querydata) }); }
4、基本脚本默认值defaultsettings.js
//查询参数设置 var loadlistdefaultsettings = { url: "getlist", gridid: "datagrid", querywindowid: "querywindow", queryboxid: "querybox", searchfields: "name", addquerystring: true, prompt: "请输入" }; 5、可能需要使用的工具方法tool.js //判断是否包含地址栏参数 function hasquerystring(key, url) { if (typeof (url) == "undefined") url = window.location.search; var re = new regexp("[?&]" + key + "=([^\\&]*)", "i"); var a = re.exec(url); if (a == null) return false; return true; } //获取地址栏参数,如果参数不存在则返回空字符串 function getquerystring(key, url) { if (typeof (url) == "undefined") url = window.location.search; var re = new regexp("[?&]" + key + "=([^\\&]*)", "i"); var a = re.exec(url); if (a == null) return ""; return a[1]; } //将当前地址栏参数加入到url function addurlsearch(url) { var newparams = []; var paramkeys = window.location.search.replace('?', '').split('&'); for (var i = 0; i < paramkeys.length; i++) { var key = paramkeys[i].split('=')[0]; if (key == "" || key == "_t" || key == "_winid" || key == "istab") continue; if (!hasquerystring(key, url)) newparams.push(paramkeys[i]); } if (url.indexof('?') >= 0) return url + "&" + newparams.join('&'); else return url + "?" + newparams.join('&'); } //url增加参数 function addsearch(url, key, value) { if (!hasquerystring(key, url)) { if (url.indexof('?') >= 0) return url + "&" + key + "=" + value; else return url + "?" + key + "=" + value; } else return url; } //获取数组中对象的某个值,逗号分隔 function getvalues(rows, attr) { var fieldvalues = []; for (var i = 0; i < rows.length; i++) { if (rows[i] != null) fieldvalues.push(rows[i][attr]); } return fieldvalues.join(','); }
6、可能需要使用的私有方法private.js,此文件包含的方法专供base.js使用,开发web时禁止使用
//替换掉url中的{}参数 function replaceurl(settings, pty) { if (!pty) pty = "url"; if (!settings[pty]) return; var str = settings[pty]; var guid = ""; var result = str.replace(/\{[0-9a-za-z_]*\}/g, function (e) { var key = e.substring(1, e.length - 1); if (key == "guid") { if (!guid) { $.ajax({ url: "getguid", type: "post", async: false, success: function (text, textstatus) { guid = text; } }); } return guid; } if (hasquerystring(key)) //从地址栏返回 return getquerystring(key); if (settings.currentrow && settings.currentrow[key])//从当前行返回 return settings.currentrow[key]; if (settings.paramfrom) { //从指定控件返回 var ctrl = mini.get(settings.paramfrom); if (ctrl == undefined) { $.messager.alert({ title: 'ui出错', msg: "id为" + settings.paramfrom + "的控件不存在!", icon: "error" }); return; } else if (ctrl.showcheckbox) { return getvalues(ctrl.getcheckednodes(), key); } else if (ctrl.getselecteds) return getvalues(ctrl.getselecteds(), key); else if (ctrl.getvalue) return ctrl.getvalue(); } return e; }); settings[pty] = result; return result; } //转化为全路径 function changetofullurl(settings) { var url = settings.url; if (url.indexof('/') == 0 || url.indexof("http://") == 0 || url.indexof('?') == 0 || url == "") return url; currenturlpathname = window.location.pathname; var currentpathnameparts = currenturlpathname.split('/'); var pathnameparts = url.split('?')[0].split('/'); if (currentpathnameparts[currentpathnameparts.length - 1] == "") currentpathnameparts.pop(); //去掉一个反斜线 if (pathnameparts[pathnameparts.length - 1] == "") pathnameparts.pop(); //去掉一个反斜线 var index = currentpathnameparts.length - 1; for (var i = 0; i < pathnameparts.length; i++) { if (pathnameparts[i] == "..") { index = index - 1; if (index <= 0) { $.messager.alert({ title: "系统异常", msg: "url错误:" + url + "!", icon: "error" }); return; } continue; } if (index < currentpathnameparts.length) currentpathnameparts[index] = pathnameparts[i]; else currentpathnameparts.push(pathnameparts[i]); index = index + 1; } var length = currentpathnameparts.length; for (var i = index; i < length; i++) { currentpathnameparts.pop(); } var result = currentpathnameparts.join('/'); if (url.indexof('?') > 0) result += url.substring(url.indexof('?')); settings.url = result; }
我们期望简洁的后台代码,如下:
using system; using system.collections.generic; using system.linq; using system.web; using system.web.mvc; using formula; using system.data; namespace demo.areas.basic.controllers { public class customercontroller : basecontroller { public jsonresult getlist(querybuilder qb) { sqlhelper sqlhelper = new sqlhelper("demo"); var data = sqlhelper.executegriddata("select *,id=customerid from customer", qb); return json(data); } } }
为了这种简洁的代码我们需要:
1、controller基类basecontroller:
using system; using system.collections.generic; using system.linq; using system.text; using system.web.mvc; using system.data.entity; using system.data.sqlclient; using system.data.entity.validation; using system.componentmodel; using system.reflection; using system.web.security; using formula; namespace formula { public abstract class basecontroller : controller { #region 处理不存在的action protected override void handleunknownaction(string actionname) { if (request.httpmethod == "post") { httpcontext.clearerror(); httpcontext.response.clear(); httpcontext.response.statuscode = 500; httpcontext.response.write("没有action:" + actionname); httpcontext.response.end(); } // 搜索文件是否存在 var filepath = ""; if (routedata.datatokens["area"] != null) filepath = string.format("~/areas/{2}/views/{1}/{0}.cshtml", actionname, routedata.values["controller"], routedata.datatokens["area"]); else filepath = string.format("~/views/{1}/{0}.cshtml", actionname, routedata.values["controller"]); if (system.io.file.exists(server.mappath(filepath))) { view(filepath).executeresult(controllercontext); } else { httpcontext.clearerror(); httpcontext.response.clear(); httpcontext.response.statuscode = 500; httpcontext.response.write("没有action:" + actionname); httpcontext.response.end(); } } #endregion #region 基类json方法重载 protected override jsonresult json(object data, string contenttype, encoding contentencoding, jsonrequestbehavior behavior) { newtonjsonresult result = new newtonjsonresult() { data = data, contenttype = contenttype, contentencoding = contentencoding, jsonrequestbehavior = behavior }; return result; } protected override jsonresult json(object data, string contenttype, encoding contentencoding) { newtonjsonresult result = new newtonjsonresult() { data = data, contenttype = contenttype, contentencoding = contentencoding }; return result; } #endregion #region 异常处理 protected override void onexception(exceptioncontext filtercontext) { exception exp = filtercontext.exception; if (string.isnullorempty(exp.message)) exp = exp.getbaseexception(); if (filtercontext.requestcontext.httpcontext.request.isajaxrequest()) { var response = filtercontext.requestcontext.httpcontext.response; response.clear(); response.write(exp.message); response.statuscode = 500; response.end(); } } #endregion } }
2、查询构造器querybuilder:
using system; using system.collections.generic; using system.linq; using system.text; using system.collections; using system.web.mvc; namespace formula { [modelbinder(typeof(querybuilderbinder))] public class querybuilder : searchcondition { public int page { get; set; } public int rows { get; set; } public string sort { get; set; } public string order { get; set; } public int total { get; set; } public string getorderbystring(bool hasorderby = true) { var sortfields = this.sort.split(','); var sortorders = this.order.split(','); string str = ""; for (int i = 0; i < sortfields.length; i++) { str += sortfields[i] + " " + sortorders[i] + ","; } if (hasorderby && str != "") str = "order by " + str; return str.trim(','); } } public class searchcondition { public string fields = "*"; private list<conditionitem> quickitems = new list<conditionitem>(); private list<conditionitem> complexitems = new list<conditionitem>(); public searchcondition add(string field, string method, object val, bool isquicksearch = false) { //处理日期型数据 if (method == "<" || method == "<=") { if (val.gettype() == typeof(datetime)) { datetime t = (datetime)val; val = t.date.addhours(23).addminutes(59).addseconds(59); } } conditionitem item = new conditionitem(field, method, val); if (isquicksearch) quickitems.add(item); else complexitems.add(item); return this; } public string getwherestring(bool haswhere = true) { if (quickitems.count == 0 && complexitems.count == 0) return ""; string strwhere = ""; if (quickitems.count > 0) strwhere += " and (" + getgourpwherestring(quickitems, true) + ")"; if (complexitems.count > 0) strwhere += " and (" + getgourpwherestring(complexitems, false) + ")"; if (haswhere) strwhere = " where " + strwhere.substring(4); else strwhere = " and " + strwhere.substring(4); return strwhere; } #region 私有方法 private string getgourpwherestring(list<conditionitem> list, bool isorrelation = false) { if (list.count == 0) return ""; string strwhere = ""; for (int i = 0; i < list.count(); i++) { var item = list[i]; string str = item.getwherestring(); if (isorrelation) { strwhere += " or " + str; } else { strwhere += " and " + str; } } strwhere = strwhere.substring(4); return strwhere; } #endregion } public class conditionitem { public conditionitem(string field, string method, object val) { this.field = field; this.method = method; this.value = val; } public string field { get; set; } public string method { get; set; } public object value { get; set; } public string getwherestring() { var item = this; switch (item.method) { case "=": case "<": case ">": case "<=": case ">=": case "<>": return string.format("{0} {1} '{2}'", item.field, item.method, item.value); case "in": string v = ""; if (item.value is icollection) { icollection<string> collection = item.value as icollection<string>; v = string.join("','", collection.toarray<string>()); return string.format("{0} in('{1}')", item.field, v); } else { v = item.value.tostring().replace(",", "','"); } return string.format("{0} in ('{1}')", item.field, v); case "between": object[] objs = item.value as object[]; return string.format("{0} between '{1}' and '{2}'", item.field, objs[0], objs[1]); case "inlike": string[] arr = null; if (item.value is icollection) { icollection<string> collection = item.value as icollection<string>; arr = collection.toarray<string>(); } else { arr = item.value.tostring().split(',', ','); } string str = ""; foreach (string s in arr) { str += string.format("or {0} like '%{1}%'", item.field, s); } return "(" + str.substring(3) + ")"; case "day": datetime dt = datetime.now; if (!datetime.tryparse(item.value.tostring(), out dt)) { throw new buessinessexception("查询条件不能转化为日期时间"); } string start = dt.date.tostring("yyyy-mm-dd"); string end = dt.date.adddays(1).tostring("yyyy-mm-dd"); return string.format("{0} between '{1}' and '{2}'", item.field, start, end); case "startwith": return string.format("{0} like '{1}%'", item.field, item.value); case "endwith": return string.format("{0} like '%{1}'", item.field, item.value); default: return ""; } } } }
3、查询构造器querybuilder的创建方法querybuilderbinder:
using system; using system.collections.generic; using system.linq; using system.text; using system.web.mvc; namespace formula { public class querybuilderbinder : imodelbinder { public object bindmodel(controllercontext controllercontext, modelbindingcontext bindingcontext) { var qb = (querybuilder)(bindingcontext.model ?? new querybuilder()); var dict = controllercontext.httpcontext.request.params; var quickquerylist = !string.isnullorempty(dict["quickquerydata"]) ? jsonhelper.tolist(dict["quickquerydata"]) : new list<dictionary<string, object>>(); var querylist = !string.isnullorempty(dict["querydata"]) ? jsonhelper.tolist(dict["querydata"]) : new list<dictionary<string, object>>(); foreach (var dic in quickquerylist) { var val = dic["value"].tostring(); if (val == "") continue; qb.add(dic["field"].tostring(), dic["method"].tostring(), val, true); } foreach (var dic in querylist) { var val = dic["value"].tostring(); if (val == "") continue; qb.add(dic["field"].tostring(), dic["method"].tostring(), val, false); } qb.page = !string.isnullorempty(dict["page"]) ? int.parse(dict["page"].tostring()) : 1; qb.rows = !string.isnullorempty(dict["rows"]) ? int.parse(dict["rows"].tostring()) : 10; qb.sort = !string.isnullorempty(dict["sort"]) ? dict["page"].tostring() : "id"; qb.order = !string.isnullorempty(dict["order"]) ? dict["order"].tostring() : "desc"; return qb; } } }
4、数据库查询帮助类sqlhelper:
using system; using system.collections.generic; using system.linq; using system.text; using system.data.sqlclient; using system.data; using system.web; namespace formula { public class sqlhelper { #region 构造函数 public sqlhelper(string connname) { if (system.configuration.configurationmanager.connectionstrings[connname] == null) throw new buessinessexception(string.format("配置文件中不包含数据库连接字符串:{0}", connname)); this.connname = connname; this.connstring = system.configuration.configurationmanager.connectionstrings[connname].connectionstring; } public string connname { get; private set; } public string connstring { get; private set; } public string dbname { get { sqlconnection conn = new sqlconnection(connstring); return conn.database; } } #endregion #region 基本方法 public object executescalar(string cmdtext) { using (sqlconnection conn = new sqlconnection(connstring)) { conn.open(); sqlcommand cmd = new sqlcommand(cmdtext, conn); return cmd.executescalar(); } } public datatable executedatatable(string cmdtext) { using (sqlconnection conn = new sqlconnection(connstring)) { datatable dt = new datatable(); sqldataadapter apt = new sqldataadapter(cmdtext, conn); apt.fill(dt); return dt; } } public datatable executedatatable(string cmdtext, int start, int len) { using (sqlconnection conn = new sqlconnection(connstring)) { datatable dt = new datatable(); sqldataadapter apt = new sqldataadapter(cmdtext, conn); apt.fill(start, len, dt); return dt; } } public string executenonquery(string cmdtext) { using (sqlconnection conn = new sqlconnection(connstring)) { conn.open(); sqlcommand cmd = new sqlcommand(cmdtext, conn); return cmd.executenonquery().tostring(); } } #endregion #region 支持查询对象 public datatable executedatatable(string sql, searchcondition cnd, string orderby) { string sqlwhere = " where 1=1" + geturlfiltersqlwhere(sql) + cnd.getwherestring(false); sql = string.format("select {0} from ({1}) sourcetable {2} {3}", cnd.fields, sql, sqlwhere, orderby); datatable dt = this.executedatatable(sql); return dt; } public dictionary<string, object> executegriddata(string sql, querybuilder qb) { string sqlwhere = " where 1=1" + geturlfiltersqlwhere(sql) + qb.getwherestring(false); qb.total = (int)this.executescalar(string.format("select count(1) from ({0}) sourcetable {1}", sql, sqlwhere)); sql = string.format("select {0} from ({1}) sourcetable {2} {3}", qb.fields, sql, sqlwhere, qb.getorderbystring()); datatable dt = executedatatable(sql, (qb.page - 1) * qb.rows, qb.rows); dictionary<string, object> dic = new dictionary<string, object>(); dic.add("total", qb.total); dic.add("rows", dt); return dic; } #endregion #region 私有方法 private string geturlfiltersqlwhere(string sql) { sql = string.format("select * from({0}) as dt1 where 1=2", sql); var dtfield = executedatatable(sql); stringbuilder sb = new stringbuilder(); foreach (string key in httpcontext.current.request.querystring.keys) { if (string.isnullorempty(key) || key.tolower() == "id") continue; if (dtfield.columns.contains(key)) { string value = httpcontext.current.server.urldecode(httpcontext.current.request[key]); value = value.replace(",", "','"); sb.appendformat(" and {0} in ('{1}')", key, value); } } return sb.tostring(); } #endregion } }
5、用于取代返回值jsonresult的newtonjsonresult:
using system; using system.collections.generic; using system.linq; using system.text; using system.web.mvc; using system.web; using system.data; namespace formula { public class newtonjsonresult : jsonresult { public override void executeresult(controllercontext context) { //确认是否用于响应http-get请求 if (this.jsonrequestbehavior == jsonrequestbehavior.denyget && string.compare(context.httpcontext.request.httpmethod, "get", true) == 0) { throw new invalidoperationexception("禁止get请求"); } httpresponsebase response = context.httpcontext.response; //设置媒体类型和编码方式 response.contenttype = string.isnullorempty(this.contenttype) ? "application/json" : this.contenttype; if (this.contentencoding != null) { response.contentencoding = this.contentencoding; } //序列化对象,并写入当前的httpresponse if (null == this.data) return; if (this.data is string) { response.write(data); } else if (this.data is datarow) { dictionary<string, object> dic = new dictionary<string, object>(); datarow row = this.data as datarow; foreach (datacolumn col in row.table.columns) { dic.add(col.columnname, row[col]); } response.write(jsonhelper.tojson(dic)); } else { response.write(jsonhelper.tojson(this.data)); } } } }
6、json序列化和反序列的帮助类jsonhelper:
using system; using system.collections.generic; using system.linq; using system.text; using newtonsoft.json.converters; using newtonsoft.json; namespace formula { public static class jsonhelper { public static string tojson<t>(t obj) { if (obj == null || obj.tostring() == "null") return null; if (obj != null && (obj.gettype() == typeof(string) || obj.gettype() == typeof(string))) { return obj.tostring(); } isodatetimeconverter dt = new isodatetimeconverter(); dt.datetimeformat = "yyyy'-'mm'-'dd't'hh':'mm':'ss"; return jsonconvert.serializeobject(obj, dt); } /// <summary> /// 从一个json串生成对象信息 /// </summary> /// <param name="jsonstring">json字符串</param> /// <typeparam name="t">对象类型</typeparam> /// <returns></returns> public static t toobject<t>(string json) where t : class { if (string.isnullorempty(json)) return null; t obj = jsonconvert.deserializeobject<t>(json); return obj; } /// <summary> /// 返回 diction<string,object> /// </summary> /// <param name="json"></param> /// <returns></returns> public static dictionary<string, object> toobject(string json) { if (string.isnullorempty(json)) return new dictionary<string, object>(); return toobject<dictionary<string, object>>(json); } /// <summary> /// 返回 list<dictionary<string, object>> /// </summary> /// <param name="json"></param> /// <returns></returns> public static list<dictionary<string, object>> tolist(string json) { if (string.isnullorempty(json)) return new list<dictionary<string, object>>(); return toobject<list<dictionary<string, object>>>(json); } /// <summary> /// 组装对象 /// </summary> /// <param name="json"></param> /// <param name="obj"></param> public static void populateobject(string json, object obj) { if (string.isnullorempty(json)) return; jsonconvert.populateobject(json, obj); } } }
7、用于区分系统异常和业务异常的businessexception:
using system; using system.collections.generic; using system.linq; using system.text; using system.runtime.serialization; namespace formula { /// <summary> /// 业务异常 /// </summary> [serializable] public class buessinessexception : exception { /// <summary> /// 系统异常构造函数 /// </summary> public buessinessexception() { } /// <summary> /// 系统异常构造函数 /// </summary> /// <param name="message">异常的消息</param> public buessinessexception(string message) : base(message) { } /// <summary> /// 系统异常构造函数 /// </summary> /// <param name="message">异常的消息</param> /// <param name="inner">内部的异常</param> public buessinessexception(string message, system.exception inner) : base(message, inner) { } /// <summary> /// 系统异常构造函数 /// </summary> /// <param name="info">存有有关所引发异常的序列化的对象数据</param> /// <param name="context">包含有关源或目标的上下文信息</param> public buessinessexception(serializationinfo info, streamingcontext context) : base(info, context) { } } }
最后,上代码:http://xiazai.jb51.net/201604/yuanma/asp.net-mvc-easyui-sampleframe%28jb51.net%29.rar
推荐阅读
-
利用ASP.NET MVC+EasyUI+SqlServer搭建企业开发框架
-
利用ASP.NET MVC+EasyUI+SqlServer搭建企业开发框架
-
企业信息平台的快速搭建,框架如何选? 框架工作敏捷开发
-
SpringCloud 搭建企业级开发框架之实现多租户多平台短信通知服务(微服务实战)
-
利用maven搭建自己的快速开发框架和整合第三方框架
-
企业信息平台的快速搭建,框架如何选? 框架工作敏捷开发
-
asp.net开发微信公众平台(2)多层架构框架搭建和入口实现
-
asp.net开发微信公众平台(2)多层架构框架搭建和入口实现
-
SpringCloud 搭建企业级开发框架之实现多租户多平台短信通知服务(微服务实战)