可扩展的单据编号生成器 + 简单的解释器
可扩展的单据编号生成器 + 简单的解释器
背景
在企业应用中单据编号的自定义是一个很常见的需求,能不能抽象一个通用的框架呢?之前写个一篇 自定义密码强度 的博文,感觉他们两个思路应该很相似。就让我们试试吧。
思路 这里的难点在于实现"解释器",比如将"前缀_<日期:yyyy_MM_dd>"解释为“工号生成器”,而且“解释器”的“规则”允许动态增加。
实现 代码下载
类图
核心代码
CodeRuleGenerator.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 using System.Text.RegularExpressions;
8
9 namespace EntityCodeRuleDemo
10 {
11 public sealed class CodeRuleGenerator : ICodeRuleGenerator
12 {
13 private readonly IEnumerable<ICodeRuleProvider> _providers = new List<ICodeRuleProvider> ();
14
15 internal CodeRuleGenerator(IEnumerable<ICodeRuleProvider> providers)
16 {
17 _providers = providers;
18 }
19
20 public string Generate( object entity)
21 {
22 var sb = new StringBuilder();
23
24 foreach ( var provider in _providers)
25 {
26 sb.Append(provider.Generate(entity));
27 }
28
29 return sb.ToString();
30 }
31 }
32 }
CodeRuleInterpreter.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 using System.Text.RegularExpressions;
8
9 using EntityCodeRuleDemo.RuleProviders;
10
11 namespace EntityCodeRuleDemo
12 {
13 public static class CodeRuleInterpreter
14 {
15 private static Dictionary<Regex, Func< string , ICodeRuleProvider>> _providerFactorys = new Dictionary<Regex, Func< string , ICodeRuleProvider>> ();
16
17 static CodeRuleInterpreter()
18 {
19 SetProviderFactory( new Regex( " ^[^<].*?[^>]?$ " ), LiteralRuleProvider.LiteralRuleProviderFactory);
20 SetProviderFactory( new Regex( " ^<日期(:(?<格式>.*?))?>$ " ), DateRuleProvider.DateRuleProviderFactory);
21 SetProviderFactory( new Regex( " ^<属性(:(?<名称>.*?))?>$ " ), PropertyRuleProvider.PropertyRuleProviderFactory);
22 }
23
24 public static void SetProviderFactory(Regex regex, Func< string , ICodeRuleProvider> providerFactory)
25 {
26 _providerFactorys[regex] = providerFactory;
27 }
28
29
30 public static ICodeRuleGenerator Interpret( string codeRule)
31 {
32 var providers = GetProviders(codeRule);
33
34 return new CodeRuleGenerator(providers);
35 }
36
37 private static IEnumerable<ICodeRuleProvider> GetProviders( string codeRule)
38 {
39 var literals = codeRule.Replace( " < " , " $< " ).Replace( " > " , " >$ " ).Split( ' $ ' );
40
41 return literals
42 .Where(x => ! string .IsNullOrEmpty(x))
43 .Select(GetProvider)
44 .ToList();
45 }
46
47 private static ICodeRuleProvider GetProvider( string literal)
48 {
49 var providerFactory = _providerFactorys
50 .FirstOrDefault(x => x.Key.IsMatch(literal))
51 .Value;
52
53 if (providerFactory == null )
54 {
55 throw new FormatException( " 格式化错误 " );
56 }
57
58 return providerFactory(literal);
59 }
60 }
61 }
Program.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 namespace EntityCodeRuleDemo
8 {
9 class Program
10 {
11 static void Main( string [] args)
12 {
13 var employeeCode = CodeRuleInterpreter
14 .Interpret( " 前缀_<日期:yyyy_MM_dd>_<属性:NamePinYin> " )
15 .Generate( new Employee { NamePinYin = " DUANGW " });
16
17 Console.WriteLine(employeeCode);
18 }
19 }
20
21 class Employee
22 {
23 public string NamePinYin { get ; set ; }
24 public string EmployeeCode { get ; set ; }
25 }
26 }
运行效果
备注
按照这种思路,基本上能满足企业应用的多数编码规则要求。在真实的项目中,这些规则是要持久化到数据库的,这样就可以做到运行时动态的修改规则了。
可扩展的单据编号生成器 之 顺序号(防止重复)
背景
我在上篇文章“ .NET:可扩展的单据编号生成器 + 简单的解释器 ”中介绍了一个简单的单据编号框架。有朋友留言问如何实现“ 顺序号,且不能重复 ”,本篇文章就针对这个问题用上篇介绍的框架进行实现。
思路 顺序号 = 上次顺序号 + 步长
根据上面的公式,问题可以化解为:如何获取上次顺序号?获取上次顺序号有两种方式:
扫描单据表,找出最新的一条记录。 引入种子表,种子表记录了最新的顺序号。因为生成的顺序号不能重复,这里就有了并发的要求,为了最大限度的提高并发性,我选择2( 引入种子表 )。
并发处理可以选择:悲观锁或乐观锁,这里为了简单,我选择 悲观锁 。
实现 代码下载: http://yunpan.cn/Q5KMUTA3qGPct 。
种子表设计
1 CREATE TABLE [ dbo ] . [ CodeSeeds ] ( 2 [ Id ] UNIQUEIDENTIFIER NOT NULL , 3 [ Key ] NVARCHAR ( 500 ) NOT NULL , 4 [ Value ] INT NOT NULL , 5 PRIMARY KEY CLUSTERED ( [ Id ] ASC ) 6 );
SeedCodeRuleProvider.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 using System.Transactions;
8 using System.Text.RegularExpressions;
9 using System.Data.Entity.Infrastructure;
10
11 namespace EntityCodeRuleDemo
12 {
13 public class SeedCodeRuleProvider : ICodeRuleProvider
14 {
15 private readonly int _width;
16
17 public SeedCodeRuleProvider( int width)
18 {
19 _width = width;
20 }
21
22 public string Generate( object entity)
23 {
24 return GetSeedValue(entity).ToString().PadLeft(_width, ' 0 ' );
25 }
26
27 protected virtual string GetKey( object entity)
28 {
29 return entity.GetType().FullName;
30 }
31
32 private int GetSeedValue( object entity)
33 {
34 try
35 {
36 var value = 0 ;
37 var key = this .GetKey(entity);
38
39 using ( var ts = new TransactionScope(TransactionScopeOption.RequiresNew,
40 new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
41 {
42 using ( var context = new TestContext())
43 {
44 var seed = context.CodeSeeds.Where(x => x.Key == key).FirstOrDefault();
45 if (seed == null )
46 {
47 seed = new CodeSeed { Id = Guid.NewGuid(), Key = key, Value = - 1 };
48 context.CodeSeeds.Add(seed);
49 }
50
51 seed.Value++ ;
52 value = seed.Value;
53 context.SaveChanges();
54 }
55
56 ts.Complete();
57 }
58
59 return value;
60 }
61 catch (DbUpdateException)
62 {
63 return this .GetSeedValue(entity);
64 }
65 }
66
67 public static SeedCodeRuleProvider SeedCodeRuleProviderFactory( string literal)
68 {
69 var match = new Regex( " ^<种子(:(?<宽度>.*?))?>$ " ).Match(literal);
70
71 var width = match.Groups[ " 宽度 " ].Value;
72
73 return new SeedCodeRuleProvider( string .IsNullOrEmpty(width) ? 5 : int .Parse(width));
74 }
75 }
76 }
Program.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Threading.Tasks;
6
7 using System.Text.RegularExpressions;
8
9 namespace EntityCodeRuleDemo
10 {
11 class Program
12 {
13 static void Main( string [] args)
14 {
15 CodeRuleInterpreter.RegistProviderFactory( new Regex( " ^<种子(:(?<宽度>.*?))?>$ " ), SeedCodeRuleProvider.SeedCodeRuleProviderFactory);
16
17 var employeeCode = CodeRuleInterpreter
18 .Interpret( " 前缀_<日期:yyyy_MM_dd>_<属性:NamePinYin>_<种子:6> " )
19 .Generate( new Employee { NamePinYin = " DUANGW " });
20
21 Console.WriteLine(employeeCode);
22 }
23 }
24 }
运行结果
备注
有写业务要求会强制编号的连续性或编号的随机性,对于这两种需求,还需要单独开发Provider,有机会再写文章介绍了。
现在可选的框架现在我们开发一个.net应用,面临的选择比较多。我们可以选择entity framework, enterprise library, nhibernate, 还有一个mybatis.net, 即java世界mybatis/ibatis的.net版。 IOC的框架可以选择Unity, Ninject,Spring.net(java的spring对应的.net版本)。Entity framework可以使用linq查询,有好几种开发模式,如code first, db first, 可以不用写sql。Entity framework适合sql server。虽有mysql提供了entity framework的provider,但是不是很好, 经常不得不单独写sql来操作mysql. Enterprise library是个不错的选择,有log, exception, policy injection的一些东西,操作数据库也比较好。nhibernate是java版hibernate的.net对应版本。对数据库包装得比较多,可以不用写sql. 但是有些时候对数据库操作不够优化,会有一些多余数据库操作。带来一些性能的影响。mybatis.net是个介于ado.net与nhibernate之间的框架,它负责数据库对象与内存对象的映射。程序员必须写sql语句来操作数据库。IOC的框架中Unity, Ninject, Spring.net, 都是不错的框架。有时这些框架能照顾到我们大部分的需求,也有一些情况,不能全部照顾到我们的需求。这时就得用一些老办法。比如直接就用ado.net
适应多种数据库的db helper代码这里主要是想说一些基于ado.net来开发asp.net mvc应用的一些思路。为什么选用ado.net呢,ado.net性能可以达到最好,灵活。先看一段db helper的代码:
using System;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Collections.Generic;
namespace DataAccessCommon
{
/// <summary>
/// The MyDBHelper class is intended to encapsulate high performance, scalable best practices for
/// common uses of SqlClient, OracleClient, OleDb, and others
/// </summary>
public static class MyStaticDBHelper
{
public struct MyDBParameter
{
public string strParameterName;
public DbType dbType;
public object value;
public ParameterDirection parameterDirection;
public MyDBParameter( string parameterName, DbType type, object theValue, ParameterDirection direction = ParameterDirection.Input)
{
strParameterName = parameterName;
dbType = type;
value = theValue;
parameterDirection = direction;
}
}
public static string DatabaseType = " SqlServer " ;
private static Dictionary< string , string > providers = new Dictionary< string , string > () {
{ " SqlServer " , " System.Data.SqlClient " }
, { " Oracle " , " System.Data.OracleClient " }
, { " OleDb " , " System.Data.OleDb " }
};
private static DbProviderFactory dataFactory = DbProviderFactories.GetFactory(providers[DatabaseType]);
public static string CONNECTION_STRING = ConfigurationManager.AppSettings[ " ConnectionString " ];
#region private methods
private static void AttachParameters(DbCommand command, DbParameter[] parameters)
{
if (parameters != null )
{
command.Parameters.AddRange(parameters);
}
}
private static DbCommand CreateCommand( object conn)
{
DbCommand command = null ;
// If it is just a connection(not a transaction)
if (conn is DbConnection)
{
command = ((DbConnection)conn).CreateCommand();
if (command.Connection.State != ConnectionState.Open)
{
command.Connection.Open();
}
}
else // It is a transaction, then join the transaction
{
command = ((DbTransaction)conn).Connection.CreateCommand();
command.Transaction = (DbTransaction)conn;
}
return command;
}
private static DbCommand SetupCommand( object conn, CommandType commandType, string strSQLOrSPName, List<MyDBParameter> myDBParameters)
{
DbParameter[] parameters = myDBParameters != null ? CreateDBParameters(myDBParameters).ToArray() : null ;
DbCommand command = CreateCommand(conn);
command.CommandText = strSQLOrSPName;
command.CommandType = commandType;
AttachParameters(command, parameters);
return command;
}
private static DbParameter CreateDBParameter( string strParameterName, DbType dbType, object value, ParameterDirection direction)
{
DbParameter parameter = dataFactory.CreateParameter();
parameter.ParameterName = strParameterName;
parameter.DbType = dbType;
parameter.Value = value;
parameter.Direction = direction;
return parameter;
}
private static List<DbParameter> CreateDBParameters(List<MyDBParameter> myDBParameters)
{
List <DbParameter> parameters = new List<DbParameter> ();
foreach (MyDBParameter myDBParameter in myDBParameters)
{
parameters.Add(CreateDBParameter(myDBParameter.strParameterName, myDBParameter.dbType, myDBParameter.value, myDBParameter.parameterDirection));
}
return parameters;
}
#endregion
public static DbConnection GetConnection()
{
DbConnection connection = dataFactory.CreateConnection();
connection.ConnectionString = CONNECTION_STRING;
return connection;
}
public static int ExecuteNonQuery( object conn, CommandType commandType, string strSQLOrSPName, List<MyDBParameter> myDBParameters = null )
{
DbCommand command = SetupCommand(conn, commandType, strSQLOrSPName, myDBParameters);
return command.ExecuteNonQuery();
}
public static DataSet ExecuteDataset( object conn, CommandType commandType, string strSQLOrSPName, List<MyDBParameter> myDBParameters = null )
{
DbCommand command = SetupCommand(conn, commandType, strSQLOrSPName, myDBParameters);
DbDataAdapter dataAdaptor = dataFactory.CreateDataAdapter();
DataSet ds = new DataSet();
dataAdaptor.SelectCommand = command;
dataAdaptor.Fill(ds);
return ds;
}
public static DbDataReader ExecuteReader( object conn, CommandType commandType, string strSQLOrSPName, List<MyDBParameter> myDBParameters = null )
{
DbCommand command = SetupCommand(conn, commandType, strSQLOrSPName, myDBParameters);
return command.ExecuteReader();
}
public static object ExecuteScalar( object conn, CommandType commandType, string strSQLOrSPName, List<MyDBParameter> myDBParameters = null )
{
DbCommand command = SetupCommand(conn, commandType, strSQLOrSPName, myDBParameters);
return command.ExecuteScalar();
}
}
}
此代码能支持访问Oracle, sql server, OleDB。用的都是DbConnection之类的。只要开始选择了正确的provider, DbProviderFactories就给创建相应的connection, command等类,就可以顺利地处理这个对应的数据库了。sql的参数是DbType。用来适应数据库类型。MyDBParameter结构封装了参数名,类型,参数值,传入传出方向。目前的版本只考虑了一个数据库连接。连接串只有一个。DbProviderFactory只有一个实例。没有考虑到动态切换连接的情况。如果是要多个连接,得要多个DbProviderFactory的实例。CreateCommand方法里判断了传入的的数据库连接是一个DbConnection还是一个DbTransaction,如果是一个DbTransaction的话,可以加入这个数据库事务。如果只是一个DbConnection则不加入已有的数据库事务,使用自动的数据库事务。
数据实体类
using System;
using System.Collections.Generic;
namespace DataEntity
{
public class UserMenuItem
{
#region Properties
public int MenuItemID { get ; set ; }
public string MenuItemName { get ; set ; }
public int MenuID { get ; set ; }
public int Ordinal { get ; set ; }
public int Indent { get ; set ; }
#endregion
}
}
纯数据的类。这里使用了比较老的c#语法。也可以加上DataAnnotation的标签。可以实现验证数据,也可以加上Display标签,引用资源文件。这个数据实体类在MVC页面里绑定时可以显示想应的label。label的内容来自于资源文件,便于使用多语言的界面。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Resource.Entity;
namespace DataEntity
{
public class UserAccount
{
#region Properties
public int ID { get ; set ; }
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName= " Common_Required_ErrorMessage " )]
[StringLength( 30 , ErrorMessageResourceType= typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " NAME_StringLength_ErrorMessage " )]
[RegularExpression( @" [a-zA-Z].* " , ErrorMessageResourceType= typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " NAME_RegularExpression_ErrorMessage " )]
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name= " NAME_DisplayName " )]
public string Name { get ; set ; }
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " Common_Required_ErrorMessage " )]
[RegularExpression( @" [a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])? "
, ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName= " EMAIL_RegularExpression_ErrorMessage " )]
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name= " EMAIL_DisplayName " )]
public string Email { get ; set ; }
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name = " PASSWORD_DisplayName " )]
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " Common_Required_ErrorMessage " )]
[StringLength( 32 , ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " PASSWORD_StringLength " , MinimumLength = 8 )]
public string Password { get ; set ; }
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name = " Balance " )]
public decimal Balance { get ; set ; }
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " Common_Required_ErrorMessage " )]
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name = " CONFIRMPASSWORD_DisplayName " )]
[Compare( " Password " , ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " CONFIRMPASSWORD_CompareErrorMessage " )]
public string ConfirmPassword { get ; set ; }
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " Common_Required_ErrorMessage " )]
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name = " OLDNAME_DisplayName " )]
public string OldName { get ; set ; }
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " Common_Required_ErrorMessage " )]
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name = " OLDEMAIL_DisplayName " )]
public string OldEmail { get ; set ; }
[Required(ErrorMessageResourceType = typeof (Resource.Entity.UserAccount), ErrorMessageResourceName = " Common_Required_ErrorMessage " )]
[Display(ResourceType = typeof (Resource.Entity.UserAccount), Name = " OLDPassword_DisplayName " )]
public string OldPassword { get ; set ; }
#endregion
}
}
下面是数据访问的代码:
using System;
using System.Collections.Generic;
using System.Data;
using DataEntity;
using DataAccessCommon;
namespace DataAccess
{
public class DALUserMenuItem
{
#region data access methods
public int DeleteUserMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " DELETE FROM [UserMenuItem] WHERE [MenuItemID] = @MenuItemID " ;
int result = 0 ;
result = MyStaticDBHelper.ExecuteNonQuery(conn, System.Data.CommandType.Text, strSQL, paras);
return result;
}
public int UpdateUserMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemName " , DbType.String, usermenuitem.MENUITEMNAME),
new MyStaticDBHelper.MyDBParameter( " @MenuID " , DbType.Int32, usermenuitem.MENUID),
new MyStaticDBHelper.MyDBParameter( " @Ordinal " , DbType.Int32, usermenuitem.ORDINAL),
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " UPDATE [UserMenuItem] SET [MenuItemName] = @MenuItemName, [MenuID] = @MenuID, [Ordinal] = @Ordinal WHERE [MenuItemID] = @MenuItemID " ;
int result = 0 ;
result = MyStaticDBHelper.ExecuteNonQuery(conn, System.Data.CommandType.Text, strSQL, paras);
return result;
}
public int AddUserMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemName " , DbType.String, usermenuitem.MENUITEMNAME),
new MyStaticDBHelper.MyDBParameter( " @MenuID " , DbType.Int32, usermenuitem.MENUID),
new MyStaticDBHelper.MyDBParameter( " @Ordinal " , DbType.Int32, usermenuitem.ORDINAL),
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " INSERT INTO [UserMenuItem] ( [MenuItemName] , [MenuID] , [Ordinal] ) VALUES( @MenuItemName, @MenuID, @Ordinal ); SELECT SCOPE_IDENTITY() as [MenuItemID] " ;
int result = 0 ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras);
if (ds.Tables.Count > 0 && ds.Tables[ 0 ].Rows.Count > 0 ){
usermenuitem.MENUITEMID = Convert.ToInt32(ds.Tables[ 0 ].Rows[ 0 ][ 0 ]);
result = 1 ;
}
return result;
}
public List<UserMenuItem> GetAllUserMenuItem(Object conn)
{
string strSQL = " SELECT * FROM [UserMenuItem] " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL);
return DataMapper.MapDataTableToObjectList<UserMenuItem>(ds.Tables[ 0 ]);
}
public UserMenuItem FindAUserMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " SELECT * FROM [UserMenuItem] WHERE [MenuItemID] = @MenuItemID " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras);
return DataMapper.MapDataTableToSingleRow<UserMenuItem>(ds.Tables[ 0 ]);
}
public System.Int32 SelectCountUserMenuItem(Object conn)
{
string strSQL = " SELECT COUNT(1) AS Count FROM [UserMenuItem] " ;
Object obj = null ;
obj = MyStaticDBHelper.ExecuteScalar(conn, System.Data.CommandType.Text, strSQL);
return (System.Int32)obj;
}
public System.Int32 SelectCountWhereClauseUserMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " SELECT COUNT(1) AS Count FROM [UserMenuItem] WHERE [MenuItemID] = @MenuItemID " ;
Object obj = null ;
obj = MyStaticDBHelper.ExecuteScalar(conn, System.Data.CommandType.Text, strSQL, paras);
return (System.Int32)obj;
}
public List<UserMenuItem> SelectTopUserMenuItem(Object conn)
{
string strSQL = " SELECT Top 50 * FROM [UserMenuItem] " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL);
return DataMapper.MapDataTableToObjectList<UserMenuItem>(ds.Tables[ 0 ]);
}
public List<UserMenuItem> SelectOrderByPrimaryKeyUserMenuItem(Object conn)
{
string strSQL = " SELECT * FROM [UserMenuItem] ORDER BY [MenuItemID] , [MenuItemName] , [MenuID] , [Ordinal] " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL);
return DataMapper.MapDataTableToObjectList<UserMenuItem>(ds.Tables[ 0 ]);
}
public List<UserMenuItem> SelectGroupByPrimaryKeyUserMenuItem(Object conn)
{
string strSQL = " SELECT * FROM [UserMenuItem] GROUP BY [MenuItemID] , [MenuItemName] , [MenuID] , [Ordinal] " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL);
return DataMapper.MapDataTableToObjectList<UserMenuItem>(ds.Tables[ 0 ]);
}
public List<UserMenuItem> SelectUserMenuItemsByMenuID(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuID " , DbType.Int32, usermenuitem.MENUID)
};
string strSQL = " SELECT * FROM [UserMenuItem] WHERE [MenuID] = @MenuID ORDER BY [Ordinal] " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras);
return DataMapper.MapDataTableToObjectList<UserMenuItem>(ds.Tables[ 0 ]);
}
public UserMenuItem SelectPreviousMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuID " , DbType.Int32, usermenuitem.MENUID),
new MyStaticDBHelper.MyDBParameter( " @Ordinal " , DbType.Int32, usermenuitem.ORDINAL)
};
string strSQL = " SELECT TOP 1 * FROM [UserMenuItem] WHERE [MenuID] = @MenuID AND [Ordinal] < @Ordinal ORDER BY [Ordinal] DESC " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras);
return DataMapper.MapDataTableToSingleRow<UserMenuItem>(ds.Tables[ 0 ]);
}
public UserMenuItem SelectNextMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuID " , DbType.Int32, usermenuitem.MENUID),
new MyStaticDBHelper.MyDBParameter( " @Ordinal " , DbType.Int32, usermenuitem.ORDINAL)
};
string strSQL = " SELECT TOP 1 * FROM [UserMenuItem] WHERE [MenuID] = @MenuID AND [Ordinal] > @Ordinal ORDER BY [Ordinal] ASC " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras);
return DataMapper.MapDataTableToSingleRow<UserMenuItem>(ds.Tables[ 0 ]);
}
public int MoveLeftMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " UPDATE [UserMenuItem] SET [Indent] = CASE WHEN [Indent] - 1 >= 0 THEN [Indent] - 1 ELSE 0 END WHERE [MenuItemID] = @MenuItemID " ;
int iResult = 0 ;
iResult = MyStaticDBHelper.ExecuteNonQuery(conn, System.Data.CommandType.Text, strSQL, paras);
return iResult;
}
public int MoveRightMenuItem(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuItemID " , DbType.Int32, usermenuitem.MENUITEMID)
};
string strSQL = " UPDATE [UserMenuItem] SET [Indent] = CASE WHEN [Indent] + 1 <= 2 THEN [Indent] + 1 ELSE 2 END WHERE [MenuItemID] = @MenuItemID " ;
int iResult = 0 ;
iResult = MyStaticDBHelper.ExecuteNonQuery(conn, System.Data.CommandType.Text, strSQL, paras);
return iResult;
}
public UserMenuItem SelectMaxOrdinal(Object conn, UserMenuItem usermenuitem)
{
List <MyStaticDBHelper.MyDBParameter> paras = new List<MyStaticDBHelper.MyDBParameter> {
new MyStaticDBHelper.MyDBParameter( " @MenuID " , DbType.Int32, usermenuitem.MENUID)
};
string strSQL = " SELECT IsNull(Max(Ordinal),0) as Ordinal FROM [UserMenuItem] WHERE [MenuID] = @MenuID " ;
DataSet ds = null ;
ds = MyStaticDBHelper.ExecuteDataset(conn, System.Data.CommandType.Text, strSQL, paras);
return DataMapper.MapDataTableToSingleRow<UserMenuItem>(ds.Tables[ 0 ]);
}
#endregion
}
}
这个数据访问的代码都是这种方式,开始准备参数。用的都是MyStaticDBHelper.MyDBParameter结构。给出参数名,参数类型,参数值和参数方向(输入还是输出)。然后就是一个sql语句,其中有参数。之后是根据查询的类型,update/delete/insert的就调用db helper的ExecuteNonQuery方法,如果是select一个表的话,调用db helper的ExecuteDataset方法。再之后,就要将返回的值给转换成对应的返回类型。如一个实体对象或者实体对象列表。这个类里的sql语句都是预先设计好的sql语句,每个sql语句都有参数,然后每个sql查询都有一个c#方法与之对应。DataMapper.MapDataTableToSingleRow是将DataTable中第一行转换成一个实体对象。DataMapper.MapDataTableToObjectList是将返回的DataTable转换成实体类的列表, 即List<实体类>,这里DataMapper类采用了Reflection的方式来进行转换实体类。虽然不是最快的。在某些情况下也可以接受。我们做过一个实例程序来对比,将DataTable转成实体类列表,有直接赋值,Emit, Reflection, delegate, Expression tree等不同的方法,经过性能测试,直接赋值是最快的。Emit稍微比直接赋值慢,但是已经很快了。直接赋值写代码比较繁琐。Emit的方法代码稍微有点复杂,但是运行效率不错。又比较灵活。是个相当好的方法,Emit和Expresssion Tree的方法有一些缺点,就是很难调试,万一出现问题会很难找到问题的根源。这里是比较不同方法将DataTable转成数据实体类的测试代码: https://files.cnblogs测试数据/mikelij/testGenerateEntity.zip , 大家可以下载了去试试,应该说这几种方法都还不错。这里的代码选用了Reflection方法。因为Reflection也没有慢得很多。Reflection方法的兼容性好。不会出问题。Emit和Express tree方法经常会遇到不兼容类型的问题。而且很难排查问题。
数据映射的类
using System;
using System.Data;
using System.Configuration;
using System.Collections.Generic;
using System.Reflection;
namespace DataAccessCommon
{
public class DataMapper
{
public static List<TType> MapDataTableToObjectList<TType>(DataTable dt) where TType : new ()
{
List <TType> result = new List<TType> ();
foreach (DataRow currentRow in dt.Rows)
{
TType ttype = new TType();
for ( int i = 0 ; i < dt.Columns.Count; i++ )
{
for ( int j = 0 ; j < ttype.GetType().GetProperties().Length; j++ )
{
if (dt.Columns[i].ColumnName.ToUpper() == ttype.GetType().GetProperties()[j].Name.ToUpper())
{
ttype.GetType().GetProperties()[j].SetValue(ttype, currentRow[i], null );
break ;
}
}
}
result.Add(ttype);
ttype = new TType();
}
return result;
}
public static TType MapDataTableToSingleRow<TType>(DataTable dt) where TType : new ()
{
TType ttype = new TType();
if (dt.Rows.Count > 0 )
{
DataRow currentRow = dt.Rows[ 0 ];
for ( int i = 0 ; i < dt.Columns.Count; i++ )
{
for ( int j = 0 ; j < ttype.GetType().GetProperties().Length; j++ )
{
if (dt.Columns[i].ColumnName.ToUpper() == ttype.GetType().GetProperties()[j].Name.ToUpper())
{
ttype.GetType().GetProperties()[j].SetValue(ttype, currentRow[i], null );
break ;
}
}
}
}
return ttype;
}
}
}
商业类的代码
商业类的代码是基于我们OOA/OOD设计出的。比如一个银行ATM的例子,其业务里有若干名词,比如银行户头,ATM机等名词,每个名词下又有若干属性,比如银行帐号,帐号所有者名字,开立日期等,ATM机有ATM机号,地理位置,所属银行编号,等等。围绕着这些名词,有相关的一些动作。比如取钱,存钱,插卡入ATM机,记录ATM流水。等等等等。这里已经将名词的数据属性放到了数据实体类里。这些数据实体类里就只有那些名词的数据属性,没有那些动作,即一个纯数据的类。这里要提到的商业类包含了商业方法的类,这些商业方法就对应着那些动作。比如取钱,就有一个Withdraw方法对应。存钱就就一个Deposite方法对应。这两个方法都放在一个叫BankAccount的的商业类里面。这里用的银行的例子,说明这里所用到的设计方法。
使用Unity之类的IOC容器进行policy injection
下面就要说说IOC了,就是说我们设计一个商业类,里面有几个商业方法。如果让调用者直接依赖于这个商业类,那么将来有一天要改变这些商业方法时,可能就不得不同时改调用者和商业类。为了避免这种情况,我们可以从商业类提取出一个接口。这个接口只有这些动作的名字,没有具体具体实现。然后由负责具体实现的商业类来实现这些接口。说了这些东西与IOC有什么关系呢?这样做正是为了实现IOC打下基础。要知道象Unity这样的IOC容器,都是负责创建对象。它负责从接口映射到具体的商业类。当调用者需要创建一个接口的实例,接口本身是不能实例化的,容器会为调用者创建一个实现了该接口商业类的实例。
一个商业接口的例子:
using System;
namespace BusinessLogic
{
[MyDBHandler]
public interface IBLLUserMenu
{
int AddUserMenu(DataEntity.UserMenu usermenu);
int DeleteUserMenu(DataEntity.UserMenu usermenu);
DataEntity.UserMenu FindAUserMenu(DataEntity.UserMenu usermenu);
System.Collections.Generic.List <DataEntity.UserMenu> GetAllUserMenu();
int SelectCountUserMenu();
int SelectCountWhereClauseUserMenu(DataEntity.UserMenu usermenu);
System.Collections.Generic.List <DataEntity.UserMenu> SelectGroupByPrimaryKeyUserMenu();
System.Collections.Generic.List <DataEntity.UserMenu> SelectMenusByApplicationID(DataEntity.UserMenu usermenu);
System.Collections.Generic.List <DataEntity.UserMenu> SelectOrderByPrimaryKeyUserMenu();
System.Collections.Generic.List <DataEntity.UserMenu> SelectTopUserMenu();
int UpdateUserMenu(DataEntity.UserMenu usermenu);
object CONNECTION { get ; set ; }
DataEntity.UserMenu USERMENU { get ; set ; }
System.Collections.Generic.List <DataEntity.UserMenu> USERMENU_LIST { get ; set ; }
}
}
此代码中的MyDBHandler是一个字定义的attribute, 用于Unity来进行拦截判断。有这个attribute就拦截,没有就不拦截。
而相应的商业类就是这样的:
using System;
using System.Collections.Generic;
using System.Data;
using DataEntity;
using DataAccess;
using DataAccessCommon;
using CommonUtil;
namespace BusinessLogic
{
internal class BLLUserMenu : BusinessLogic.IBLLUserMenu
{
private readonly DataAccess.DALUserMenu dal = new DataAccess.DALUserMenu();
private object conn = null ;
private UserMenu usermenu;
private List<UserMenu> usermenus;
public object CONNECTION
{
get
{
return conn;
}
set
{
conn = value;
}
}
public UserMenu USERMENU
{
get
{
return usermenu;
}
set
{
usermenu = value;
}
}
public List<UserMenu> USERMENU_LIST
{
get
{
return usermenus;
}
set
{
usermenus = value;
}
}
#region business logic method
public int DeleteUserMenu(UserMenu usermenu)
{
return dal.DeleteUserMenu(conn,usermenu);
}
public int UpdateUserMenu(UserMenu usermenu)
{
return dal.UpdateUserMenu(conn,usermenu);
}
public int AddUserMenu(UserMenu usermenu)
{
return dal.AddUserMenu(conn,usermenu);
}
public List<UserMenu> GetAllUserMenu()
{
return dal.GetAllUserMenu(conn);
}
public UserMenu FindAUserMenu(UserMenu usermenu)
{
return dal.FindAUserMenu(conn,usermenu);
}
public System.Int32 SelectCountUserMenu()
{
return dal.SelectCountUserMenu(conn);
}
public System.Int32 SelectCountWhereClauseUserMenu(UserMenu usermenu)
{
return dal.SelectCountWhereClauseUserMenu(conn,usermenu);
}
public List<UserMenu> SelectTopUserMenu()
{
return dal.SelectTopUserMenu(conn);
}
public List<UserMenu> SelectOrderByPrimaryKeyUserMenu()
{
return dal.SelectOrderByPrimaryKeyUserMenu(conn);
}
public List<UserMenu> SelectGroupByPrimaryKeyUserMenu()
{
return dal.SelectGroupByPrimaryKeyUserMenu(conn);
}
public List<UserMenu> SelectMenusByApplicationID(UserMenu usermenu)
{
return dal.SelectMenusByApplicationID(conn, usermenu);
}
#endregion
}
}
目前这个商业类的方法都比较简单,如果有比较复杂的,可能一个商业方法里需要调用数据访问的方法好多个,在做一些逻辑判断。那么这些商业方法就可以变得复杂多了。如这样的一个商业方法:
public bool MoveUpItem(UserMenuItem usermenuitem)
{
usermenuitem = dal.FindAUserMenuItem(conn, usermenuitem);
UserMenuItem previousMenuItem = dal.SelectPreviousMenuItem(conn, usermenuitem);
int iTempOrdinal = usermenuitem.Ordinal;
usermenuitem.Ordinal = previousMenuItem.Ordinal;
previousMenuItem.Ordinal = iTempOrdinal;
dal.UpdateUserMenuItem(conn, usermenuitem);
dal.UpdateUserMenuItem(conn, previousMenuItem);
return true ;
}
Unity配置信息:
< unity xmlns ="http://schemas.microsoft测试数据/practices/2010/unity" >
< sectionExtension type ="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Microsoft.Practices.Unity.Interception.Configuration" />
< namespace name ="BusinessLogic" />
< container name ="myContainer" >
< extension type ="Interception" />
< register type ="BusinessLogic.IBLLApplication, BusinessLogic" mapTo ="BusinessLogic.BLLApplication, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLDomain, BusinessLogic" mapTo ="BusinessLogic.BLLDomain, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLFormElement, BusinessLogic" mapTo ="BusinessLogic.BLLFormElement, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserAccount, BusinessLogic" mapTo ="BusinessLogic.BLLUserAccount, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserColumns, BusinessLogic" mapTo ="BusinessLogic.BLLUserColumns, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserForm, BusinessLogic" mapTo ="BusinessLogic.BLLUserForm, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserMenu, BusinessLogic" mapTo ="BusinessLogic.BLLUserMenu, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserMenuItem, BusinessLogic" mapTo ="BusinessLogic.BLLUserMenuItem, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserSession, BusinessLogic" mapTo ="BusinessLogic.BLLUserSession, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< register type ="BusinessLogic.IBLLUserTables, BusinessLogic" mapTo ="BusinessLogic.BLLUserTables, BusinessLogic" >
< interceptor name ="myinterceptor" type ="InterfaceInterceptor" isDefaultForType ="true" />
< policyInjection />
</ register >
< interception >
< policy name ="mypolicy" >
< callHandler name ="myHandler1" type ="BusinessLogic.MyDBHandler, BusinessLogic" ></ callHandler >
< matchingRule name ="myrule" type ="CustomAttributeMatchingRule" >
< constructor >
< param name ="attributeType" type ="System.Type, mscorlib" >
< value value ="BusinessLogic.MyDBHandlerAttribute, BusinessLogic" typeConverter ="BusinessLogic.GetTypeConverter, BusinessLogic" />
</ param >
< param name ="inherited" type ="bool" >
< value value ="true" />
</ param >
</ constructor >
</ matchingRule >
</ policy >
</ interception >
</ container >
</ unity >
注意到这些register的节点没有?这些节点实现了接口到具体商业类的映射。接口表示的是一个抽象。它只有方法的声明,没有具体实现。在调用者需要一个具体的实现了这个接口的商业类时,容器帮助我们创建这个商业类的实例,而接口到商业类的映射就是在Unity配置文件里做的。
Unity除了帮助我们实现接口到商业类的映射,还可以帮助我们实现aop. 比如log, db transaction, exception handling.
using System;
using System.Data;
using System.Data.Common;
using System.Collections.Generic;
using Microsoft.Practices.Unity.InterceptionExtension;
using DataAccessCommon;
using CommonUtil;
namespace BusinessLogic
{
public class MyDBHandler : ICallHandler
{
private int iOrder = 0 ;
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var retvalue = getNext()(input, getNext); // call the intercepting method
if (retvalue.Exception != null )
{
SysLog.GetInstance().LogError(retvalue.Exception);
}
return retvalue;
}
public int Order
{
get
{
return iOrder;
}
set
{
iOrder = value;
}
}
}
}
这个MyDBHandler已经在之前的Unity配置中指定了。即这句:
< callHandler name ="myHandler1" type ="BusinessLogic.MyDBHandler, BusinessLogic" ></ callHandler >
这句是去调用被拦截的方法:
retvalue = getNext()(input, getNext);
被拦截方法(即我们的商业方法)返回以后,程序就检查retvalue.Exception有没有出错,有就调用logging的类来写log。将出错信息完整地打印出来。
自定义的attribute类:
using System;
using System.Collections.Generic;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;
namespace BusinessLogic
{
public class MyDBHandlerAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new MyDBHandler();
}
}
}
至于db transaction, 如果数据库事务比较简单,可以用TransactionScope,前面的MyDBHandler的invoke方法就替换成这样。
using (TransactionScope ts = new TransactionScope())
{
var retvalue = getNext().Invoke(input, getNext);
if (retvalue.Exception != null )
{
SysLog.GetInstance().LogError(retvalue.Exception);
}
else
{
ts.Complete();
}
return retvalue
}
Unity配置里用到的一个工具类代码:
using System;
using System.Collections.Generic;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;
namespace BusinessLogic
{
public class GetTypeConverter : System.ComponentModel.TypeConverter
{
public override object ConvertFrom(System.ComponentModel.ITypeDescriptorContext context,
System.Globalization.CultureInfo culture,
object value)
{
return Type.GetType(value.ToString());
}
}
}
这个类用来做类型转换的。用来帮助Unity来找自定义的MyDBHandlerAttribute的。
商业类的调用者为了调用商业类,我们有一个类来帮助创建相应的商业类的实例:
using System;
using System.Collections.Generic;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
namespace BusinessLogic
{
public static class BusinessClassCreator
{
public static IUnityContainer container = new UnityContainer().LoadConfiguration( " myContainer " );
public static T GetInstance<T> ()
{
return (T)container.Resolve( typeof (T), null );
}
public static object GetInstance(Type type)
{
return container.Resolve(type, null );
}
}
}
在调用的地方代码这么写:
[HttpGet]
public ActionResult SaveMenuScheme()
{
UserMenu userMenu = new UserMenu();
userMenu.MenuID = GetMenuID( this );
userMenu = BusinessClassCreator.GetInstance<IBLLUserMenu> ().FindAUserMenu(userMenu);
short bMenuScheme = 0 ;
bMenuScheme = ( short )DesignTableController.GetID( this );
userMenu.Scheme = bMenuScheme;
BusinessClassCreator.GetInstance <IBLLUserMenu> ().UpdateUserMenu(userMenu);
return DisplayMenuList();
}
这是一个在asp.net mvc中调用上述商业类的样例代码,首先通过BusinessClassCreator.GetInstance<IBLLUserMenu>()得到接口IBLLUserMenu对应的商业类对象,然后再调用IBLLUserMenu接口上的方法。比如此例中调用了FindUserMenu方法和UpdateUserMenu方法。每个方法的返回类型已经由接口定义好。我们只要按照这个接口的定义来使用这个接口就可以了。接口在这里的好处就是它定义了商业类的规范,所有实现此接口的商业类都符合此接口的规范。而且具体实现和接口定义是分离的。这样我们就可以单独改变实现接口的商业类。商业类的调用者既可以是一个asp.net mvc的程序,也可以是一个asp.net web form的,还可以是一个winform程序。
demo代码下载: http://dl.vmall测试数据/c08haaatpu , 博客园这里上传不了。没有办法。只能选别处了。
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息查看更多关于可扩展的单据编号生成器 + 简单的解释器的详细内容...