点击这里给我发消息 点击这里给我发消息

使用asp.net mvc再造一个digg 第一部分

添加时间:2010-1-5
    相关阅读: 搜索引擎优化 网页 SEO ASP 开发 ASP.NET 搜索引擎
 本文示例源代码或素材下载

  原文作者:Kazi Manzur Rashid

  本文地址:http://www.cnblogs.com/jpwar/archive/2008/03/02/1087092.html

  本文译者:第一控制.NET

  学习怎样使用asp.net mvc, LINQ to SQL and ASP.NET AJAX打造一个digg类型的应用程序。

  前言

  前面一段时间,我一直试着学习新的ASP.NET MVC framework,我看到了很多这方面的高级话题的讨论,比如ioc容器/依赖注入,View Engine,Controller factory等。但是我找不到一篇简单的文章来展现ASP.NET MVC framework的能量。当然,知道那些高级话题确实有一些额外的好处,但是他们并不能对你你在ASP.NET MVC Framework开发上有所帮助。在DotNetSlackers team打造的这篇文章里,我将用asp.net mvc开发一个简单的类
似Digg / DotNetKicks类型的程序。你可以在下面的地址里查看这个程序:

  【程序演示】

  注意:这篇文章和代码都是基于ASP.NET 3.5 Extensions的第一个预览版。每当新的预览出来的时候,我们将相应的更新。

  译注:貌似mix08版很快就要出来的样子。

  准备

  scottgu关于ASP.NET MVC Framework的一套介绍:

  ASP.NET MVC 框架 简介

  ASP.NET MVC 教程 (第一部分)

  ASP.NET MVC 教程 (第二部分: URL路径选择)

  ASP.NET MVC 教程 (第三部分: 把ViewData从控制器传到视图)

  ASP.NET MVC 教程 (第四部分: 处理表单编辑和提交场景)

  译注:以上链接全部换为博客堂scottgu博客中文版链接,并比原文增加了第四部分的链接。

  Scott Hanselman的一段很酷的视频教程Screencast。

  概述

  MVC (模式-视图-控制器)是一套开发ui为中心应用程序中很流行的模式。他建立在一个简单的概念上:把整个应用分割成三个逻辑模块

  Model,模式

  View,视图

  Controller.控制器

  ASP.NET MVC Framework是mvc模式的一套实现,并且内置了开发web应用的能力。让我们快速浏览一下这三个模块。

  图一:MVC Framework

使用asp.net mvc再造一个digg 第一部分

  Model:是你程序的领域逻辑。通常来说,model的状态会存储在数据库里。在开发一个n层应用程序中,他是介于领域模型和商业逻辑之间的中间层。

  View: 典型的用户界面,他负责把model的数据展示给用户,并且接受用户输入。

  Controller:处理用户的动作。他是整个ASP.NET MVC Framework的终极驱动力量。建立在用户动作之上,他决定了使用怎样的方法到model里取得数据,把这些数据组织成view data并且最终决定使用什么样的View去展示这些数据。

  跟web form模型相比,ASP.NET MVC Framework是一套更棒的开发web应用程序的方法。他给我们提供了一下能力:

  清晰的分层思路,一个模块专注于处理一类问题。并且在开发过程中给了我们绝佳的TDD (测试驱动开发)体验。在单元测试中我们完全不用顾及其他模块,因为在framework中绝大多数的模块都是interface-based的所以这就允许我们为他们创建模仿对象。

  整个framework都是非常容易扩展的。可以在不影响其他模块的前提下轻松替换或者自定义每一个模块。

  Pretty/SEO (搜索引擎优化) URLs. URLs的设置和创建权牢牢的掌握在我们手里。跟URL重写彻底说再见吧。

  真正的无状态网页。我们再也不需要处理postbacks和ViewState了。

  玩去控制HTML代码产生。这意味着再也没有多余的标签了。

  可以利用现有的ASP.NET的一些特性,比如Providers, Caching, Configuration等等。

  译注:我主要还是喜欢clean url和clean html。

  请求流程

  在asp.net web form程序中,URLs经常被映射到物理磁盘上的文件。当请求一个url时,于该文件相关的代码将被执行。但是,在ASP.NET MVC Framework中,URLs在Controllers就结束了,而不是传统的物理文件。Routing Handler把URL映射到Controller。当应用程序启动时,他需要注册URL筛选规则。当请求来临时,Routing Handler使用这些规则把请求映射到controller。让我们来快速浏览一下在ASP.NET MVC Framework中请求在不同层之间的流转过程:

  图二:The Request Flow

使用asp.net mvc再造一个digg 第一部分

  用户请求了一个URL.

  ASP.NET MVC Framework在已经注册的筛选规则中寻找请求的URL所匹配的Controller.Framework把这个请求交给匹配的Controller

  Controller调用Model来创建ViewData。在ViewData的创建过程中可能会多次调用model。

  Model前面提到过他是一个中间层。他也许是一个数据存取模块,工作流,依赖于外部的web service等等。Model把Controller请求的数据返回给他。.

  Controller选择一个View并且把他刚从model里得到的数据发送给View。View展现这些数据,并且生成html给用户浏览。

  默认约定

  在下一步开发之前,我们必须先知道下面一些默认的约定。

  首先,当为进来的请求匹配Controller时,framework使用类似UrlPathController 这样的模式来匹配。例如,如果请求的是 http://www.example.com/Home,那么就要使用HomeController 来处理这个请求。一旦当请求到达Controller,Controller根据子路径来执行一个指定的行为。或者,如果路径中没有行为那么就执行默认的行为。Controller 的默认行为是在application start事件中和筛选规则定义时一起定义的。行为在Controller类中定义为方法。例如,如果请求以下地址http://www.example.com/Home/Index,那么就会自动执行HomeController中的Index方法。如果url中还有子路径,那么就会把子路径中每个部分转换成方法的参数。

  其次,当使用Visual Studio建立ASP.NET MVC项目时,会自动建立Controllers, Models和Views这三个文件夹。推荐在对应的文件夹里创建文件。但是,如果你在开发一个很大型的项目,那么你可以把models分离出来放在一个或多个项目中。但是Controllers 和Views必须放在MVC项目中。对于每一个Controller,在Views下都会有名字与他想对应的文件夹。比如,如果有个名叫HomeController的Controller,那么在Views 文件夹里就必然又一个名叫Home 的文件夹。如果多个Controller需要使用同一个view,那么这个view就必须放在views文件夹的共享目录里。这个共享文件夹里也可以包含共享的用户控件,css文件,javascript文件等等。

  kigg的相关知识和功能

  在动手之前,先让我们探
讨一下Digg/DotNetKicks类型程序的一些相关知识。这两个程序都是完全的社区驱动,人们在网上找到他们感兴趣的内容,然后在程序里提交。这些内容会立刻出现在upcoming story队列中。其他用户可以对这些文章投票,一旦投票达到某个数值,他就会出现在首页上。

  程序的主要功能如下:

  所有已经发布的Stories列表.

  根据Stories的分类进行列表.

  Upcoming Stories列表.

  根据Stories的标签进行列表.

  根据Stories的发布用户进行列表.

  搜索Stories.

  查看Story的详细内容.

  允许用户提交新的Story (需要登录)

  允许用户对Story 进行Kigg (投票) (需要登录)

  允许用户对Story 进行评论(需要登录)

  允许用户登录.

  允许用户注册

  允许用户重设丢失的密码.

  Controllers 和 Actions的定义

  Kigg的功能是和Story和用户有关的。所以我们可以把所有的功能归为以下两类:

  StoryController: 处理所有Story的列表搜索提交投票等等。

  UserController:处理身份验证,注册,忘记密码等等。

  译注:这里的排版稍微变动了一下。把上面一句总述从列表里独立出来了。

  建议使用实际的功能名称给Controller Actions命名。下面这些代码给出了StoryController里所以的行为方法:

:  1. public class StoryController 
  2. { 
  3.   //List published stories for all category or for a specific category 
  4.   [ControllerAction] 
  5.   public void Category(string name, int? page) 
  6.   { 
  7.   } 
  8. 
  9.   //List all upcoming stories regardless the category 
 10.   [ControllerAction] 
 11.   public void Upcoming(int? page) 
 12.   { 
 13.   } 
 14. 
 15.   //List Stories for a specific tag 
 16.   [ControllerAction] 
 17.   public void Tag(string name, int? page) 
 18.   { 
 19.   } 
 20. 
 21.   //List Stories Posted by a Specific User 
 22.   [ControllerAction] 
 23.   public void PostedBy(string name, int? page) 
 24.   { 
 25.   } 
 26. 
 27.   //Search the Stories 
 28.   [ControllerAction] 
 29.   public void Search(string q, int? page) 
 30.   { 
 31.   } 
 32. 
 33.   //View the details of a specific story 
 34.   [ControllerAction] 
 35.   public void Detail(int id) 
 36.   { 
 37.   } 
 38. 
 39.   //Submit a Story 
 40.   [ControllerAction] 
 41.   public void Submit(string storyUrl, string storyTitle, int storyCategoryId, 
 42.            string storyDescription, string storyTags) 
 43.   { 
 44.   } 
 45. 
 46.   //Kigg the Story 
 47.   [ControllerAction] 
 48.   public void Kigg(int storyId) 
 49.   { 
 50.   } 
 51. 
 52.   //Post a Comment 
 53.   [ControllerAction] 
 54.   public void Comment(int storyId, string commentContent) 
 55.   { 
 56.   } 
 57. } 下面这些代码片段则展示了UserController里所以的行为方法:
  1. public class UserController 
  2. { 
  3.   // Login 
  4.   [ControllerAction] 
  5.   public void Login(string userName, string password, bool rememberMe) 
  6.   { 
  7.   } 
  8. 
  9.   //Logout 
 10.   [ControllerAction] 
 11.   public void Logout() 
 12.   { 
 13.   } 
 14. 
 15.   // Reset the current password and mail back the new password 
 16.   [ControllerAction] 
 17.   public void SendPassword(string email) 
 18.   { 
 19.   } 
 20. 
 21.   //User Registration 
 22.   [ControllerAction] 
 23.   public void Signup(string userName, string password, string email) 
 24.   { 
 25.   } 
 26. } 
注意所以的行为方法都定义为public并且使用了ControllerAction 属性。在下一个版本的ASP.NET MVC中这个属性就不再需要了,所有定义为公共的方法将自动成为一个行为方法。

  定义筛选规则

  一旦Controllers的参数签名确定就应该立刻开始声明把URLs映射到Controllers的行为方法的筛选规则。前面我提到过,这些映射规则在web.config文件中的application start事件中定义。定义筛选规则的时候你要注意,把最特殊的规则放在最上面。这个就跟try/catch块中定义错误处理规则一样,要遵循从特殊到一般的原则。如果你打开Global.asax 文件查看,你会发现我们明确的定义了两个方法来定义这些规则,并且在application start事件中调用方法。这么做是因为我们不想把不同版本iis中使用的规则弄乱。在iis7中有一个很cool的特性,所有的URLs都不需要扩展名,但是在老版本的iis中URLs都需要一个.mvc的扩展名。所以,为了同事支持这个两个版本的iis,我们就要把同一个URL定义两遍,为新的iis定义一套没扩展名的,为旧的iis定义一套有扩展名的。在这里,我们在web.confitg文件里设置当前程序是跑在什么版本的iis中,然后只把当前版本iis用的那套筛选规则读出来。这样做还有一个好处,那就是我们一会要讲的单元测试。下面这些代码展现了筛选规则是如何实现的:

  1. protected void Application_Start(object sender, EventArgs e) 
  2. { 
  3.   RegisterRoutes(RouteTable.Routes); 
  4. } public static void RegisterRoutes(RouteCollection routes) 
  5. { 
  6.   int iisVersion = Convert.ToInt32(ConfigurationManager.AppSettings["IISVersion"]); 
  7. 
  8.   if (iisVersion >= 7) 
  9.   { 
 10.     RegisterRoutesForNewIIS(routes); 
 11.   } 
 12.   else 
 13.   { 
 14.     RegisterRoutesForOldIIS(routes); 
 15.   } 
 16. } 
 17. private static void RegisterRoutesForNewIIS(ICollection<Route> routes) 
 18. { 
 19.   var defaults = new 
 20.   { 
 21.     controller = "Story", 
 22.     action = "Category", 
 23.     name = (string)null, 
 24.     page = (int?)null 
 25.   }; 
 26. 
 27.   routes.Add( 
 28.           new Route 
 29.           { 
 30.             Url = "User/Login", 
 31.             RouteHandler = typeof(MvcRouteHandler), 
 32.             Defaults = new 
 33.             { 
 34.               controller = "User", 
 35.               action = "Login" 
 36.             } 
 37.           } 
 38.         ); 
 39. 
 40.   routes.Add( 
 41.           new Route 
 42.           { 
 43.             Url = "User/Logout", 
 44.             RouteHandler = typeof(MvcRouteHandler), 
 45.             Defaults = new 
 46.             { 
 47.               controller = "User", 
 48.               action = "Logout" 
 49.             } 
 50.           } 
 51.         ); 
 52. 
 53.   routes.Add( 
 54.           new Route 
 55.           { 
 56.             Url = "User/Signup", 
 57.             RouteHandler = typeof(MvcRouteHandler), 
 58.             Defaults = new 
 59.             { 
 60.               controller = "User", 
 61.               action = "Signup" 
 62.             } 
 63.           } 
 64.         ); 
 65. 
 66.   routes.Add( 
 67.           new Route 
 68.           { 
 69.             Url = "User/SendPassword", 
 70.             RouteHandler = typeof(MvcRouteHandler), 
 71.             Defaults = new 
 72.             { 
 73.               controller = "User", 
 74.               action = "SendPassword" 
 75.             } 
 76.           } 
 77.         ); 
 78. 
 79.   routes.Add( 
 80.           new Route 
 81.           { 
 82.             Url = "Story/Detail/[id]", 
 83.             RouteHandler = typeof(MvcRouteHandler), 
 84.             Defaults = new 
 85.             { 
 86.               controller = "Story", 
 87.               action = "Detail" 
 88.             } 
 89.           } 
 90.         ); 
 91. 
 92.   routes.Add( 
 93.           new Route 
 94.           { 
 95.             Url = "Story/Upcoming/[page]", 
 96.             RouteHandler = typeof(MvcRouteHandler), 
 97.             Defaults = new 
 98.             { 
 99.               controller = "Story", 
100.               action = "Upcoming" 
101.             } 
102.           } 
103.         ); 
104. 
105.   routes.Add( 
106.           new Route 
107.           { 
108.             Url = "Story/Search/[q]/[page]", 
109.             RouteHandler = typeof(MvcRouteHandler), 
110.             Defaults = new 
111.             { 
112.               controller = "Story", 
113.               action = "Search" 
114.             } 
115.           } 
116.         ); 
117. 
118.   routes.Add( 
119.           new Route 
120.           { 
121.             Url = "Story/Category/[page]", 
122.             RouteHandler = typeof(MvcRouteHandler), 
123.             Defaults = defaults 
124.           } 
125.         ); 
126. 
127.   routes.Add( 
128.           new Route 
129.           { 
130.             Url = "Story/[action]/[name]/[page]", 
131.             RouteHandler = typeof(MvcRouteHandler), 
132.             Defaults = defaults 
133.           } 
134.         ); 
135. 
136.   routes.Add( 
137.           new Route 
138.           { 
139.             Url = "[controller]/[action]/[id]", 
140.             RouteHandler = typeof(MvcRouteHandler), 
141.             Defaults = defaults 
142.           } 
143.         ); 
144. 
145.   routes.Add( 
146.           new Route 
147.           { 
148.             Url = "Default.aspx", 
149.             RouteHandler = typeof(MvcRouteHandler), 
150.             Defaults = defaults 
151.           } 
152.         ); 
153. }

  如你所见,我们把类似User/Login, User/Signup, Story/Detail, Story/Category这类比较特殊的规则放在前面,而把Story/[action], [controller]/[action]这类一般的规则放在后面。当碰到变量时候,我们用[]来表示。MVC framework有两个固定的变量名:[controller]和[action],其他的就用controller里行为方法的变量名来命名。最后一个规则我们把default.aspx映射到所有分类列表来处理路径/。

  测试筛选规则

  当上面那些筛选规则定义好以后,我们就应该立刻开始测试。这样就可以帮助我们确定现有的筛选规则能否很好的映射所有的controller行为和URL中传递的方法是否正确。下面这个表列出了我们想测试的所有映射规则:

  Table 1: Tests

Functionality Url Format Controller Action
Login登录 User/Login UserController Login
SendPassword发送密码 User/SendPassword UserController SendPassword
Signup注册 User/Signup UserController Signup
Logout注销 User/Logout UserController Logout
List All Published Story列表所有发布的Story Story/Category

  Story/Category/[page]

StoryController Category
List Published Stories for a specific Category

  列表某一类别里所有发布的Storys

Story/Category/[categoryName]

  Story/Category/[categoryName]/[page]

StoryController Category
List Upcoming Stories

  列表Upcoming Stories

Story/Upcoming

  Story/Upcoming/[page]

StoryController Upcoming
List Stories for a specific Tag

  列表某一标签里所有Stories

Story/Tag/[tagName]

  Story/Tag/[tagName]/[page]

StoryController Tag
List Stories Posted By an User

  列表某一用户发布的Stories

Story/PostedBy/[userName]

  Story/PostedBy/[userName]/[page]

StoryController PostedBy
Search Stories

  查询Stories

Story/Search?q=query

  Story/Search/[q]/[page]

StoryController Search
View Details of a Story查看一个Storie的详细信息 Story/Detail/[storyID] StoryController Detail
Submit a Story提交一个Story Story/Submit StoryController Submit
Vote a Story给一个Story投票 Story/Kigg StoryController Kigg
Post a Comment发布评论 Story/Comment StoryController Comment

  你可以在测试项目里找到我们测试所使用的Route.cs文件。我们同时创建了VSTSTest和NUnit两个版本的单元测试。我们用了Rhino Mocks来制作模仿对象。这些测试Phil Haack几个礼拜之前发布的Testing Routes In ASP.NET MVC一文里部分代码创建的。

  译注:模仿对象是单元测试时常用的一种方法。

  下面这些代码片段测试了筛选规则:

  1. [TestInitialize] 
  2. public void Init() 
  3. { 
  4.   routes = new RouteCollection(); 
  5.   Global.RegisterRoutes(routes); 
  6. 
  7.   mocks = new MockRepository(); 
  8. } 
  9. 
 10. [TestMethod] 
 11. public void VerifyDefault() 
 12. { 
 13.   IHttpContext httpContext; 
 14. 
 15.   using (mocks.Record()) 
 16.   { 
 17.     httpContext = GetHttpContext(mocks, "~/Default.aspx"); 
 18.   } 
 19. 
 20.   using (mocks.Playback()) 
 21.   { 
 22.     RouteData routeData = routes.GetRouteData(httpContext); 
 23. 
 24.     Assert.IsNotNull(routeData); 
 25.     Assert.AreEqual("Story", routeData.Values["Controller"]); 
 26.     Assert.AreEqual("Category", routeData.Values["action"]); 
 27.   } 
 28. } 
 29. 
 30. [TestMethod] 
 31. public void VerifyAllCategory() 
 32. { 
 33.   IHttpContext httpContext; 
 34. 
 35.   using (mocks.Record()) 
 36.   { 
 37.     httpContext = GetHttpContext(mocks, "~/Story/Category/20"); 
 38.   } 
 39. 
 40.   using (mocks.Playback()) 
 41.   { 
 42.     RouteData routeData = routes.GetRouteData(httpContext); 
 43. 
 44.     Assert.IsNotNull(routeData); 
 45.     Assert.AreEqual("Story", routeData.Values["Controller"]); 
 46.     Assert.AreEqual("Category", routeData.Values["action"]); 
 47.     Assert.IsNull(routeData.Values["name"]); 
 48.     Assert.AreEqual("20", routeData.Values["page"]); 
 49.   } 
 50. }

  实现UserController

  前面我们定义了UserController的签名,现在我们就来具体实现他。UserController使用了ASP.NET Membership provider来实现登录、注册和其他一些功能。这个controller和其他controller唯一的区别就是这个controller里所以方法都返回一个JSON数据而不是HTML输出。客户端通过ASP.NET AJAX Framework来调用这个controller里的方法。下面这些代码是Login方法的实现:
  
  1. [ControllerAction] 
  2. public void Login(string userName, string password, bool rememberMe) 
  3. { 
  4.   using (new CodeBenchmark()) 
  5.   { 
  6.     JsonResult result = new JsonResult(); 
  7. 
  8.     if (string.IsNullOrEmpty(userName)) 
  9.     { 
 10.       result.errorMessage = "User name cannot be blank."; 
 11.     } 
 12.     else if (string.IsNullOrEmpty(password)) 
 13.     { 
 14.       result.errorMessage = "Password cannot be blank."; 
 15.     } 
 16.     else if (!UserManager.ValidateUser(userName, password)) 
 17.     { 
 18.       result.errorMessage = "Invalid login credentials."; 
 19.     } 
 20.     else 
 21.     { 
 22.       //The following check is required for TDD 
 23.       if (HttpContext != null) 
 24.       { 
 25.         FormsAuthentication.SetAuthCookie(userName, rememberMe); 
 26.       } 
 27. 
 28.       result.isSuccessful = true; 
 29.     } 
 30. 
 31.     RenderView("Json", result); 
 32.   } 
 33. } 

  如你所见,我们在方法开头就创建了一个JsonResult对象。JsonResult是一个用来反馈controller行为是否成功的简单类,他只有两个属性isSuccessful和errorMessage, errorMessage。如果操作不成功,就把失败原因存在errorMessage里。在结尾处,我们把结果当作一个名为Json的共享视图里的view data返回。因为是一个共享视图,所以他可以被UserController和StoryController使用。我们把他放在了Views里名为Shared 的文件夹里。这个controller 里的其他方法工作原理都跟这个十分相似。我这里需要提到的一件重要的事情是,我们在构造函数里传递了一个抽象的membership provider来代替静态的Membership 类。这样做是因为在单元测试中我们可以传递一个模仿的Membership Provider,我们将在下一小节展示。在另一个构造函数中,我们传递web.config中定义的默认membership provider。

  测试UserController

  为了测试这个Controller,我们仍然使用Phil Haack前几周在Writing Unit Tests For Controller Actions一文中的方法。前面一小节说过,我们把一个模仿的Membership Provider传递过去来测试这个controller。我们预想在controller 调用这个membership provider然后能够得到正确的数据并发送给view。下面这些代码展示了正确的登录和当用户名为空时错误的登录:

  1. [TestInitialize] 
  2. public void Init() 
  3. { 
  4.   mocks = new MockRepository(); 
  5.   userManager = mocks.PartialMock<MembershipProvider>(); 
  6.   controller = new UserControllerForTest(userManager); 
  7. } 
  8. 
  9. [TestMethod] 
 10. public void ShouldLogin() 
 11. { 
 12.   using(mocks.Record()) 
 13.   { 
 14.     Expect.Call(userManager.ValidateUser(DefaultUserName, DefaultPassword)).IgnoreArguments().Return(true); 
 15.   } 
 16. 
 17.   using(mocks.Playback()) 
 18.   { 
 19. 
 20.     controller.Login(DefaultUserName, DefaultPassword, true); 
 21.   } 
 22. 
 23.   Assert.AreEqual(controller.SelectedView, "Json"); 
 24.   Assert.IsInstanceOfType(controller.SelectedViewData, typeof(JsonResult)); 
 25.   Assert.IsTrue(((JsonResult)controller.SelectedViewData).isSuccessful); 
 26.   Assert.IsNull(((JsonResult)controller.SelectedViewData).errorMessage); 
 27. } 
 28. 
 29. [TestMethod] 
 30. public void ShoudNotLoginForEmptyUserName() 
 31. { 
 32.   controller.Login(string.Empty, DefaultPassword, false); 
 33. 
 34.   Assert.AreEqual(controller.SelectedView, "Json"); 
 35.   Assert.IsInstanceOfType(controller.SelectedViewData, typeof(JsonResult)); 
 36.   Assert.IsFalse(((JsonResult)controller.SelectedViewData).isSuccessful); 
 37.   Assert.AreEqual(((JsonResult)controller.SelectedViewData).errorMessage, "User name cannot be blank."); 
 38. }

  综述

  我最初想用一篇文章搞定所有问题,但是你也发现了,这篇文章实在是太长了。

  译注:确实长的可以。

  在这篇文章中,我们首先简单了解了一下ASP.NET MVC Framework,然后讲解了如何在controllers里定义功能,如何定义筛选规则并通过URLs测试他们,我们也看见了如何在Controller 中使用JSON 数据来代替完整的HTML视图。在本文的下一个部分里,我们讲着重讲解Controller,如何展示完整的HTML视图,使用master pages和user controls来创建视图,给视图发送强类型的view data和最后创建Model。就此停笔

本文作者:
咨询热线:020-85648757 85648755 85648616 0755-27912581 客服:020-85648756 0755-27912581 业务传真:020-32579052
广州市网景网络科技有限公司 Copyright◎2003-2008 Veelink.com. All Rights Reserved.
广州商务地址:广东省广州市黄埔大道中203号(海景园区)海景花园C栋501室
= 深圳商务地址:深圳市宝源路华丰宝源大厦606
研发中心:广东广州市天河软件园海景园区 粤ICP备05103322号 工商注册