精华内容
下载资源
问答
  • 创建动态数据输入用户界面

    千次阅读 2005-04-16 00:26:00
    下载实例 简介 当创建数据驱动的 Web 站点时,Web 开发人员...需求确定之后,下一步是设计数据输入 Web 窗体,包括创建图形用户界面以及编写根据用户输入更新数据库的代码。 当数据输入窗体需求事先已知,并且此数

    下载实例

    简介
    当创建数据驱动的 Web 站点时,Web 开发人员遇到的最常见的任务之一就是创建数据输入窗体。数据输入窗体是为系统用户提供数据输入方法的 Web 页。创建特定数据输入窗体的任务通常应该先从需求分析入手,即,明确指出需要从用户收集何种信息。需求确定之后,下一步是设计数据输入 Web 窗体,包括创建图形用户界面以及编写根据用户输入更新数据库的代码。

    当数据输入窗体需求事先已知,并且此数据输入窗体对系统中所有用户都一样时,创建这样的输入窗体就毫无挑战性。然而,如果需要动态的数据输入窗体,任务就会变得更艰巨。例如,考虑一个公司的 Internet Web 应用程序,其目的是收集客户购买的产品的信息;一种产品在线注册系统。对于这样的应用程序,向用户提出的问题可能会因购买不同产品而异,或者因从店铺购买还是从公司 Web 站点购买而异。

    如上面提到的例子,当遇到需要提供动态数据输入用户界面时,一种选择可能是“强加”一种解决方案。您可以为公司销售的每种产品创建独立的 Web 页,每张页面包含需要的特定数据输入元素。这种原始方法的问题是当发布新产品时,就需要添加新的页面。虽然创建这些新页面可能不会很困难,但是却很耗时,而且如果没有充分的调试和测试时间就很容易出错。

    理想情况下,当发布新产品时,应由某个非技术人员通过易用的、基于 Web 的界面来指定需要提出什么问题。这样的系统对 ASP.NET 来说是可以实现的,因为它具备在运行时往 ASP.NET Web 页动态加载控件的能力。只需要在开发和测试时投入少量的初期投资,您就可以创建一个可重复使用的、动态的数据输入用户界面引擎。即使对计算机了解甚少的用户,通过这种技术都可以轻松地创建自定义数据输入窗体。在本文中,我们会介绍使用 ASP.NET 中的动态控件的基础知识,然后介绍一个完整的、可运转的动态数据输入系统,可以轻松地对它进行自定义和扩展。

    ASP.NET 中的动态控件入门
    众所周知,ASP.NET Web 页由两部分组成: 

    ? HTML 部分,它包含静态的 HTML 标记和 Web 控件,通过声明性语法来添加。
     
    ? 代码部分,可以作为独立的类文件实现(如采用 Visual Studio .NET),或者包含在 HTML 文件的 <script runat="server"> 块中。 
     

    ASP.NET Web 页的 Web 控件是在设计时通过声明性语法来添加的,它明确指出了要添加的 Web 控件及其初始属性值,如:

      
    < asp:WebControlName runat = " server " prop1 = " Value1 " prop2 = " Value2 " ... propN = " ValueN " > </ asp:WebControlName >

    要理解的一个重点是,当第一次访问 ASP.NET 页面,或者当其 HTML 部分修改后第一次访问时,ASP.NET 引擎会自动将混合的静态 HTML 内容和 Web 控件语法转换成一个类。这个自动生成的类的作用是创建控件层次结构。这个控件层次结构是组成页面的控件集 — 静态的 HTML 标记转换成 LiteralControl 实例,而 Web 控件转换成相应类类型的实例(例如, 转换成 System.Web.UI.WebControls 命名空间中的 TextBox 类的实例)。

    之所以称为控件层次结构是因为它是控件的真正的层次结构。每个 ASP.NET 服务器控件可以有一组子控件和一个父控件。当自动生成的类构造控件层次结构时,它会将代表 ASP.NET 页面的 Page 类实例放在层次结构的顶层。Page 类的子控件是那些在页面的 HTML(通常是一些静态的 HTML 标记以及 Web 窗体的服务器控件)中定义的顶级服务器控件。(ASP.NET 页面的 Web 窗体 — 也就是 <form runat="server">标记 — 是作为 HtmlForm 类的实例实现的,可以在 System.Web.UI.HtmlControls 命名空间中找到这个类。)

    和任何其他服务器控件一样,这个 Web 窗体可以包含子控件。Web 窗体的子控件是那些在该 Web 窗体本身中发现的控件。甚至 Web 窗体中的控件本身还可能有子控件:Panel 控件的内容构成了其子控件;当将数据绑定到一个 DataGrid 时,产生的内容构成了它的子控件集。因为顶级 Page 类可能有子控件,子控件又有子控件,子控件又有子控件,等等,这组控件就构成了控件层次结构。

    为了帮助彻底理解这个概念(理解它对使用动态控件是至关重要的),请想象您有一个 ASP.NET 页面,它在 HTML 部分有以下内容:

      
    < html > < body > < h1 > Welcome to my Homepage! </ h1 > < form runat ="server" > What is your name? < asp:TextBox runat ="server" ID ="txtName" ></ asp:TextBox > < br /> What is your gender? < asp:DropDownList runat ="server" ID ="ddlGender" > < asp:ListItem Select ="True" Value ="M" > Male </ asp:ListItem > < asp:ListItem Value ="F" > Female </ asp:ListItem > < asp:ListItem Value ="U" > Undecided </ asp:ListItem > </ asp:DropDownList > < br /> < asp:Button runat ="server" Text ="Submit!" ></ asp:Button > </ form > </ body > </ html >

    当第一次访问该页面时,会自动生成一个类,这个类包含以编程方式构建控件层次结构的代码。这个示例的控件层次结构如图 1 所示。



    图 1. 控件层次结构

    以编程方式使用控件层次结构
    正如前面提到的,每个 ASP.NET 服务器控件可以包含一组子控件和一个父控件。子控件可通过类型为 ControlCollection 的服务器控件的 Controls 属性访问。ControlCollection 类提供了以下功能: 

    ? 使用 Count 只读属性来确定有多少子控件。 
     
    ? 使用 Add() 或 AddAt() 方法向控件集合添加新项。 
     
    ? 通过 Clear() 方法删除所有子控件,或者通过 Remove() 或 RemoveAt() 方法删除特定控件。 
     

    要将一个控件作为 X 控件的子控件添加到控件层次结构中,只需创建该控件的相应类实例并添加到 X 控件的 Controls 集合中。例如,要向 Page 类的 Controls 集合添加一个 Label 控件,可以使用下列代码:

      
    Create a new Label instance Dim lbl as New Label Add the control to the Page’s Controls collection Page.Controls.Add(lbl) Set the Label’s Text property to the current date/time lbl.Text = DateTime.Now

    在 Page 的 Controls 集合尾部添加控件会使该控件出现在 Web 页的底部。如果您需要的控件比动态添加的控件的位置多,您可以在页面中添加一个 PlaceHolder Web 控件,在层次结构中指定要添加一个或多个动态控件的位置。要在该位置中添加动态控件,只需将它们添加到 PlaceHolder 的 Controls 集合中。例如,如果您想将 Label 放在 Web 窗体中的某个点,您可以按如下方式添加一个 PlaceHolder 控件:

      
    < html > < body > ... < form runat ="server" > ... < asp:PlaceHolder runat ="server" id ="dateTimeLabel" ></ asp:PlaceHolder > ... </ form > </ body > </ html >

    要在上一个示例中添加动态的 Label,不应该使用 Page.Controls.Add(lbl),而应该使用 dateTimeLabel.Controls.Add(lbl),从而将该 Label 添加到 PlaceHolder 的 Controls 集合中,而不是添加到 Page 的 Controls 集合中。图 2 图示了将动态 Label 添加到 PlaceHolder 的 Controls 集合前后的控件层次结构。


    图 2. 图示了添加动态 Label 前后的控件层次结构

    通常,最好的方式是使用 Add() 方法将动态控件添加到 Controls 集合的尾部,而不是使用 AddAt() 将其添加到集合中的特定位置。其原因在于,视图状态的保存方式是每个控件记录自己的视图状态及其子控件的视图状态。当保存其子控件的视图状态时,每个控件记录子控件的视图状态及该控件在 Controls 集合中的序号索引。 

    在回发过程中,当重新加载视图状态时,将反向执行这一过程,同时每个控件加载其子控件的视图状态。重新加载视图状态的控件通过视图状态信息枚举,在 Controls 集合的指定位置应用该控件的视图状态。如果您在视图状态加载之前在 Controls 集合的非尾部位置插入一个控件,则会出现问题,因为每个子控件的视图状态信息是与 Controls 集合中的特定索引相连的。

    要查看在非尾部位置添加动态控件为何会导致重新加载视图状态的问题,请参考图 3。图 3 显示了一个服务器控件 p,它具有三个子控件:c0、c1 和 c2,其中控件 c1 有一些视图状态在回发过程中保持不变。如果在回发过程中向 p 的 Controls 集合前端添加一个动态控件 c,则当重新加载视图状态时,p 会试图重新加载索引 1 中的 c1 的视图状态,而它现在已被 c0 所占用。



    图 3. 具有三个子控件的服务器控件 p

    当删除控件时,也同样会出现与视图状态相关的问题。当然,这一切都取决于在页面生命周期的什么时候添加或删除控件。有关视图状态、页面生命周期,以及添加和删除动态控件与视图状态的相关问题等的更详细讨论,请务必阅读我以前的文章  Understanding ASP.NET View State

    访问动态添加的控件
    当向 ASP.NET 页面添加静态 Web 控件时,Visual Studio .NET 会自动在代码隐藏类中添加对 Web 控件的引用。这些对 Web 控件的引用允许对控件、其属性及方法进行强类型访问。当处理动态添加的控件时,可以使用两种技术来访问控件的属性、方法和事件。

    一种方法是通过对控件层次结构进行彻底的检查,从而发现动态控件。例如,以下代码演示了如何递归循环访问以指定控件为根的控件层次结构。例如,如果已将大量 DropDownList 控件动态添加到指定的 PlaceHolder 中,则这样的代码就十分有用。在这种情况下,您可以通过调用 RecurseThroughControlHierarchy(PlaceHolderControl) 来枚举 PlaceHolder 的控件子代,在“Do whatever it is you need to do with the current control, c 的类型是否是 DropDownList,如果是,就采取某种操作。

      
    Private Sub RecurseThroughControlHierarchy(ByVal c as Control) Do whatever it is you need to do with the current control, c Recurse through c’s children controls For Each child as Control in c.Controls RecurseThroughControlHierarchy(child) Next End Sub

    如果您有大量相似的服务器控件需要共同处理,则上述方法行得通。但在很多情况下,您可能有大量不同的控件,需要在不同时间分别访问并对每个控件执行不同的操作。要以编程方式处理特定的动态添加的控件,您可以使用 FindControl(ID) 方法,根据控件的 ID 搜索控件。FindControl() 方法是在 System.Web.UI.Control 类中定义的,所以所有的 服务器控件,从 TextBox 到 PlaceHolder,再到 Web 窗体,都有这个方法。

    调用一个控件的 FindControl() 方法并不需要搜索该控件的所有子代控件。FindControl() 只搜索当前的命名容器 (naming container)。实现 INamingContainer 的控件行为上就像一个命名容器,意味着它们在控件层次结构中创建自己的 ID 命名空间。例如,DataGrid 控件是一个命名容器。给定一个 ID 为 myDataGrid 的 DataGrid,其子控件的 ID 以父控件的 ID 为前缀,如 myDataGrid:childID。重要的是认识到 FindControl() 只枚举子控件集或命名容器中的控件,而非控件层次结构中父控件的所有子代。(另外,要使搜索范围超越命名容器中的第一级控件,您需要使用作用域恰当的 ID。)其要点是,当使用 FindControl() 来查寻动态添加的控件时,要从该动态控件的父控件(通常是 PlaceHolder 控件)调用 FindControl()。

    当使用 FindControl() 方法时,可以使用如下代码来分配一个唯一的 ID 给动态添加的控件,然后引用上述控件。

      
    When adding the control, set the ID property Dim tb As New TextBox PlaceHolderID.Controls.Add(tb) tb.ID = " dynTextBox" At some later point in the page lifecycle, reference the dynamic TextBox Dim dTB As TextBox dTB = CType (PlaceHolderID.FindControl( " dynTextBox " ), TextBox)

    由于 FindControl() 方法使用控件的 ID 来定位控件,所以当使用这种技术来访问动态添加的控件时,为每个动态添加的控件的 ID 属性分配一个唯一可识别的值是很重要的。根据情况的不同,可以使用不同的方法。我们在本文后面也将看到,当检查动态数据输入用户界面引擎时,每个动态问题都由数据库中的一行表示,它包含一个唯一的主键字段。这个主键字段值即在 ASP.NET 页面中作为每个动态添加的控件的 ID 使用。如果您不需要区分动态添加的控件,则可以使用另一种技术,该技术向这些控件提供递增的编号作为 ID,如 myDynCtrl1 用于第一个动态添加的控件,myDynCtrl2 用于第二个,等等。

    页面生命周期和动态控件
    任何时候访问一个 ASP.NET Web 页(不管是初始页面访问还是回发),每次 ASP.NET 引擎自动生成的类都会从头开始重新构建控件层次结构。不仅重新构造控件层次结构,而且将控件的事件重新连接到其指定事件处理程序。因此,当向 ASP.NET 页面添加动态控件时,确保在每次 页面访问添加这些控件是很重要的。许多开发人员在开始添加动态控件时都使用以下模式来实现:

      
    In the Page_Load event handler... If Not Page.IsPostBack Then Add dynamic controls... End If

    这段代码的问题是它只在第一次页面访问时添加动态控件,而在后续回发时则没有添加。如果您尝试使用这段代码,您会发现,只要发生回发,您的动态控件就会从页面中消失。因此,您必须确保在所有页面访问中添加所有动态控件,方法是将这段代码移到 If Not Page.IsPostBack 条件语句外面。

    添加动态控件引出的一个重要问题是此类控件应在页面生命周期的什么时候添加。正如我在  Understanding ASP.NET View State 中讨论的,只要一个请求到达,ASP.NET 页面就要经历许多步骤。让我们花点时间概述一下页面生命周期内几个紧密相连的阶段。为了能够更深入理解,要确保先阅读一下关于视图状态的文章,重点关注那篇文章中的 The ASP.NET Page Lifecycle 部分。

    ASP.NET 页面生命周期回顾
    页面生命周期中的第一个阶段是实例化,在这个阶段中,自动生成的类会根据页面的 HTML 部分中定义的静态控件构建控件层次结构。构造控件层次结构时,声明性语法中指定的值会赋给添加的每个控件的属性。实例化之后是初始化阶段,在这个阶段,静态控件层次结构已经构造,但还没重新加载视图状态(假定页面请求是回发)。如果页面请求是回发,则在初始化之后是加载视图状态阶段。在这个阶段中,页面会过滤出在隐藏的 VIEWSTATE 窗体字段中发现的视图状态数据,如果需要,控件层次结构中的每个控件会更新自己的状态。

    如果页面请求是回发,则在加载视图状态阶段之后是加载回发数据阶段。这个阶段会检查发送的窗体字段值,并据此更新相应控件的属性。例如,通过 POST 机制(发出信号表示 TextBox 控件的名称和用户输入的值),来回送用户在 TextBox Web 控件中输入的文本。页面获得这些值,在控件层次结构中定位恰当的 TextBox,并将接收的值赋给它的 Text 属性。

    下一个阶段是加载阶段,发生在 Page_Load 事件处理程序激发时。加载阶段之后还有更多阶段,如引发回发事件、保存视图状态和呈现 Web 页,但这些与动态控件的主题无关,因此不加以讨论。图 4 图示了页面在生命周期内所经历的事件。



    图 4. 页面生命周期

    确定在页面生命周期的什么时候添加动态控件
    关于在页面生命周期的什么时候添加动态控件的问题可以归纳如下:动态控件需要在加载视图状态和重新加载回发数据之前添加,因为我们想要正确添加特定于动态控件的任何视图状态或回发值。考虑到这些限制,添加动态控件的正常时间是在初始化阶段,因为它发生在加载视图状态阶段和加载回发数据阶段之前。

    然而,在初始化阶段,视图状态和回发数据都还没还原,因此不建议访问或设置可能存储在视图状态或被回发值修改的控件属性(不管是动态还是静态控件),因为这些值将被生命周期后续阶段的视图状态和回发值所覆盖。当处理动态控件时我使用了以下模式: 

    ? 在初始化阶段,我向控件层次结构添加动态控件并设置 ID 属性 
     
    ? 在加载阶段,我在 If Not Page.IsPostback 条件语句中为动态控件赋予任何需要的初始值。 
     

    我需要在每次回发时添加动态控件,但只在第一次页面加载时设置属性值,因为这些值会保留在视图状态中。以下代码片段说明了这种模式:

      
    In the Init event of the Page, add a dynamic TextBox Dim tb as New TextBox PlaceHolderID.Controls.Add(tb) tb.ID = " dynTextBox" In the Page_Load event handler, set the properties of the TextBox If Not Page.IsPostBack Then Dim dTB As TextBox dTB = CType (PlaceHolderID.FindControl( " dynTextBox " ), TextBox) dTB.Text = " Some initial value" dTB.BackColor = Color.Red initial BackColor End If

    除了在初始化阶段加载动态控件外,您还可以在加载阶段添加,这样不会有什么负面影响。当将控件添加到另一个控件的 Controls 集合时,所添加的控件会立即在其新父控件的生命周期内被确立。例如,如果父控件处于初始化阶段,则会引发所添加控件的 Init 事件,使该控件与其父控件保持同步。如果父控件处于加载阶段或以后的阶段,则所添加的子控件会立即经历初始化阶段、加载视图状态阶段、加载回发数据阶段和加载阶段。

    当在加载阶段添加控件时,有一个警告需要注意。当一个控件完成其加载视图状态阶段后,它就开始跟踪对其视图状态的更改。这意味着加载视图状态阶段之后 的任何属性更改都会自动保留在控件的视图状态中。在一个控件开始跟踪其视图状态的更改之前,属性值更改不会保留在视图状态中。如果您在初始化阶段添加控件然后在加载阶段设置其属性,则不会有问题,因为在初始化阶段和加载阶段之间已经发生了加载视图状态阶段,控件的跟踪视图状态更改标志已设置。也就是说,如果在初始化阶段添加动态控件,则从运行加载阶段起,动态控件的属性赋值会保留在视图状态中。

    注 页面开发人员无法修改“跟踪视图状态更改标志”。System.Web.UI.Control(所有 ASP.NET 服务器控件都由此派生)只提供对该标志的受保护访问。具体来说,有一个名为 IsTrackingViewState 的受保护只读属性来指示是否在跟踪视图状态,还有一个受保护的 TrackViewState() 方法来指示应该开始跟踪视图状态。所有控件在初始化阶段结束时都会自动调用这个方法。

    然而,如果您直到加载阶段才添加动态控件,则该动态控件的任何属性只有在将该控件添加到控件层次结构之后 才能设置,这一点很重要。为了帮助理解其中原因,请考虑如果在加载阶段执行以下代码会发生什么:

      
    Dim tb as New TextBox If Not Page.IsPostBack Then tb.BackColor = Color.Red initial BackColor End If PlaceHolderID.Controls.Add(tb)

    正如您所看到的,在每次页面加载时都会创建一个 TextBox。只有在第一次页面加载时才会将 TextBox 的 BackColor 属性设置为 Red,而在以后的每次页面加载都会将该控件添加到控件层次结构中。虽然在第一次页面加载时 TextBox 的背景颜色确实为 Red,但问题是在回发时 TextBox 的背景颜色会还原为默认值(没有背景颜色)。这是因为 TextBox 的 BackColor 属性赋值没有保留到视图状态中,所以在回发时丢失。丢失的原因是 TextBox 与其他任何服务器控件一样,只有在加载视图状态阶段之后才开始跟踪视图状态。但是 TextBox 只有在被添加到控件层次结构之后才会经历这个阶段,所以 BackColor 赋值没有保留到视图状态中。若要更正这个问题,请确保将控件添加到控件层次结构中,使其提前经历加载视图状态阶段,然后再对其属性赋值,如下所示:

      
    Dim tb as TextBox PlaceHolderID.Controls.Add(tb) If Not Page.IsPostBack Then tb.BackColor = Color.Red initial BackColor End If

    如果您在初始化阶段添加动态控件,则与上述细节无关。有关这个问题的更深入讨论,请参考  my blog 条目  Control Building and View State Lesson for the Day

    事件和动态控件
    与静态服务器控件一样,动态添加的控件也可以将事件与事件处理程序相关联。正如每次页面访问都必须将控件添加到控件层次结构中,每次页面访问也都需要将动态控件的事件与指定事件处理程序连接起来。这样做的部分挑战是您需要在类中定义适当的事件处理程序。如果您的控件确实是动态的,则如何知道代码隐藏类需要什么样的事件处理程序呢?依我的经验,我发现处理事件和动态控件的最好办法是使用用户控件,而不要使用单独的 Web 控件。对于用户控件,我可以在用户控件的代码部分嵌入特定事件处理程序和程序设计逻辑。我们将在下一节介绍如何动态添加用户控件。

    如果您必须将动态添加的 Web 控件的事件与事件处理程序相关联,请确保在每次页面访问时都进行关联。以下代码(包含在本文下载中)演示了如何将一个动态添加的 Button Web 控件的 Click 事件与一个现有的事件处理程序相关联。(ph 是页面中 PlaceHolder 控件的名称。有关用 C# 将事件与事件处理程序连接起来的示例,以及在 .NET Framework 中进行事件处理的更详细信息,请参阅 Peter Bromberg 的文章  Delegates to the Event。)

      
    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Dim b As New Button ph.Controls.Add(b) If Not Page.IsPostBack Then b.Text = "Click Me" End If AddHandler b.Click, New EventHandler(AddressOf Me.ButtonClickEventHandler) End Sub Private Sub ButtonClickEventHandler(ByVal sender As Object, ByVal e As EventArgs) Response.Write("The button has been clicked!") End Sub

    构建动态数据输入用户界面引擎
    在过去几年里,我参与过的大量项目都需要动态数据输入用户界面,即依赖于一个或多个受用户影响因素的用户界面。所有这些项目的一个基本要求是需要这些动态界面能够由非电脑通的用户轻松地创建、更新和删除。经过这些项目的锻炼,我开发了一个动态数据输入用户界面引擎,它允许开发人员创建用户界面生成块,然后由非开发人员将它们拼凑在一起以形成特定于特殊用户的用户界面。 

    在本文的剩下部分,我将循序渐进地介绍这个引擎的简化版本。特别是,本文中的演示说明了如何根据客户的类型产生针对客户的独特数据输入用户界面。例如,显示给普通客户的用户界面与在线客户 (online-only) 或批量购买的客户不同。

    动态数据输入用户界面引擎的基本组成是: 

    用户界面生成块:用户界面生成块是用户控件,由小组中的开发人员负责创建。这些生成块的设计只特定于它们收集的信息的类型,而不特定于所请求的数据。例如,本演示中包含的一个 UI 生成块是一个提示用户输入整数值的生成块。该用户控件包含一个 TextBox 和一个 CompareValidator,用于确保用户输入的是有效的整数值。只要将生成块与一个问题(如“您多大岁数了?”或“您家到办公室有多少公里?”)相关联,该生成块就可以组合进一个动态数据输入用户界面。 
     
    问题:问题是自定义生成块,是由非电脑通用户通过基于 Web 的界面创建的。一个问题将一些文本和一个 UI 生成块相关联。 
     
    区分变量:每个动态数据输入用户界面以一个或多个变量为基础。例如,对于在线产品注册 Web 站点,用户界面可能与购买何种产品有关。对于雇员信息的数据输入,UI 可能因雇员部门而异。对于本文所提出的引擎,区分变量被硬编码为客户类型。 
     
    动态问题:对于给定的区分变量,指定了一组问题。问题和区分变量的组合映射形成了系统的动态问题。 
     
    动态答案:当给定客户的动态数据输入窗体完成时,该客户的信息必须保存到数据库中。给定客户的答案集就是系统中的动态答案。 
     

    在 ASP.NET 应用程序中,动态数据输入用户界面引擎的用户界面生成块部分是作为用户控件实现的。其余部分则是作为数据库实体来实现的。图 5 显示了引擎的实体关系图,描述了各个部分在数据库中如何表示。



    图 5. 实体关系图

    当查看图 5 时,首先注意到 dq_Questions 表。表中的记录表示系统中的问题。每个问题都有一些与问题相关的文本 (QuestionText) 和一个用户控件 (ControlSrc)。ControlSrc 字段包含了用户控件的文件名,如 DQIntegerInput.ascx。其次,在左下角有一个 dq_Customers 表。每个客户都有一个特定的客户类型,dq_CustomerTypes 表中明确指出了所有这些类型。

    动态问题(问题和客户类型之间的映射集)通过 dq_DynamicQuestions 表来实现。其中,一个问题与一种客户类型和一种排序顺序相关联,后者指示对于特定客户类型提出问题的顺序。最后,动态答案存储在 dq_DynamicAnswers 表中,它将每个动态问题与一个特定客户相关联。因为我们不能确定给定问题的答案类型(它可能是字符串、布尔值、整数等),所以 dq_DynamicAnswers 表有六列,每列对应系统允许的一种数据类型。给定的问题可以只有一种类型,对于其答案,相应的字段有答案的值,而其他列则为 NULL 值。

    注 数据模型方面有几点亟需注意。我决定在 dq_DynamicQuestions 表中使用一个综合主键 (DynamicQuestionID) 而不是将 CustomerTypeID 和 QuestionID 作为组合主键使得特定的客户类型允许有重复的问题。例如,一个问题可能是“其他意见”并使用包含多行 TextBox 的用户界面生成块。因为您可能需要在其他许多问题之后有“其他意见”问题,所以我决定允许有重复的问题。dq_Questions 表有着最简单的格式。在过去的项目中我来回使用以下两种做法:保留非常简单的一个表并将详细信息嵌入到用户界面生成块中,或者将附加字段添加到与 UI 交互的这个表中。例如,一个应用程序可能需要能够指示一些问题是必选的,而其他问题是可选的。在这样的系统中有两种方式可以解决这个问题。第一种是将责任交给 UI 生成块。也就是说,不是创建单一的 UI 生成块(比如整数输入),而是创建两个 — 一个使用 RequiredFieldValidator 来确保输入了一个值,另一个则不强加此类条件。当组织这样的问题时,管理员可以根据问题是否为必选来选择使用哪个 UI 生成块。一种替代办法是将 Required 字段添加到 dq_Questions 表中,并只使用一个 UI 生成块。当采用第二种方法时,每个 UI 生成块都需要一个 Required 属性,并负责根据这个属性值启用或禁用适当的验证控件。

    最后是 dq_DynamicAnswers 表,它有六个与答案相关的字段,并仅允许来自一个 UI 生成块的标量答案。也就是说,一个 UI 生成块的答案可以是字符串、整数、双精度型、日期、货币或布尔值。但如果我们需要 UI 生成块有更复杂的答案,如本身可能含有几个字段的地址,则怎么办呢?对于这样复杂的答案,当返回答案时需要由 UI 生成块将其序列化为一个可接受的类型。当显示答案时,需要对这类结果进行相应的反序列化。要实现这一点您可以依赖 .NET 固有的二进制序列化能力,但要这样做您可能需要在这个表中添加一个类型为 binary 的 BinaryAnswer 字段。

    用户界面生成块的设计规则
    为了方便生成具有开发人员设计的用户控件的真正动态数据输入用户界面,作为 UI 生成块使用的用户控件能够提供一个基本级别的功能是很重要的。IUIBuildingBlock 接口明确指出了这种基本级别的功能。这个接口定义了三个属性,所有 UI 生成块都必须实现: 

    DataType:一个只读属性,返回 UI 生成块所提供的答案的数据类型。必须是来自 DQDataTypes 枚举的一个值。 
     
    QuestionText:要在 UI 生成块中显示的问题文本。 
     
    答案:该 UI 生成块的答案。 
     

    为了阐述如何使用这些属性,我们来看一个简单的 UI 生成块。假设我们想要创建一个 UI 生成块,它提示用户输入一个整数。我们可以这样实现:创建一个新的用户控件,它在 HTML 部分包含以下内容:

      
    < asp:Label id ="dqQuestion" runat ="server" CssClass ="DQQuestionText" ></ asp:Label > : < asp:TextBox id ="dqAnswer" runat ="server" CssClass ="DQAnswer" Columns ="4" ></ asp:TextBox > < asp:CompareValidator id ="CompareValidator1" runat ="server" CssClass ="DQErrorMessage" ErrorMessage ="You must enter a number here." ControlToValidate ="dqAnswer" Type ="Integer" Operator ="DataTypeCheck" ></ asp:CompareValidator >

    这个标记包括: 

    ? 一个 Label Web 控件 (dqQuestion),显示 UI 生成块的 QuestionText 属性; 
     
    ? TextBox (dgAnswer),用户在其中输入整数值 
     
    ? CompareValidator,确保输入的确实是一个整数。 
     

    该用户控件的源代码部分相当简单。它让用户控件的类实现 IUIBuildingBlock 接口,并为三个必需的属性提供逻辑:

      
    Public Class DQIntegerQuestion Inherits System.Web.UI.UserControl Implements IUIBuildingBlock ... Public ReadOnly Property DataType() As DQDataTypes Implements IUIBuildingBlock.DataType Get Return DQDataTypes.Integer End Get End Property Public Property Answer() As Object Implements IUIBuildingBlock.Answer Get If dqAnswer.Text.Trim() = String.Empty Then Return DBNull.Value Else Return dqAnswer.Text End If End Get Set(ByVal Value As Object) dqAnswer.Text = Value End Set End Property Public Property QuestionText() As String Implements IUIBuildingBlock.QuestionText Get Return dqQuestion.Text End Get Set(ByVal Value As String) dqQuestion.Text = Value End Set End Property End Class

    DataType 只读属性返回用户控件所返回的数据类型 — 整数。QuestionText 属性只从 dqQuestion Label 控件的 Text 属性读取或向其写入,而 Answer 属性则从 dgAnswer TextBox 的 Text 属性读取或向其写入。所有这些都包含在 UI 生成块中。对于简单的 UI 生成块(就像这一个),代码和 HTML 标记很少,但不要让如此简单的示例掩饰了 UI 生成块的真正强大之处。因为用户控件可以有多个包含事件处理程序等的 Web 控件,所以您可以构建丰富的 UI 生成块。包含在本文代码下载中的一个 UI 生成块阐述了如何在一个 UI 生成块中拥有两个依赖 DropDownList。

    注 当创建 UI 生成块时,确保将它们都放在同一个目录中。不过具体放在哪个目录并无关系。在 Web.config 文件中,您可以找到一个键名为 buildingBlockPath 的 元素。这个设置需要提供对用户控件目录的引用。在代码下载中,其默认路径是 ~/UserControls/,但您可以随意进行更改(如果喜欢的话)。

    有关使用带动态加载用户控件的界面的好处的详细信息,请务必阅读 Tim Stall 的文章  Understanding Interfaces and their Usefulness

    生成问题并将它们与客户类型相关联
    为了使创建动态数据输入用户界面成为非开发人员也能轻松执行的任务,我创建了一个基于 Web 的管理界面,可以用它来生成问题并将它们与客户类型相关联。该界面可以在本文的代码下载中获得。

    管理界面中有两个紧密相关的页面。第一个是 CreateQuestion.aspx,它允许管理员生成新的问题。回顾一下,一个问题就是特定的问题文本加上 UI 生成块。该 Web 页非常简单,它提供了让用户输入问题文本和从 UI 生成块目录(其路径在 Web.config 文件中指定)中选择一个用户控件的方法。图 6 显示了该页面的一个快照。



    图 6. 为非开发人员设计的基于 Web 的界面

    管理界面中的下一屏允许管理员指定将什么问题以及按照什么顺序与每个客户类型关联起来。该界面(如图 7 所示)一看就明白。管理员从最顶部的 DropDownList 选择一个客户类型,然后就可以从第二个 DropDownListBox 添加问题。DataGrid 列出了选定的客户类型的当前问题,它允许用户从列表中删除问题或者通过向上和向下箭头对它们进行重排序。



    图 7. 用于选择问题顺序的 Web UI

    显示动态问题和保存结果
    一旦系统管理员已生成问题并将它们映射到特定客户类型后,就可以输入客户的数据。EnterData.aspx 页面通过查询字符串获取客户的 ID,并构建对应于该客户的客户类型的动态数据输入用户界面。这个页面有三个需要关注的方法: 

    BuildDynamicUI():这个方法是在 Page_Init 事件处理程序(它在页面生命周期中的初始化阶段执行)中调用的,它构建对应于适当客户类型的动态控件。正如前面讨论的,BuildDynamicUI() 只是将必要的控件添加到控件层次结构中。 
     
    Page_Load:该 Page_Load 事件处理程序为动态添加的 Web 控件赋予初始的默认值。例如,如果用户已经为特定客户提供一些值,则当访问页面时,这些值就会填充到适当的动态控件中。这些属性只在第一次页面访问时设置,以后的回发将不再设置。 
     
    btnSaveValues_Click:该方法与 Save 按钮的 Click 事件相连接。它枚举动态添加的控件并更新数据库。 
     

    让我们简要看一下这三个方法。BuildDynamicUI() 方法是在 Page_Init 事件处理程序中调用的。(该事件处理程序由 Visual Studio .NET 在“Web Form Designer Generated Code”区域自动添加。)该方法通过查询字符串捕捉客户 ID,然后用对应于指定客户类型的动态问题填充一个 SqlDataReader。然后会循环访问此 SqlDataReader。对于每条记录,指定的用户控件会加载并添加到 dynamicControls PlaceHolder。为每个动态控件提供一个 dqDynamicQuestionID 形式的 ID。

      
    Private Sub BuildDynamicUI() Called from Page_Init CustomerID = Convert.ToInt32(Request.QueryString("ID")) ... Get the list of dynamic controls for the specified customer reader = SqlHelper.ExecuteReader(connectionString, _ CommandType.StoredProcedure, _ "dq_GetDynamicQuestionsForCustomerType", _ New SqlParameter("@CustomerTypeID", CustomerTypeID)) For each question, add the necessary user control While reader.Read Dim dq As UserControl = _ LoadControl(ResolveUrl(buildingBlockPath & _ reader("ControlSrc"))) CType(dq, IUIBuildingBlock).QuestionText = reader("QuestionText") dq.ID = String.Concat("dq", reader("DynamicQuestionID")) dynamicControls.Controls.Add(dq) dynamicControls.Controls.Add(New LiteralControl("")) End While reader.Close() End Sub

     在本文所包括的示例代码中,我使用 Microsoft Data Access Application Block (DAAB) 2.0 版来访问数据库。该 DAAB 的 SqlHelper 类提供了一个包装,可以用于通过一行代码从 Microsoft SQL Server 数据库访问数据。有关 DAAB 的更多信息,请务必访问  Data Access Application Block for .NET 官方页面,以及阅读 John Jakovich 的文章  Examining the Data Access Application Block

    另外,如代码所示,要动态加载一个用户控件,您需要使用 LoadControl(UserControlPath) 方法而不是创建用户控件类的新实例。有关其中原因的详尽讨论以及对用户控件的深入介绍,请务必阅读  An Extensive Examination of User Controls

    接下来,在 Page_Load 事件处理程序中,从数据库检索针对动态控件的客户当前答案并进行循环访问。引用相应的动态控件,并将其 Answer 属性设置为从数据库获得的答案。这只在第一次页面访问时执行,在以后回发时不执行,因为我们不想覆盖用户在其中一个窗体字段中输入的值。

      
    Get the answers for this customer Get the list of dynamic controls for the specified customer Dim reader As SqlDataReader = _ SqlHelper.ExecuteReader(connectionString, _ CommandType.StoredProcedure, _ " dq_GetDynamicAnswersForCustomer " , _ New SqlParameter( " @CustomerID " , CustomerID)) While reader.Read Dim dq As IUIBuildingBlock = dynamicControls.FindControl( String .Concat( " dq " , reader( " DynamicQuestionID " ))) If Not dq Is Nothing Then Select Case dq.DataType Case DQDataTypes. String dq.Answer = reader( " StringAnswer " ).ToString() Case DQDataTypes. Integer dq.Answer = Convert.ToInt32(reader( " IntegerAnswer " )) Case DQDataTypes. Double dq.Answer = Convert.ToSingle(reader( " DoubleAnswer " )) Case DQDataTypes. Date dq.Answer = Convert.ToDateTime(reader( " DateAnswer " )) Case DQDataTypes.Currency dq.Answer = Convert.ToDecimal(reader( " CurrencyAnswer " )) Case DQDataTypes. Boolean dq.Answer = Convert.ToBoolean(reader( " BooleanAnswer " )) End Select End If End While

    最后,当用户单击  Save 按钮时,会枚举 dynamicControls PlaceHolder 的 Controls 集合,对于已经做出回答的每个动态添加的控件,答案会写回到数据库中。

      
    Create the needed parameters Dim stringParam As New SqlParameter( " @StringAnswer " , SqlDbType.NText) Dim integerParam As New SqlParameter( " @IntegerAnswer " , SqlDbType. Int ) Dim doubleParam As New SqlParameter( " @DoubleAnswer " , SqlDbType. Decimal ) Dim dateParam As New SqlParameter( " @DateAnswer " , SqlDbType.DateTime) Dim currencyParam As New SqlParameter( " @CurrencyAnswer " , SqlDbType.Money) Dim booleanParam As New SqlParameter( " @BooleanAnswer " , SqlDbType.Bit) Enumerate each answer and save it back to the database For Each c As Control In dynamicControls.Controls If TypeOf c Is IUIBuildingBlock Then Mark all of the parameters as NULL stringParam.Value = DBNull.Value : integerParam.Value = DBNull.Value doubleParam.Value = DBNull.Value : dateParam.Value = DBNull.Value currencyParam.Value = DBNull.Value : booleanParam.Value = DBNull.Value Determine which parameter needs to be set Dim uib as IUIBuildingBlock = CType (c, IUIBuildingBlock) Select Case uib.DataType Case DQDataTypes. String stringParam.Value = uib.Answer Case DQDataTypes. Integer integerParam.Value = uib.Answer Case DQDataTypes. Double doubleParam.Value = uib.Answer Case DQDataTypes. Date dateParam.Value = uib.Answer Case DQDataTypes.Currency currencyParam.Value = uib.Answer Case DQDataTypes. Boolean booleanParam.Value = uib.Answer End Select Dim dynamicQuestionID As Integer = Convert.ToInt32(c.ID.Substring( 2 )) SqlHelper.ExecuteReader(connectionString, _ CommandType.StoredProcedure, " dq_AddDynamicAnswer " , _ New SqlParameter( " @CustomerID " , CustomerID), _ New SqlParameter( " @DynamicQuestionID " , dynamicQuestionID), _ stringParam, integerParam, doubleParam, _ dateParam, currencyParam, booleanParam) End If Next

    结束语
    该动态数据输入用户界面引擎是为您的 Web 应用程序开发此类系统的一个好起点,但它不是为现有系统提供无缝集成而设计的。它的设计只是作为演示系统,而不是作为完整的工作系统。该系统的一个部分还不完善,那就是管理界面,虽然也实现了一定功能,但与完整的系统相比还差得很远。具体而言,在如何处理从特定客户类型删除动态问题方面还存在问题。例如,假设管理员对系统进行配置,使得可以对在线客户提出一个是非问题,“这是您在本公司购买的第一件产品吗?”

    现在,假设有大量客户回答了这个问题。如果管理员决定从在线用户的问题集中删除这个问题,则将会发生什么呢?应该将相应的答案从 dq_DynamicAnswers 表中删除吗?应该保存它们以便提供对以前答案的历史视图吗?对于您的应用程序,您需要对这个问题做出回答。现在,当您删除一种客户类型的一个动态问题时,管理界面并没有做什么事情,这意味着如果有一个或多个客户回答了这个问题,就会产生异常而且不会删除问题,因为这样做与在数据库中建立的引用完整性相冲突。

    小结
    在本文中,我们介绍了如何利用 ASP.NET 中的动态控件创建动态数据输入用户界面。正如本文前半部分所讨论的,ASP.NET 页面由一个控件层次结构组成,后者通常严格地由静态定义的控件组成。然而,我们可以在运行时操作该控件层次结构,方法是在该层次结构中的现有控件的 Controls 集合中添加动态控件。我们还了解了访问动态添加的控件的技术以及添加这些控件并与之进行交互的通用模式。

    本文的后半部分介绍创建和使用动态数据输入用户界面的特定实现。所介绍的引擎允许非技术人员的用户根据用户界面生成块轻松地生成问题,该用户界面生成块是由开发人员创建的 ASP.NET 用户控件。针对这些问题,那些非技术人员的管理用户可以将一组问题与特定的客户类型相关联。一个单一的 Web 页,EnterData.aspx 根据访问页面的客户显示和保存适当的数据输入窗体字段和值。

    该工具是一个强大实用的工具,它可以在运行时操作 ASP.NET 页面的控件层次结构,使得应用程序可以适应许多常见场景。通过阅读本文,您应该能够自信地在您的 ASP.NET 页面中使用动态控件。

    尽情享受编程的乐趣吧!

    特别感谢……
    在将本文提交给我的 MSDN 编辑之前,有许多志愿者帮助我校对本文并为本文的内容、语法和目的提供反馈。本文审阅过程中的主要贡献者包括 Milan Negovan、Marko Rangel、Hilton Giesenow、Carlos Santos、Dave Donaldson 和 Carl Lambrecht。如果您有兴趣加入到不断壮大的审阅者队伍中,请通过 mitchell@4guysfromrolla.com 给我发邮件。 

    Scott Mitchell 著有六本书,是 4GuysFromRolla.com 的创始人,也是一个各方面都很优秀的人才。他自 1998 年起就开始从事 Microsoft Web 技术。Scott 是一位独立顾问、培训师和作家。您可以通过  mailto:mitchell@4guysfromrolla.com或他的网络日记  http://scottonwriting.net/ 来与他取得联系。
    展开全文
  • 电商用户行为数据分析

    万次阅读 多人点赞 2019-05-15 20:00:54
    本文针对淘宝app的运营数据,以行业常见指标对用户行为进行分析,包括 一、提出问题 1.电商常用分析体系 2.电商常用分析指标 3.本次分析的业务问题以及分析逻辑 本次想通过对淘宝用户行为数据的分析,解决...

    前言

    本文针对淘宝app的运营数据,以行业常见指标对用户行为进行分析,包括UV、PV、新增用户分析、漏斗流失分析、留存分析、用户价值分析、复购分析等内容;
    本文使用的分析工具以MySQL为主,涉及分组汇总,引用变量,视图,关联查询等内容。

    一、提出问题

    1.本次分析的业务问题以及分析逻辑

    本次想通过对淘宝用户行为数据的分析,为以下问题提供解释和改进建议:

    1)基于AARRR漏斗模型,使用常见电商分析指标,从新增用户数量、各环节转化率、新用户留存率三个方面进行分析,确定影响新增用户数量的因素,找到需要改进的转化环节,发现留存现存问题

    2)研究用户在不同时间尺度下的行为规律,找到用户在不同时间周期下的活跃规律

    3)找出最具价值的核心付费用户群,对这部分用户的行为进行分析

    4)找到用户对不同种类商品的偏好,制定针对不同商品的营销策略

    以下为本次分析逻辑:
    在这里插入图片描述

    2.本次分析所使用的模型和体系

    电商分析通常从四个方面展开,即流程效率分析、流量/用户分析、商品分析、产品分析,通过流程效率拆解追踪问题产生环节,通过用户粘性、价值、满意度分析来进行用户分层及流失预警,通过商品生命周期及关联分析来划分商品等级,通过产品分析提升用户浏览-购买过程体验;
    本文通过常用的电商数据分析指标,采用AARRR漏斗模型拆解用户进入APP后的每一步行为;并使用RFM模型,对用户价值进行评价,找到最有价值的用户群,针对这部分用户进行差异化营销。
    电商数据分析体系
    电商数据分析指标
    AARRR漏斗模型

    二、理解数据

    1.数据来源

    阿里云天池:https://tianchi.aliyun.com/dataset/dataDetail?dataId=649&userId=1

    数据集包含了2017年11月25日至2017年12月3日之间,约一百万随机用户的所有行为(行为包括点击、购买、加购、喜欢)。数据集大小情况为:用户数量约100万(987,994),商品数量约410万(4,162,024),商品类目数量9,439以及总的淘宝用户行为记录数量为1亿条(100,150,807)。

    2.本次选取的数据样本

    原数据集一共有1亿条数据记录,数据量庞大,本次分析选取了大约100万条记录进行分析,数据整体情况参考如下表格。

    3.字段含义及数据量

    在这里插入图片描述

    三、数据清洗

    1. 选择子集

    数据集的每一个字段都有效,此处全部选择。

    2. 列名重命名

    原数据集没有表头,用sql语句创建表及6个字段,把淘宝用户行为数据导入Mysql数据库。

    3. 删除重复值

    数据导入时,将主键定义为:user_id,item_id,timestamps,保证没有重复数据。

    4. 缺失值处理

    在创建表格的时候,5个字段均定义为NOT NULL,数据导入保证没有缺失值。

    5.一致化处理

    转换时间数据类型,并添加datentime,dates,hours三个字段,将转换好的数据放进去

    ALTER TABLE UserBehavior ADD COLUMN datentime TIMESTAMP(0) NULL;
    UPDATE UserBehavior
    SET datentime = FROM_UNIXTIME(timestamps);
    
    ALTER TABLE UserBehavior ADD COLUMN dates CHAR(10) NULL;
    UPDATE UserBehavior
    SET dates = SUBSTRING(datentime FROM 1 FOR 10);
    
    ALTER TABLE UserBehavior ADD COLUMN hours CHAR(10) NULL;
    UPDATE UserBehavior
    SET hours = SUBSTRING(datentime FROM 12 FOR 2);
    
    6. 数据异常值处理

    检查日期是否在规定范围内:2017年11月25日至2017年12月3日。

    SELECT MAX(timestamps),
           MIN(timestamps),
           MAX(datentime),
           MIN(datentime)
    FROM UserBehavior;
    

    检查时间是否正确,并将不符合规定的数据删除

    DELETE FROM UserBehavior
    WHERE datentime < '2017-11-25 00:00:00'
    OR datentime > '2017-12-04 00:00:00';
    

    一共删除了509行数据,再次验证日期时间的准确性,结果符合要求。

    完成清洗后的数据:
    在这里插入图片描述

    四、构建模型

    1.分析用户行为的漏斗模型

    数据主要涉及每日新增用户数,用户购买转化环节从浏览到最终购买整个流程的流失情况(包括浏览、收藏、加购、购买),用户次日、3日、7日留存情况,以及用户在研究时段内的复购次数和复购率

    1)获客:每日新增用户情况

    此处选取2017年11月25日为APP启用的首天,并定义新增用户为出现第一次购买行为的用户。
    如下图所示,2017年11月25日至2017年12月3日期间,每天都有新增用户,但是新增用户在递减,在12月2日当天有小幅回涨,故周末推出的营销活动或正在预热的双十二营销活动能够吸引新用户。
    在这里插入图片描述
    2)转化:转化及流失情况

    ● AAP在9天内的各项指标情况如下

    访问用户总数(UV):9768
    页面总访问量(PV):897293

    #计算UV,PV
    SELECT COUNT(DISTINCT(user_id)) AS 'UV',
    (SELECT count(behavior) FROM UserBehavior
    WHERE behavior = 'pv') AS 'PV',
    FROM UserBehavior;
    

    在这里插入图片描述
    ● 跳失率=只点击一次浏览的用户数量/总用户访问量

    当统计时长为9天时,有586个人浏览了一个页面就离开了APP,占总访问量的0.65%,几乎可以忽略不计,说明淘宝有足够的吸引力让用户停留。

    SELECT COUNT(DISTINCT user_id) AS '跳失用户数'
    FROM UserBehavior
    WHERE user_id NOT IN(SELECT DISTINCT user_id FROM UserBehavior WHERE behavior = 'fav')
    AND user_id NOT IN(SELECT DISTINCT user_id FROM UserBehavior WHERE behavior = 'cart')
    AND user_id NOT IN(SELECT DISTINCT user_id FROM UserBehavior WHERE behavior = 'buy');
    

    在这里插入图片描述
    ● 用户行为转化漏斗计算

    在购物环节中收藏和加入购物车都是确定购物意向的行为,没有先后之分,所以将这两个环节合并为购物环节中的一步。
    由下图可以看到,从浏览到确定购买意向只有9%左右的转化率,夹点出现在点击-收藏或加购这一过程中,可能原因是用户花了大量时间寻找合适的产品,可以针对性的优化平台的筛选功能,让用户能够更容易的找到合适产品,并将流程指标再细化后进行分析,找出影响用户流失的关键问题点。
    在这里插入图片描述

    # 首先创建用户行为视图
    CREATE VIEW user_behavior
    AS
    SELECT user_id, count(behavior),
    SUM(CASE WHEN behavior='pv' THEN 1 ELSE 0 END) AS '点击数',
    SUM(CASE WHEN behavior='fav' THEN 1 ELSE 0 END) AS '收藏数',
    SUM(CASE WHEN behavior='cart' THEN 1 ELSE 0 END) AS '加购数',
    SUM(CASE WHEN behavior='buy' THEN 1 ELSE 0 END) AS '购买数'
    FROM userbehavior
    GROUP BY user_id
    ORDER BY COUNT(behavior) DESC;
    # 再计算转化率
    SELECT CONCAT(ROUND(SUM(点击数)/SUM(点击数)*100,2),'%') AS 'pv',
    CONCAT(ROUND((SUM(加购数)+SUM(收藏数))/SUM(点击数)*100,2),'%') AS 'pv_to_favcart',
    CONCAT(ROUND(SUM(购买数)/SUM(点击数)*100,2),'%') AS 'pv_to_buy'
    FROM user_behavior;
    

    转化率
    ● 独立访客漏斗计算

    APP约有68%的付费用户,用户付费转化率相当高。

    SELECT behavior,COUNT(DISTINCT user_id) AS '用户数'
    FROM userbehavior
    GROUP BY behavior
    ORDER BY COUNT(DISTINCT user_id) DESC;
    

    在这里插入图片描述
    3)留存:新增用户的留存情况

    由下表数据,次日、3日、7日留存率均维持在20%左右,数据作为周留存率来看,表现还是非常优秀的,但作为次日留存来看,就不是很理想了,可以结合产品设计和新用户转化路径来分析用户的流失原因,通过不断的修改和调整来降低用户流失,提升次日留存率;另外,12月2日、3日的留存率相较之前有2%-8%的上涨,故促销活动能为提升留存带来一定正向的影响。

    注:本次研究把2017年11月25日当天的用户全作为新用户,与实际情况不符,考虑实际情况进行分析,淘宝APP的留存已经过了流失期和蒸馏期,进入稳定期,在这样的条件下,若留存率达到20%还是很可观的。
    在这里插入图片描述
    4)变现:复购分析

    下图展示了不同复购次数对应的用户数量,发现复购5次以上的用户仅占有购买行为用户数的10%,我们发现高复购次数的用户很少,商家可以从商品质量、服务质量、物流体验三方面寻找原因,定位所在问题点,寻求高复购率突破。
    在这里插入图片描述
    经下面代码块计算,复购率为65%,淘宝APP的整体复购率相对可观,应将经营重点转化为培养用户的忠诚度上,鼓励用户更高频次的消费。

    SELECT CONCAT(ROUND
    (SUM(case when 购买数>=2 then 1 else 0 end) 
     / SUM(case when 购买数>0 then 1 else 0 end) * 100), '%') 
     AS '复购率' 
    FROM user_behavior;
    

    在这里插入图片描述

    2.不同时间尺度下的用户行为模式分析

    分别以研究全时段(9天,此处觉得再研究单周意义不大),日为单位,分析用户购买行为,找出活跃用户规律。

    1)分析2017年11月25日至12月3日 9天中每天的用户行为

    由图可见在研究日期范围内用户活跃度较为平稳,仅在12月2日、3日有明显增长,该日期和11月25日、26日同为周末,故除特别营销活动外,周末能为提升用户活跃度带来的影响较小;
    另外,几项指标在12月2日点击数涨幅最大,收藏数涨幅最小,可能是双十二促销活动刚开始预热,用户开始大量浏览商品;加入购物车是用户在批量购买商品时的前置动作,因此加入购物车的行为发生次数同样大幅增加。
    在这里插入图片描述

    SELECT dates, COUNT(behavior),
    SUM(CASE WHEN behavior='pv' THEN 1 ELSE 0 END) AS '点击数',
    SUM(CASE WHEN behavior='fav' THEN 1 ELSE 0 END) AS '收藏数',
    SUM(CASE WHEN behavior='cart' THEN 1 ELSE 0 END) AS '加购数',
    SUM(CASE WHEN behavior='buy' THEN 1 ELSE 0 END) AS '购买数'
    FROM userbehavior
    GROUP BY dates
    ORDER BY dates;
    

    2)分析一天中每小时的用户行为

    这里用SQL提取每天的数据,用Excel绘成动态图表,观察发现每日各项行为数据变化趋势相同,这里我们仅选择一个周五2017年12月1日进行分析;
    观察下图发现,12月1日这天共有两个高峰期,分别是晚上20点至22点和上午10点至下午13点,对应了许多上班族中午和晚上休息的时间,符合大部分人的作息规律;
    另外,我们发现加购和收藏量的峰值出现在晚上20-22点,而购买量的峰值则在早上10点,大部分人喜欢晚上收藏加购,早晨购买,说明收藏和购买是异步的用户行为,收藏的峰值通常发生在购买行为的前一段时间,加购则是购买的前置动作,其峰值也会发生在购买行为前。
    在这里插入图片描述

    3. 用户价值分析

    1)用户价值分层(RFM模型)

    由于数据缺少M(消费金额)列,暂且通过R(最近一次购买时间)和F(消费频率)的数据对客户价值进行打分。

    通过打分可以了解每位顾客的特性,从而实现差异化营销。例如对于user_value=44的用户,可划分为VIP用户重点关注,并且投放活动时不要引起反感;而对于user_value=14这类用户粘性不强而消费时间间隔较短,运营活动可以重点针对这部分用户,提高用户使用产品的频率,可以通过拼团打折、积分兑换、捆绑销售等活动唤起用户注意力;对于user_value=41这类忠诚度不高而购买能力强的用户,则需要关注他们的购物习惯做精准化营销。
    在这里插入图片描述

    SELECT r.user_id,f.frequency,recent_rank,freq_rank,
    CONCAT(
    CASE WHEN recent_rank<=(6589)/4 THEN '4' 
    WHEN recent_rank>(6589)/4 AND recent_rank<=(6589)/2 THEN '3'
    WHEN recent_rank>(6589)/2 AND recent_rank<=6589/4*3 THEN '2'
    ELSE '1' END,
    CASE WHEN freq_rank<=(6589)/4  THEN '4' 
    WHEN freq_rank>(6589)/4  AND freq_rank<=(6589)/2 THEN '3'
    WHEN freq_rank>(6589)/2 AND freq_rank<=6589/4*3 THEN '2'
    ELSE '1' END
    )AS user_value
    FROM 
    (SELECT a.*,(@rank:=@rank+1) AS recent_rank FROM
    (SELECT user_id,
    DATEDIFF('2017-11-25',max(dates)) AS recent
    FROM userbehavior AS t1 WHERE behavior='buy' GROUP BY user_id ORDER BY recent
    )AS a,(SELECT @rank:=0) AS b) AS r,
    (SELECT *,(@rank2:=@rank2+1) AS freq_rank FROM
    (SELECT user_id,count(*) AS frequency FROM userbehavior WHERE behavior='buy' 
    GROUP BY user_id  ORDER BY frequency DESC)
    AS a,(SELECT @rank2:=0)AS b) AS f 
    WHERE r.user_id=f.user_id;
    
    4.商品销售分析

    1)商品销售情况

    统计所有商品的购买次数,同时找到购买次数、浏览次数、收藏次数和加入购物车次数最多的商品。

    本次分析的商品共有392778中,用户购买的商品共有16743种,却没有出现购买数量非常集中的商品;在本次统计的数据中,只购买一次的商品有14817种,占用户购买商品数的88.5%,说明商品售卖主要依靠长尾商品的累积效应,而非爆款商品的带动。
    在这里插入图片描述

    SELECT product_buytimes, COUNT(*) AS product_type_count
    FROM
    (SELECT COUNT(user_id) AS product_buytimes
    FROM UserBehavior
    WHERE behavior = 'buy'
    GROUP BY item_id) AS product_buypool
    GROUP BY product_buytimes
    ORDER BY product_buytimes ASC;
    

    ● 商品销量top20
    列出销量前20位的商品,item_ID为3122135的商品销量最高,为15次,那么是否浏览次数最高的商品销量也最高呢?
    在这里插入图片描述
    ● 商品浏览top20
    我们看到浏览数最高的商品为812879,而浏览次数最高的商品甚至没有进入销量前20,说明这些吸引用户更多注意力的商品没有很好的转化为实际销量。
    在这里插入图片描述
    ● 商品收藏top20
    下图为收藏数前20的商品,可以看到排在收藏前1、15、20位的商品在浏览top20中也有出现,分别排名2、10、4,说明收藏和浏览的同步几率更大。
    在这里插入图片描述
    ● 商品加购top20
    在加入购物车次数最多的前20个商品中,可以看到销量第3、4、10位的商品在加购数中排第1、10、8,说明加购数与销量的关系更为直接。
    在这里插入图片描述

    2)商品类目销售情况
    商品类目销售情况中有较为明显的集中趋势,top20如下表所示,可根据畅销类目优化商品展示、加强商品捆绑,进而提高销量。
    在这里插入图片描述

    SELECT category_id , COUNT(*) AS cat_count
    FROM userbehavior
    WHERE behavior = 'buy'
    GROUP BY category_id
    ORDER BY cat_count DESC;
    

    五、结论与建议

    本文分析了淘宝APP用户行为数据共100万条,从四个不同角度提出业务问题,使用AARRR模型和RFM模型分析数据给出如下结论和建议。

    1.通过AARRR模型分析用户使用的各个环节

    1)获取用户
    由于数据中没有给出每个用户第一次的登陆的时间,我们暂且把2017年11月25日作为每个用户的第一次登陆时间来处理。

    在研究时段内,每天都有新增用户,但是新增用户在递减,仅在推出营销活动的周末有小幅回升,故推出的营销活动能够对新用户产生吸引,营销活动的推广渠道或许是用户量增长多少的关键。

    2)激活用户
    在购物环节中收藏和加入购物车都是确定购物意向的行为,没有先后之分,所以将这两个环节合并为购物环节中的一步。

    从浏览到有购买意向只有9%的转化率,当然有一部分用户是直接购买,但也说明大多数用户以浏览页面为主而购买转化较少,此处为转化漏斗中需要改善和提高的环节。

    针对这一环节改善转化率的建议有:
    ①优化电商平台的搜索匹配度和推荐策略,主动根据用户喜好推荐相关的商品,优化商品搜索的准确度和聚合能力,对搜索结果排序优先级进行优化;
    ②给客户提供同类产品比较的功能,让用户不需要多次返回搜索结果反复查看,便于用户确定心怡产品;
    ③在商品详情页的展示上突出用户关注的重点信息,精简信息流的呈现方式,减少用户寻找信息的成本;
    ④优化加入购物车和收藏按键的触达,用户在滑屏时也能方便触达,增加功能使用的次数。

    3)提高留存
    留存分析同样把2017年11月25日作为每个用户的第一次登陆时间来处理。

    淘宝APP的留存相对而言较为稳定,周留存表现优秀,但次日留存略显不理想,可以结合产品设计和新用户转化路径来分析用户的流失原因,通过不断的修改和调整来降低用户流失,提升次日留存率;另外,12月2日、3日的留存率相较之前有2%-8%的上涨,故营销活动能为提升留存带来一定正向的影响,可以多推出一些营销活动,让用户提高使用淘宝电商平台的频率。

    4)增加收入
    使用APP的用户中有61%的付费用户,付费转化率相当高。

    有购买行为的用户中,大概有65%的用户会重复购买,淘宝APP的整体复购率相对可观,应将经营重点转化为培养用户的忠诚度上,鼓励用户更高频次的消费。

    高复购次数的用户少,商家可以从商品质量、服务质量、物流体验三方面寻找原因,定位所在问题点,寻求高复购率突。

    5)用户推荐
    淘宝本身用户基数庞大,知名度高,个人认为在一二线城市的用户基本已经达到饱和,传播工作需要针对三四线城市的渠道下沉,在这些地区针对用户价格敏感度高的特性开展类似拼多多的拼团转发和打折促销活动,扩大这部分用户的使用率。

    2.研究用户在不同时间尺度下的行为规律,找到用户在不同时间周期下的活跃规律

    研究的9天内共有两个周末,第一个周末仅有小幅上涨,而第二个周末因有营销活动预热,出现点击量和加购量的大幅上涨,故周末可多推出营销活动,上班族周末空闲时间的消费欲望还存在很大的挖掘潜力。

    每天有两个高峰期,晚8点至10点,上午10点至下午1点,用户通常喜欢晚上加购收藏,早上进行购买。

    针对高峰期进行营销活动收益最高,此时使用人数最多,活动容易触达用户,营销活动的形式可以通过促销、拼团等形式进行。

    3.通过RFM模型找出最具价值的核心付费用户群,对这部分用户的行为进行分析

    评分是44的用户是体系中的最有价值用户,需要重点关注。并且活动投放时需谨慎对待,不要引起用户反感。

    对于价值评分是14的用户,其粘性不强但消费时间间隔较短,运营活动可以重点针对这部分用户,提高用户的产品使用频率,通过拼团打折、积分兑换等活动唤起用户注意力。

    4.找到用户对不同种类商品的偏好,制定针对不同商品的营销策略

    商品售卖主要依靠长尾商品的累积效应,而非爆款商品的带动。销量最高的商品在浏览数榜单上默默无闻,而浏览次数最高的商品甚至没有进入销量前20,说明这些吸引用户更多注意力的商品没有很好的转化为实际销量。

    针对浏览量高而销量不高的这部分商品,商家可以从以下几个方面提高销售额:
    ①诚信吸引用户,有的商家为吸引用户点击,在商品展示页投放的价格具有较强吸引力,而实际价格偏高,反而造成用户流失;
    ②从用户角度出发设计详情页信息流展示,便于用户获取信息;
    ③优化商品展示的形式,可以采用视频等更直观的展示方式;
    ④评论区评价管理,尤其对于差评区的用户反馈进行认真对待,分析自身劣势,并做出积极的回应和弥补。

    展开全文
  • SQL游标遍历用户表及各字段查找具有指定数据的字段

    为配合公司SAP系统上线,公司内网中需要将所有员工工号格式改变,将工号中的指定部分修改为新值。目前已写好了新旧工号的更新程序,但需要将包含旧工号的数据表和字段名称导入到程序中。

    由于内网用到的数据表很多,每个数据表又有很多字段,逐个数据表查找特别的费时费力,所以我想通过动态SQL逐个表逐个字段统计包含员工数据的个数,如果该字段的所有数据中有包含员工工号,则记录下该数据表和字段;

    具体实现方法如下:

    展开全文
  • FullCalendar: 动态获取数据

    万次阅读 2018-03-29 12:37:09
    项目中需要用到日历插件,在进行一番比较之后选择了FullCalendar,但是看官方文档的过程中...1. 从后台获取数据并按数据状态显示不同背景颜色 关键点:将ajax请求插入calendar 的初始化代码中,将返回的数据转化为...

    项目中需要用到日历插件,在进行一番比较之后选择了FullCalendar,但是看官方文档的过程中发现文档写得不是很友好(也可能是国人跟老外的思维方式不同,哈哈哈...),所以花了不少时间来完成自己想要的功能。FullCalendar请自行安装,项目中涉及的功能如下:

    1. 从后台获取数据并按数据状态显示不同背景颜色

    关键点:将ajax请求插入calendar 的初始化代码中,将返回的数据转化为日历所需要的数据格式

    FullCalendar 自带的数据渲染方式包括数组、json格式、function,这里只需要设置渲染方式为function即可。

    _InitCalendar () {
      var containerEl = $('#calendar')
      // 保存this指针
      var _this = this
      // 渲染日历
      containerEl.fullCalendar({
        header: false, // 不显示头部
        contentHeight: 500, // 设置内容高度
        locale: 'zh-cn', // 语言
        editable: true, // 可编辑
        eventLimit: true, // allow "more" link when too many events
        // 使用function动态获取数据,start为开始时间,end为结束时间,这个不需要我们来设置,日历自动获取
        events: function (start, end, timezone, callback) {
          let from = _this.GmtToStr(start._d)
          let to = _this.GmtToStr(end._d)
          console.log('ajax get from:' + from + ' to:' + to)
          // 这里是ajax请求,替换为你正在使用的ajax方式就可以
          _this.apiGet('/tp_laoren/php/index.php/admin/laoren/status/1/record?from=' + from + '&to=' + to)
            .then((res) => {
              let events = []
              if (res.errcode == 0) {
                for (let item in res.data) {
                  // 数据处理,将返回的数据添加到events中
                  events.push({
                    // 标题,即你想展示的内容
                    title: _this.GetMoneyByCount(res.data[item].count),
                    start: res.data[item].date,
                    // 自行定义函数,根据status字段返回边框颜色
                    color: _this.GetColorByStatus(res.data[item].status),
                    // 同上,这里是背景颜色
                    backgroundColor: _this.GetColorByStatus(res.data[item].status)
                  })
                }
                // 虽然不知道这个回调的具体内容是什么,但它可以将刚刚获取到的events数据渲染到日历中
                callback(events)
              } else {
                console.log('获取信息失败!')
              }
            }, (err) => {
              console.log(err)
            })
        },
        // 点击某一天响应事件
        dayClick: function (date, jsEvent, view, resourceObj) {
          _this.showModalConsumeLog = true
          // 获取当前点击的日期
          _this.nowDate = date.format()
        }
      })
    }
    

    2. 按月份查找,重新渲染日历内容

    关键点:选择月份后,日历跳转到相关月份,重新获取数据渲染日历显示结果

    先获取用户选择的时间值,通常包含年份和月份。然后对FullCalendar 进行日期跳转动作,跳转后日历会自行刷新。

    ChangeDate () {
      // 此处time的值为用户选择的时间
      let time = this.year + '-' + this.month + '-1'
      // 将该时间转化为一个Date类型
      let date = new Date(time)
      // fullCalendar日期跳转,跳转后会自动刷新
      $('#calendar').fullCalendar('gotoDate', date)
    }

    3. 相关bug,gotoDate 无效

    被这个问题困扰了两个小时,一开始我使用的日历跳转时间方式不是按上面所说的方式进行的,而是按照以下“FullCalendar 中文文档”中介绍的方式:

    // year和month为用户选择的时间
    $('#calendar').fullCalendar('gotoDate', year, month)
    

    结果发现日期跳转总是出错,第一下操作显示跳转到2018-3-26 号,第二下操作就直接跳到了1970-1-1。一直在研究跳转为什么会出错,直到终于找不到任何原因后才开始尝试“官方文档”的日期跳转方式,然后bug就消失了!消失了!!

    4. 关于FullCalendar 日期显示

    该插件中日历显示内容包含六行(六个周),每周七天,总共42天。不论跳到哪一个月,显示的天数都是42天。也就是说,实际上跳到每个月的任意一天,日历显示的内容都是一样的,不过是使用getDate 获取到的值不一样而已。所以用户选择某年某月的时候是可以使用gotoDate 方法跳转到所选月份的任意一天的。

    展开全文
  • 用户行为数据

    千次阅读 2016-05-18 16:19:55
    总体思路是通过用户留下的文字和行为了解用户兴趣和需求,通过算法自动发掘用户行为数据。基于用户行为分析的推荐算法一般称为协同过滤算法。个性化推荐算法通过对用户行为的深度分析,可给用户带来更好的网站使用...
  • Server 2016引入的一项新的特性,通过数据屏蔽,你可以对非授权用户限制敏感数据的曝光。动态数据屏蔽会在查询结果集里隐藏指定栏位的敏感数据,而数据库中的实际数据并没有任何变化。动态数据屏蔽很容易应用到现有...
  • 蓝牙广播数据格式和动态改变

    万次阅读 2017-03-25 23:28:17
    蓝牙广播数据格式和动态改变
  • oracle数据字典及用户权限查看

    千次阅读 2017-10-22 20:02:30
    数据字典和动态性能视图 数据字典是数据库中最重要的组成部分,他提供了数据库的一些系统信息。(静态信息,常规的信息)(基表) ...包括基表和数据字典视图。 基表存放数据库的基本信息,普通
  • 携程实时用户数据采集与分析系统

    万次阅读 2017-05-31 10:40:27
    一、携程实时用户数据采集系统设计实践随着移动互联网的兴起,特别是近年来,智能手机、pad等移动设备凭借便捷、高效的特点风靡全球,同时各类APP的快速发展进一步降低了移动互联网的接入门槛,越来越多的网民开始从...
  • 网站用户行为数据收集和分析方法

    千次阅读 2016-10-25 23:07:15
    综合介绍了目前国内外对于用户行为数据收集和分析方法所进行的研究, 各种方法的特点, 并介绍一些利用相应方法所开发出的工具实例, 使得建设的网站更加符合用户的需要, 以保障用户与网站之间沟通的顺畅。 随着In ...
  • Github API:爬取Github用户数据

    万次阅读 2018-06-03 23:18:59
    Github API:爬取Github用户数据引言目标:根据给定的论文中中文作者的英文名字(实际上就是拼音),从Github上获取用户邮箱信息。一、Github API介绍详细的开发者文档想了解相关参数设置和可爬取的数据,可阅读...
  • 回顾网站数据分析历史,从“您是第***位来访用户”到现在百家齐放的专业工具提供商,网站分析已经逐渐发展衍化成一门科学。但面对形态各异的分析数据,很多人仍然困惑于数据的来源,了解数据的收集原理,也许对你...
  • 哦豁!Facebook又有5.33亿用户数据被泄露!

    千次阅读 多人点赞 2021-04-09 10:44:14
    公开的数据包括来自106个国家和地区的超过5.33亿Facebook用户的个人信息,其中包括超过3200万条美国用户记录,1100万条英国用户记录和600万条印度用户记录。 数据内容包括他们的电话号码,Facebook ID,全名,位置,...
  • VC动态建立ODBC数据

    千次阅读 2011-07-12 17:25:38
    动态创建数据源需要用到的API:SQLConfigDataSource,需要包含的头文件为:#include ,需要包含静态库:#pragma comment(lib, "ODBCCP32.lib")。 SQLConfigDataSource 函数说明 ODBC API提
  • 数据收集主要包括用户行为数据用户基础数据。 下面是某跨境电商平台的例子: 二、搭建用户画像标签体系 通过对用户行为数据进行分析和计算,为用户打上标签,可得到用户画像的标签建模,即搭建用户画像...
  • 导出、导入某用户所有数据包括表、视图、存储过程...) 前提:在CMD 命令下   导出命令:exp 用户名/密码@数据库 owner=用户名 file=文件存储路径(如:F:\abcd.dmp) 测试截图:exp ZM/sql123@ORCL owner...
  • 回顾网站数据分析历史,从“您是第***位来访用户”到现在百家齐放的专业工具提供商,网站分析已经逐渐发展衍化成一门科学。但面对形态各异的分析数据,很多人仍然困惑于数据的来源,了解数据的收集原理,也许对你...
  • Oracle的数据字典和动态性能视图

    千次阅读 2014-10-04 17:12:20
    Oracle的数据字典和动态性能视图 数据字典是oracle数据库中最重要的组成部分,它提供了数据库的一些系统信息。 动态性能视图记载了例程启动后的相关信息。 一、数据字典 数据字典记录了数据库的系统信息,...
  • 用户画像:数据指标与表结构设计

    万次阅读 2018-09-07 11:09:43
    首先介绍画像开发的数据指标,画像开发过程中通用类的指标体系包括用户属性类、用户行为标签类、用户活跃时间段类、用户消费能力类、用户偏好类等 数据指标体系 用户属性指标 用户属性指标根据业务数据来源,尽...
  • GDPR中的三大主体:用户数据所有者,以及数据传输者
  • 在业务支撑工作中,与核心网主要的交互包括用户数据管理(含签约关系、策略数据),5G核心网中与用户数据相关的NF功能体包括UDM、AUSF和PCR以及UDR,在此只简单介绍这些NF的功能: UDM:统一数据管理功能Unified ...
  • 永久保存用户设置以及数据的两个地方 罗朝辉... 转载请注明出处 ...要想将用户数据或设置永久保存起来,不受 app 更新重新安装的影响,我们只能将这写数据保存在 userDefaults 中或 Document
  • 【写在前面】上一期来梳理交流了怎么才算真正的理解“数据”。而人不同,方法、思路、数据都不同,做到了理解“数据”那么又如何做到以数据驱动业务增长呢?(【数据说第八期】怎么才算真正的理解“...
  • oracle数据字典和动态性能表

    千次阅读 2013-06-16 17:44:26
    因为数据字典中有用户的信息,我们登录的时候要查询数据字典,有权限信息,我们做操作的时候也要查询数据字典,有约束信息 我们操作数据的时候也要查询数据字典,所以数据字典对于oracle数据库非常重要。 主要了解...
  • 一定会遇到网站内容是通过ajax动态请求、异步刷新生成的json数据的情况,并且通过python使用之前爬取静态网页内容的方式是不可以实现的,所以这篇文章将要讲述如果在python中爬取ajax动态生成的数据。至于读取静态...
  • Java EE数据持久化框架 • 【第4章 MyBatis动态SQL】

    千次阅读 多人点赞 2021-04-20 12:33:04
    在JDBC操作数据表时,经常需要根据不同条件进行查询,而条件值需要根据用户的选择而定,需要拼接 SQL 语句,且拼接时经常有繁琐易错的操作。 // jdbc拼接sql片段 String sql = “select * from tb_users where 1=1...
  • 分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍...如果有包含多个值的参数(例如复选框),可以通过请求对象的getParameterValues()方法获得。当然也可以通过请求对象的get...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,918,070
精华内容 1,167,228
关键字:

动态用户数据包括