Untitled - 文曲经典数字图书馆

388

Transcript of Untitled - 文曲经典数字图书馆

前 言

·I·

B2B2C网上商城开发指南

—基于 SaaS和淘宝开放平台

邢波涛 郭 娟 著

前 言

·II·

内 容 简 介

本书以已经广泛应用到各行各业的进销存软件为需求背景,以流量最大的电子商务网站淘宝网的卖家

为需求方,以 Java、J2EE 和淘宝 API 开放平台为实现手段,介绍了如何把进销存和淘宝结合起来。对于进

销存常见的功能,例如采购管理、库存管理、销售管理,商品管理、统计查询以及淘宝卖家最常用的拆单、

合并以及打印,本书都有详细的功能论述以及架构、数据库方面的设计介绍。而对于 SaaS(Software as a Service)与管理软件和电子商务的结合,B2B2C(Business to Business to Consumer)的概念和实现,类似

ShopEx、Ecshop 网上商城的实现,本书也给出了一个完整的例子和有益的探索。 本书适合对电子商务、网上商城的开发以及对 SaaS 数据隔离感兴趣的 Java、J2EE 开发者使用,对于

PHP 和.NET 程序员仅具有解决问题思路上的参考价值。

未经许可,不得以任何方式复制或抄袭本书之部分或全部内容。 版权所有,侵权必究。 图书在版编目(CIP)数据

B2B2C 网上商城开发指南:基于 SaaS 和淘宝 API 开放平台 / 邢波涛,郭娟著. —北京:电子工业出版社,2011.4

ISBN 978-7-121-12983-4

Ⅰ. ①B… Ⅱ. ①邢…②郭… Ⅲ. ①电子商务-网站-应用程序-程序设计 Ⅳ. ①F713.36②TP393.092

中国版本图书馆 CIP 数据核字(2011)第 027540 号

策划编辑:袁金敏 责任编辑:徐津平 印 刷:三河市鑫金马印装有限公司 装 订:三河市鑫金马印装有限公司 出版发行:电子工业出版社

北京市海淀区万寿路 173 信箱 邮编 100036 开 本:787×980 1/16 印张:24.25 字数:481 千字 印 次:2011 年 4 月第 1 次印刷 定 价:49.00 元

凡所购买电子工业出版社图书有缺损问题,请向购买书店调换。若书店售缺,请与本社发行部联系,

联系及邮购电话:(010)88254888。 质量投诉请发邮件至 [email protected],盗版侵权举报请发邮件至 [email protected]。 服务热线:(010)88258888。

前 言

·III·

前 言

电子商务、云计算与移动互联网是最近两年最热闹的名词了。云计算有 Google、IBM

和微软之类的 IT 巨头领导,形成事实上的寡头垄断。虽是朝阳产业,但无论在技术门槛上,

还是产业链上,草根都很难参与;移动互联网在国内有较好的发展,以李开复老师的创新

工厂最为有名,但目前还没形成完善的产业链,草根参与进去,短期之内也很难发展;但

是在电子商务领域,根据最新的数字统计,淘宝已经超越亚马逊和 eBay 成为全球最大商圈。

在电子商务领域,事实上已经形成了一超(阿里巴巴、淘宝)+多强(慧聪网、京东商城、

卓越亚马逊、当当网……)的局面。电子商务有序竞争的整体格局,使得基于电子商务的

产业链也健康地发展起来了。早在几年前,淘宝网就率先开放了自己的 API 平台,使得大

卖家和开发者可以通过 API 的形式来调用淘宝网的商品和订单数据。基于淘宝开放平台生

存下来的个体开发者和公司很多。可以说,电子商务的成熟度已经有草根和中小软件公司

足够活下去的空间了。事实上,与电子商务相关的软件,也已经如火如荼地发展起来了。

国内著名的网上商城软件例如上海商派的 Shopex、Ecmall 和 Ecshop 系列,国外著名的例

如 Zen Cart、Magento 和 PrestaShop 等。

从技术上来说,这些软件大多是用 PHP 编写的,而本书实际上是我在创业过程中一个

实际产品开发的结果,所以实战性非常强。它从 Java、J2EE 的角度来讲解如何架构自己的

电子商务和网上商城软件;从应用上,本书结合已经广泛应用到各行各业的进销存软件,

以实际的商业进销存软件为背景,介绍了如何把进销存跟淘宝相结合起来。进销存常见的

功能,例如采购管理、库存管理、销售管理,本书都有详细的功能论述以及架构、数据库

方面的设计介绍。而在与淘宝 API 的结合方面,本书是第一本以案例形式分享淘宝开放 API

用法的第三方(非淘宝官方)书籍。对于 SaaS 架构方面,我主要介绍了自己对 SaaS 的理

解,并从技术上给出了电子商务在 SaaS 上的解决方案,即 B2B2C 平台。书中还掺杂了很

前 言

·IV·

多笔者在软件架构、项目管理等方面的个人感悟。电子工业出版社编辑曾建议去掉书中夹

杂了太多个人感情色彩的内容,担心会招致口水战。其实我并不想写一本中规中矩的技术

图书,书里面很多结论虽然夹杂了笔者的个人喜好,但却是笔者十几年的开发和架构经验

的总结。我只是把我的经验写出来,供大家参考,并促使大家养成独立思考的习惯。这也

算是本书比较的一个特色吧,欢迎大家沟通、交流和拍砖。

这本书之所以能问世,首先要感谢的就是《程序员羊皮卷》的作者张大志先生,是他

点燃了我写此书的希望并最终促成了本书的出版。其次要感谢的是我的妻子郭娟,她不仅

编写了本书的部分章节,而且女儿出生后还牺牲了自己的工作全职在家,使我无后顾之忧

地全身心投入到工作和写作中。第三个感谢的就是我的天使投资人、北京应天海乐科技发

展有限公司创始人和董事长史本才先生,正是由于史先生的支持,才使得书中软件的正式

商用成为可能。还要感谢清华大学计算机学院张勇教授、淘宝商城合作伙伴部门负责人罗

文军(花名玉关)、阿里巴巴 B2B 技术专家李锟、InfoQ 中文站张凯峰、文思同事赵焱辉,

他们都在百忙之中阅读了全书,提出了很多宝贵的意见并慨然作序。此外,司永靓、金怡

珺为本进销存软件提供了很多有价值的建议,淘宝网员工方胜、雁行、子 以及淘宝网五皇

冠大卖家李杨、孙朝晖和桔子,都为本书中出现的进销存软件提供了宝贵的更改意见,孙

朝晖还把他试用过的软件都一并介绍给我,在此一并致谢。

本书编写过程中也遇到过很多技术难题,有关于 SaaS 架构的,有关于 Flex 方面的,

也有关于淘宝 API 的,这些具体难点,有的已经解决了,有的还在探索中。主要是因为个

人能力确实有限,尤其是对 SaaS 架构理解方面,我写得还非常的肤浅,敬请读者见谅和

海涵。

中国人错过了数次工业革命,但在互联网经济面前,特别是电子商务面前,我们跟发

达国家相差不远。让我们为电子商务在国内健康发展贡献出自己的一份力吧。

邢波涛

2011 年 3 月

序 一

·V·

序 一

时值初春,想脱开世俗的春节过法,给终年劳顿的身体好好放松一下,便在杭州的莫

干山中找了个休闲之处躲了起来,白天游荡在山间:青山、绿水、翠竹、暖阳、小溪、残

雪;晚间幽居在住处:星空、炉火、烛光、犬吠、温酒、闲书,好不惬意!我自逍遥之时,

唯一的一件正事,就是受邢波涛之托,为这本新书写一个序。

原有受人之托、忠人之事的心态,哪知刚一展卷,便一发不可收拾——灯下一口气看

完了这本近 10 万文字的书稿,掩卷而坐,不由赞叹不已。

熟悉邢波涛的人知道,他是业内资深的 J2EE 专家。2003 年前后,在管理软件在向平

台型转型的浪潮中,因为一个偶然的机会认识他时,他还只是一个资深的程序开发人员,

但对于业界变化的高度敏感,入木三分的分析,远远超出了他当时应该关注的范围,让我

大为惊讶。我们对于业内许多的认知,也有着非凡的默契,从此便结为好友。后来,SaaS

的逐步发展、电子商务的逐步成熟,我是一路奔走,在公司之间跳跃着追赶一趟趟行业发

展的潮头末班车,而波涛却在继续修炼自己作为程序员的内功的同时,高度关注着 SaaS 的

发展。在业余时间,利用自己丰富的经验独立架构完成了一个 SaaS 版本的应用系统并已经

有了诸多的用户。今天波涛又充分与时俱进,将 SaaS 与电子商务完美结合起来,从底层的

架构,到应用系统,再到兼具前后台的电子商务的平台搭建,这中间,从技术到商业、从

理论到实践、从传统管理软件到 SaaS 再到电子商务的多重跨度,绝非一日之功。

书中结合行业趋势,系统分析了国内外主流的网店产品,并结合自己的实践经验,给

出了网店从前台到后台完整的需求分析、设计框架以及技术架构,是不可多得的实践结合

理论以及行业现状的实战指南。

另外值得一提的是,这本书,也是我所知范围内,第一本以案例形式分享淘宝开放 API

用法的第三方书籍——淘宝自 2009 年 6 月正式开放 API,历时一年多,除了引领得百度、

序 一

·VI·

盛大、拍拍等竞相开放之外,也以淘宝特有的优势,吸引了 3 万多名国内主流的开发者,

同时,也造就了超过 1000 个第三方商业应用在大淘宝的体系内遍地开花、精彩纷呈的格局,

中间不乏脱颖而出、年收入超百万的独立开发者。

我在 TOP(淘宝网开放平台 Taobao Open Platfrom 简称)1 年多的时间内,一直在努力

致力于如何更好的开放、如何搭建更好的开放平台运营支撑体系,以便更好地支持到大淘

宝开放体系下的用户——从年销售过亿的大商家、大大小小的开发者再到一名普通的淘

客——中间支持过的人数众多,但绝大多数都是以支持对象自用为基调,鲜有第三方能够

出来著书立说,结合实践进行充分分享。本书的作者,仅仅是淘宝开放 API 的诸多用户之

一,以这种方式进行大范围内分享,无疑是我所见到的第一个。

在此,我表示敬意,也希望更多的开发者,有志于 SaaS 以及电子商务的同仁们,能

够从中受益,让电子商务的基础设施建设,能够快速跟上行业的发展。

玉关(罗文军)

2011 年 2 月 5 日于杭州莫干山中

序 二

·VII·

序 二

电子商务已经成为我们这个时代最重要和最成功的互联网应用之一,越来越多的人都

在通过网上商城来购买商品。在北京的大街上,每天都可以看到骑着电动自行车带着大箱

小包的快递人员;在各个网上商城上闲逛,已成为了某些网虫的“强迫症”。搭建一个功能

强大的网上商城,成为各行各业开发电子商务的迫切需要。这里需要弄清楚网上商店和网

上商城的区别,前者往往只关心订单和商品,而后者则是需要支持在线分销系统,并且通

过 SOA 架构,跟企业内部管理信息系统做深度集成。

SaaS 模式作为一种在线服务模式,中小企业可以按照自己的需要,通过租用方式,获

得由第三方专业公司负责的信息化服务,实际上是一种中小企业信息化服务外包的模式。

正是由于 SaaS 模式的出现,才使得中小企业经营网上商城更加便捷。

虽然现在有一些类似的开源软件,但是在真正使用的时候,人们往往会发现这些软件

并不太适合自己的需求,必须得二次开发或者改造。本书面向有一定经验的开发者,提供

了一个既支持 SaaS 多租户架构,又支持深度个性定制化的网上商店系统,进而全面整合已

有企业业务系统的 B2B2C 运营平台。

本书的另外一个特色是把淘宝的数据,作为自己进销存销售订单数据来看待,使得淘

宝相当于自己的一个最具有人气的销售窗口或者柜台。通过这种整合,使得一个新的网上

商城能够很快利用淘宝网的巨大流量上得到收益,同时,也减轻了开发的工作量。

本书语言诙谐,描述生动,易于理解,通过大量的实例来进行讲解,深入浅出,是中

小企业开发和经营网上商城的参考宝典。

张 勇

清华大学计算机学院教授

序 三

·VIII·

序 三

马云无疑是中国互联网行业的一个传奇。马云所领导的阿里巴巴集团,是对普通中国人

影响力最大的互联网企业之一。而在阿里巴巴集团中,影响力最大的要算是淘宝网。淘宝网

在中国,已经成为了网上购物的代名词,其地位是其他同类型网站无法撼动的。在电视剧《婚

姻保卫战》中,偶像明星黄磊、佟大为都是淘宝网的忠实用户。偶像的力量是无穷的!

淘宝网取得今天的江湖地位,并不是靠单打独斗,其实围绕着淘宝网形成了一个巨大

的电子商务生态系统。淘宝网并不是只想自己赚到钱,而是希望加入到这个生态系统中的

合作者们都能赚到钱,共同发展。淘宝网开放平台,对于形成这个生态系统,具有非常重

要的作用。

其实建造开放平台,在互联网行业早已是一种潮流。同样属于电子商务企业的 eBay

和亚马逊,都有自己的开放平台。阿里巴巴所收购的两家美国电子商务企业 Vendio、Auctiva

都是依托 eBay 的开放平台来开展自己的业务的,而且业务量都很大。所以我们有理由相信,

未来在国内,也一样会出现很多依托淘宝网的中型电子商务企业。为淘宝网上的商家提供

各种服务,蕴含着巨大的商机。这块蛋糕,淘宝网自己不可能全部吃掉,也无意全部吃掉。

本书正是基于这股潮流应运而生,为准备依托淘宝网开展电子商务的企业提供了技术方

面的全方位指南。对于其他对电子商务感兴趣的网站开发者来说,这同样也是一本非常有价

值的书。读者除了可以领略到作者精湛的技术功力之外,也一定不要忽略了体会作者对于电

子商务发展趋势的深刻洞察力。我们常常说,开发人员要两条腿走路,技术、业务两方面都

不可偏废。但是很少有书能同时带给我们技术和业务两方面的收获,而这本书恰好做到了。

我强烈建议国内希望在电子商务领域一展才华的开发者们,都来认真读一下这本书。

李 锟

阿里巴巴 B2B 技术专家

序 四

·IX·

序 四

在好友邢波涛给我发来本书书稿的时候,我刚好在新浪的微博上看到这样一条信息:

来自淘宝消息,有个杭州某 IT 技术人员,基于淘宝开放平台开发了一个“抽奖”产品,花

了半年时间,每天 20 小时的编写代码;该产品一上线,给他带来每月几十万的稳定收入。

两点体会:1.一切需要坚韧、执着和极致;2.淘宝开放平台很有前景,好好利用的话,

不次于苹果公司的 iTune 商店。

一条令人震撼的消息,不是吗?蓬勃发展的互联网不仅为企业和希望创业的人们带来

各种可能的机会,开放的平台和数据甚至为普通的开发者开辟了一条致富之路,这是一个

展现个人才华,并以此创富的时代。

本书在这样的背景下,具有一定的实际意义。在很多人都投身下海开启网店,生意做

大甚至开办商城之后,作为开发者的你,有没有嗅到这里面蕴藏着的商机呢?

作者在该书的开篇就介绍了或商业或开源的一些网上商城的特性及优劣,在新的电子

商务模式 B2B2C 以及来自要求集成多方业务数据和遗留系统数据的压力下,现成的商城系

统已经不再能满足拿来主义的需要。这就是这本书存在的意义。

作者凭借自己多年积累的丰富的电子商务领域业务经验,和 Java EE 领域的实践经验,

为解决复杂的集成需求并同时保持系统的灵活性和良好的用户体验,从业务分析、系统设

计,到后台搭建、前端设计,几乎“手把手”地介绍了这样一个 B2B2C 模式的网上商城系

统的搭建过程。开发者可以基于这样的学习,搭建起一个自己的网上商城系统来,很容易

可以集成第三方比如淘宝开放平台的数据,或者租户要求的其他业务系统数据,为租户带

来无线商机可能的同时,也会拓宽自己的致富之路。

张凯峰

InfoQ 中文站

目 录

·XI·

目 录

第一部分 网上商城简介

第 1 章 网上商城与网上商店..............................................................................................2 1.1 网上商城与商店系统的现状........................................................................................2

1.2 现有网上商店与商城系统分析....................................................................................4

1.3 新一代电子商务发展趋势............................................................................................7

1.4 本章小结 .....................................................................................................................10

第2章 网上商城功能需求分析和设计..................................................................................11 2.1 网上商城后台功能需求列表和设计..........................................................................11

2.2 网上商城前台功能需求列表和设计..........................................................................28

2.3 全程电子商务下的网上商城后台设计......................................................................33

2.4 网上商城所需软硬件架构分析..................................................................................33

2.5 本章小结 .....................................................................................................................34

第 3 章 网上商城架构设计经验谈...................................................................................35 3.1 软件架构经验总结......................................................................................................35

3.2 网上商城后台架构设计..............................................................................................40

3.3 网上商城前台架构设计及选型..................................................................................54

3.4 本章小结 .....................................................................................................................55

第二部分 进销存 UI开发技术概述

第 4 章 Flex 实用开发概述................................................................................................57 4.1 Flex 与 Web 应用程序开发技术概览 ........................................................................57

目 录

·XII·

4.2 使用 SDK 开发包开发 Flex 应用程序.......................................................................58

4.3 Flex 与 J2EE 后台交互的三种方式 ...........................................................................60

4.4 Flex 模块 .....................................................................................................................63

4.5 流行 Flex MVC 框架综述 ..........................................................................................66

4.6 本章小结 .....................................................................................................................68

第 5 章 Flex 高级控件使用................................................................................................69

5.1 DataGrid.......................................................................................................................69

5.2 Tree ..............................................................................................................................72

5.3 TabNavigator................................................................................................................74 5.4 组合使用 TitleWindow 和 DataGrid...........................................................................76

5.5 在 DataGrid 上加 Checkbox........................................................................................79

5.6 扩展 Flex 控件 ............................................................................................................81

5.7 本章小结 .....................................................................................................................85

第三部分 构建自己的 SSH架构

第 6 章 创建自己的 Struts 框架 .......................................................................................87 6.1 创建背景 .....................................................................................................................87

6.2 实现自己的 MVC .......................................................................................................89

6.3 本章总结 ...................................................................................................................111

第 7 章 打造自己的 Hibernate .......................................................................................112 7.1 创建存储框架............................................................................................................112

7.2 自动实现多租户........................................................................................................115

7.3 本章小结 ...................................................................................................................119

第 8 章 打造自己的 Spring ..............................................................................................120 8.1 创建简单的业务流程管理框架................................................................................120

8.2 创建复杂的业务流程管理框架................................................................................123

8.3 本章小结 ...................................................................................................................128

目 录

·XIII·

第四部分 网上商城完整实现

第 9 章 网上商城后台之采购管理.................................................................................130 9.1 采购订单的实现........................................................................................................130

9.2 采购入库的实现........................................................................................................163

9.3 采购付款的实现........................................................................................................188

9.4 采购退货单的实现....................................................................................................200

9.5 本章总结 ...................................................................................................................206

第 10 章 网上商城后台之销售管理 ..............................................................................207 10.1 销售订单的实现......................................................................................................207

10.2 销售出库的实现......................................................................................................232

10.3 销售收款的实现......................................................................................................242

10.4 销售退货的实现......................................................................................................248

10.5 本章小结..................................................................................................................256

第 11 章 网上商城后台之库存管理 ..............................................................................257 11.1 直接入库的实现......................................................................................................257

11.2 直接出库的实现......................................................................................................259

11.3 调拨单的实现..........................................................................................................261

11.4 库存台账查询的实现..............................................................................................263

11.5 应付账款查询的实现..............................................................................................264

11.6 应收账款查询的实现..............................................................................................265

11.7 本章小结..................................................................................................................266

第 12 章 网上商城后台之数据字典实现 .....................................................................267 12.1 客户供应商管理的实现..........................................................................................267

12.2 商品维护的实现......................................................................................................269

12.3 商品类别的实现......................................................................................................286

12.4 本章小结..................................................................................................................290

第 13 章 同步淘宝数据.....................................................................................................291 13.1 了解淘宝开放平台..................................................................................................292

目 录

·XIV·

13.2 如何接入淘宝开放平台..........................................................................................295

13.3 同步淘宝数据到本地..............................................................................................305

13.4 同步本地数据到淘宝..............................................................................................315

13.5 查询自己感兴趣的其他淘宝数据..........................................................................318

13.6 本章小结..................................................................................................................320

第 14 章 网上商城前台实现技术...................................................................................321 14.1 模板技术具体实现..................................................................................................322

14.2 实现用户注册..........................................................................................................330

14.3 实现商品展示..........................................................................................................336

14.4 实现购物车..............................................................................................................344

14.5 实现用户中心管理..................................................................................................359

14.6 本章小结..................................................................................................................360

附录 A 电子商务发展简史 ...............................................................................................362 A.1 电子商务发展历史...................................................................................................362

A.2 中国电子商务发展简史...........................................................................................367

编辑后记 ...................................................................................................................................372

第 1 章 网上商城与网上商店

·1·

第一部分 网上商城简介

近几年,伴随淘宝网的兴起,国内大有全民皆商之意,电子商务也借着这股风红得

发紫起来。以 Shopex、ECshop 为代表的为电子商务服务的网上商城软件,也得到了空前

的发展。第 1 章主要列举了目前电子商务 常用的几个网上商城软件,并分析了编写自己

的网上商城软件的必要性;第 2 章讲述了开发自己的网上商城软件前所要做的需求分析,

也就是我们的网上商城系统要做的开发范围,软件要长成什么样;第 3 章是笔者关于网上

商城软件架构的经验之谈。

第一部分 网上商城简介

·2·

第 1 章 网上商城与网上商店

1.1 网上商城与商店系统的现状

电子商务近几年在国内的高速发展加速了为电子商务服务的软件行业的发展,随之

诞生了许多与之密切相关的网店和网上商城系统。对于网上商城的实现技术,无论是开

源的还是商业的,都非常的多。这里说的网上商城系统,指的是支持多商户的大卖场模

式;而网上商店,一般指的是独立 B2C 网站。在软件架构上,两者也是有所不同的,

即一个是支持多租户模式的,一个是不支持多租户模式的。多租户,也是本书讨论的核

心之一。

目前市面上的网上商城和网上商店系统,著名的如上海商派的 Shopex 和 Ecshop 以及

Ecmall,在国内市场占有率可能已经超过 70%,形成一支独大局面。外贸行业使用比较多

的国外的 Zen Cart 网店系统,面向企业级应用 Magento 网店系统。国内比较活跃的网店系

统还有 HiShop、V5Shop 等网店系统。不过,无论是开源的 ECShop、ECMall、Zend Cart、

Magento,还是市场占有率 高的不开源的 Shopex,它们都是基于 PHP 技术,HiShop 和

V5Shop 则是基于.NET 技术。也就是说,无论是国际还是国内市场上,主流网店系统都是

基于 PHP 语言的。PHP 初是 1994 年 Rasmus Lerdorf 创建的,在 1995 年以 Personal Home

Page Tools(PHP Tools)开始对外发表第一个版本, 开始 PHP 的目标是针对个人网页而

不是针对企业及应用,所以,无论是 ECShop 也好,Shopex 也好,使用 多的还是小企业。

当然,在上海商派的不懈努力与开拓下,目前使用 Shopex 的大企业也越来越多了,商派也

在推出自己的“淘系列”,其中包括面向淘宝卖家的“EC-ERP 系统”。此系统居然打上了

“ERP”的标签,不知道用友、金蝶这些传统 ERP 厂商看到了会作何感想。不过,大企业

第 1 章 网上商城与网上商店

·3·

应用 Shopex,仅仅是作为自己独立网上商店的应用,但如何与大企业客户内部已有的 ERP、

管理信息系统做对接,始终是摆在上海商派以及其他网店系统中的一个不可跨越的难题。

我 近也在跟公司其他同事一起为一个大型 B2B 网站开发一套 B2B2C 的网店系统,用户

要开发的 B2B 系统,包括商品目录管理、门店订单管理、配送中心管理、供应商管理等,

这些都是这个系统的核心。数据库设计跟 Shopex 肯定是完全不同的,如果客户前端 B2C

采用 Shopex 的话,与 B2B 的接口谁来做?如何做?而且,客户的 B2B 系统支持分布式的

架构,由 B2C 商城系统同时导数据到很多台分布式机器上是很麻烦的。所以客户 后还是

决定不采用 Shopex 了。这也是我们为什么要做一套基于 Java 和 J2EE 架构的网上商城系统

的原因。

有人也许会问,基于 Java 和 J2EE 平台的独立网店系统平台,不也同样会面临着跟

Shopex 一样的数据库的导入导出问题?这个问题问得很好。其实,基于 Java 和 J2EE 的

网店系统的一个 大好处就是集成问题,它可以跟企业已有的 ERP 系统在架构上进行很

好的集成。规模稍大一些的企业,内部的 ERP 以及其他管理系统采用的一般都会是基于

Java 和 J2EE 架构,比如我们可以采用消息中间件同步网上商城、B2B 系统以及各个门店

的 POS 系统,所以基于 Java 和 J2EE 架构的系统,一般是应用于具有一定规模的企业,

那么这套基于 J2EE 架构的网店系统,也将适用于同样规模的企业。而且各类比较成熟的

商业消息中间件,几乎都是基于 Java 的,企业内部信息系统一般也会是采用 Oracle 数据

库的。

这里并不是讨论哪个语言好,哪个语言开发的软件更有前途的问题,而是针对不同的

市场,我们可以选择不同的开发技术,没有哪一套软件能够包打天下,适用于所有企业。

由于淘宝的高速发展,造就了目前全民电子商务的机会,再加上国内电子商务近两年也在

快速地发展,给了上海商派一个很好的发展机遇。随着电子商务的不断深入,企业应用也

会越来越复杂,与旧系统集成的工作也会越来越多,IT 系统外包不仅包括从国外拿订单到

国内来做,也包括国内的电子商务企业外包它们的 IT 系统给更专业的公司来维护。上海商

派的“分销王”系统就是冲着这个目标去的,不过,还是那句话,它们的系统,更多的是

被小企业采用,如何跟规模较大的企业合作,尤其是基于 B2B2C 架构的系统,则是基于 Java

和 J2EE 系统架构的网上商城系统的机会。

第一部分 网上商城简介

·4·

1.2 现有网上商店与商城系统分析

虽然本书主要讨论的是网上商城系统的实现,由于 Shopex 和 Ecshop 等网上商店系统

在国内市场上的高占有率和和高曝光率,人们往往容易混淆二者之间的区别,所以我们先

概要讨论一下网上商店系统。网上商城与网上商店的区别,顾名思义,商城就是我们常见

的大卖场。网上商城典型的如淘宝商城,场景是 C2C 或者 B2B2C,即多个卖家和多个买家。

商店就是我们常见的独立商场,如苏宁电器商城、国美电器商城等,场景是独立 B2C,一

个卖家,多个买家。网上商店典型的如京东商城、当当网、卓越亚马逊、玛萨玛索等。不

过, 近的发展趋势是独立的 B2C 网店都在往 B2B2C 平台方向转,如京东商城、当当、

卓越亚马逊、凡客诚品、红孩子、麦考林都在出租自己的柜台,套用一句流行的话:哥的

确是在烧钱,但不仅仅是为自己卖东西铺路,哥还在做像传统百货公司那样的销售平台,

可以无限招商。

1.2.1 Shopex 网上商店系统 Shopex 目前已经成为国内市场占有率 高的网上商店系统,这当然得益于上海商派前

期的免费推广策略,通过免费推广,高高占据了网上商店软件市场份额后,不仅大大提高

了后来者进入门槛,也直接造成了在可预见的将来,后来者很难撼动它的霸主地位。

目前 Shopex 的 新版本是 4.8.5,已经可以和淘宝互联互通,大大方便了既在淘宝

上开网店,又有自己独立网店的卖家。Shopex 是在所有的网店软件中,易用性做的是

好的。

不过,由于 Shopex 是用 PHP 编写的系统,它的技术架构大大限制了它和企业管理软

件整合的可能性。往上延伸,它很难跟企业内部信息系统整合在一起,完成 B/E(工厂)

→B(分销商)→C(消费者)的架构。往下延伸,现在的社区软件绝大部分虽然也是 PHP

编写的,架构上倒不存在问题,但是商业上,较大的社区,谁愿意开放自己的数据给别人

呢?另外,阿里巴巴的 1688.com、淘宝、阿里妈妈、口碑网,都没做成全网电子商务,所

以 Shopex 试图通过这款软件,做自己的 B2B,搞一个全网电子商务,个人判断,不容易。

1.2.2 Ecshop 网上商店系统 Ecshop 的创始人是高春辉,关于高春辉,我建议大家用百度搜索一下他的履历,相当的

第 1 章 网上商城与网上商店

·5·

富有传奇色彩。1997 年 7 月开始创办个人网站,成为中国第一个个人站长。1998 年成为中

国十大网民,曾创办卓越网、天下网、手机之家等网站。2006 年 2 月,手机之家与高春辉的

另一家网站 18900.com 被纳斯达克上市公司太平洋商业网络估价 2800 万人民币收购,不久

高春辉逐渐淡出手机之家,并开始创建 Ecshop。2007 年 8 月,Ecshop 在被并入康盛创想一

年之后,2008 年 10 月,Ecshop 又被康盛创想以 200 万美元卖给了上海商派。由于 Ecshop

是免费开源的网店系统,所以使用者也众多(Shopex 免费,但不开源)。

Ecshop 坎坷的历程,注定它发展得比较缓慢,但由于使用者众多,所以上海商派没有

舍得放弃,还在继续升级,目前 新版本是 2.7.2。而 Ecshop 加入康盛创想时候的版本是

2.1.5,经过近 3 年的开发,Ecshop 的主版本信息依然是 2,没有大的变化。

Ecshop 的功能还是很完善的,现在也可以跟淘宝互联互通了。对于淘宝上的大 C 来说,

如果发展势头比较好,建议还是使用 Ecshop,有源代码,找人修改起来比较方便。

1.2.3 Ecmall 网上商城系统 Ecmall 初也是由康盛创想开发的,是基于 Ecshop 的社区版,重写了模板引擎。是允许

加盟的、多店铺的一个系统,即可以有多个店铺、多个商家进行经营活动。在 Ecshop 被康盛

创想卖给上海商派的时候,Ecmall 也一并被卖掉,目前 新版本是 2.2。我们要做的,功能上

就是类似于 Ecmall 这样的网上商城系统,同时后台又加入了面向企业级应用的进销存系统。

1.2.4 其他网上商店、网上商城系统 一、Zen Cart

Zen Cart 也是基于 PHP 和 MySQL 的免费开源的网上商店系统,在国外的用户比较多。

支持 PayPal 支付,内置电子邮件群发功能,支持多语言、多货币。以外贸商品或者以国外

客户为主的企业,建议关注 Zen Cart。

二、国内其他比较活跃的网店系统软件

在国内,其他比较活跃的网店系统软件,如 V5Shop、HiShop 等,在一些领域都有自

己的客户,不过,它们的市场占有率和 Shopex、Ecshop 以及 Zen Cart 网店系统,都是无法

相比的,倒不是输在功能上,而是它们是后来者,消费者先入为主,而且 Shopex 和 Ecshop

始终坚持免费的策略,使得后来者在通用网店软件这块,很难超越它们。要么等到 Shopex

犯错误,要么在其他领域,如外贸领域、分销系统上下工夫,否则,其他网店软件,短期

第一部分 网上商城简介

·6·

之内很难有超越它们的机会。

1.2.5 国际、国内知名电子商务网站分类 电子商务网站一般会分为 B2B、B2C 和 C2C 三大类。B2B(Business to Business)是

指企业对企业之间的营销关系,我个人的理解,就是针对企业的广告发布和产品交易平

台。国外知名的 B2B 网站如美国著名的批发网站 wholesalecentral.com,专门为中小企业

的在线交易服务的 buyerzone.com 等。国内知名的则有阿里巴巴、慧聪网、中国化工网等

网站。

B2C 是英文 Business to Consumer 的缩写,其中文简称为“商家对顾客”。“商对客”

是电子商务的一种模式,也就是通常说的商业零售,直接面向消费者销售产品和服务。这

种形式的电子商务一般以网络零售业为主,主要借助于互联网开展在线销售活动。在美国,

知名的 B2C 电子商务网站无疑是亚马逊。它成立于 1995 年,一开始只经营书籍销售业

务,现在则扩展到很多产品,包括 DVD、音乐光碟、电脑、软件、电视游戏、电子产品、

衣服、家具等。如果亚马逊的故事就到此为止,那么电子商务的传奇也就不那么精彩了,

亚马逊 神奇之处在于它不仅是传统 B2C 电子商务提供商,而且它还像 Google 一样,向

其他企业提供自己的云计算服务。而国内号称媲美美国亚马逊的当当网,除了图书,其他

连形似都谈不上,跟亚马逊完全是两类不同盈利模式的 B2C 公司。其他国内知名 B2C 公

司包括京东商城、卓越亚马逊、凡客网等。

C2C(Consumer to Consumer)则是另外一种电子商务网络交易的方式,它是指个人对

个人的交易形式,著名的网站有美国的 eBay 和国内的淘宝等。

目前,B2C 大都在往 B2B2C 方式上转,而类似 Groupon 和拉手网这样的团购网站,

既不是 B2B,也不是 B2C 或 C2C,他们的出现,又为电子商务更好地往社区化和 SNS 化

方向发展埋下了伏笔。

不同规模的网店的开发要务和重点是完全不同的。淘宝上的大卖家,往往有建立自己

独立 B2C 网店的冲动,对于这样的卖家,开始的时候用 常用的网店建店程序就可以了,

如果发展到京东商城或当当网的规模,则需要完全定制开发,数据库、中间件、负载均衡

和服务器都需要定制优化。而对于传统企业,如果往电子商务上面转的话,则需要全面考

虑目前运行的业务系统与新系统的对接问题。在这种情况下,简单的网店建店程序就不适

合了,需要定制开发。而对于分销为主的企业如果想转型,则需要有 SaaS 这种架构方式的

第 1 章 网上商城与网上商店

·7·

业务系统做支撑。

1.2.6 做自己的 B2B2C 网上商城系统 有的人会问,既然 Shopex 和 Ecshop 短期之内很难被超越,而且它们一直是免费的,

Ecshop 还是开源的,我们为什么还要做自己的网上商城系统?

第一,Shopex 和 Ecshop 是网上商店系统,不是网上商城,它们不支持多个卖家在一

个平台上销售。

第二,除非是小卖家,否则任何一款软件如果直接拿来用,都会发现这款软件并不

太适合自己的实际需求,必须得二次开发或者改造,才能适应自己的需求。所以,这也

是为什么在国内基于 Ecshop 和 Shopex、Zen Cart 网店软件做二次开发的工作室性质的团

队特别多。

第三,Ecmall 虽然支持多租户,而且也是免费开源的,从架构上来说,由于采用

PHP 架构的先天缺陷,他们很难跟企业的内部管理软件深度整合,所以我们要采用

J2EE 架构,做一套自己的网上商城系统,满足企业级网店系统的需求,不仅支持多租

户,而且支持在线分销系统,通过 SOA 架构,跟企业内部管理信息系统做深度集成。

既支持 SaaS 多租户架构,又支持深度个性定制化,这就是我们这个网上商城系统的目

标。

第四,既然我们的客户目标是规模稍大的分销企业或者工厂,而不是只有一个人或者

几个人的小卖家,那么我们的后台系统,就不能做的像 Ecshop 和 Shopex 那样简单,只关

注订单和商品,我们还必须有自己的进销存系统,满足他们日常管理的需求。

第五,Ecmall 虽然是基于多个卖家的网上商城系统,但它毕竟只是网上商城平台,只

是个表现层,我们的目标则是全面整合企业已有业务系统的 B2B2C 运营平台。

1.3 新一代电子商务发展趋势

国内的电子商务发展到现在,已经成为一超多强的局面。一超当然就是阿里巴巴了。

对于中国制造网、中国化工网等上市的 B2B 网络公司,他们也只能在垂直行业发展,综合

性的 B2B 网站,想全面超越阿里巴巴,在可预见的将来,已经是完全不可能了。 近,马

第一部分 网上商城简介

·8·

云在美国接受著名脱口秀节目主持人查理·罗斯(Charlie Rose)专访,围绕阿里的成功之

道、未来方向以及自己的创富心得等内容,马云给出了阿里巴巴在未来的使命是超越微软

和沃尔玛。

所以,现在我们如果还想做一个与阿里巴巴一模一样的一个电子商务网站,基本上可

以断定是没啥可能了。在苹果与微软几十年的竞争当中,在计算机操作系统方面,无论苹

果的技术有多么的友好和完美,也不可能超越微软了。乔布斯当然也看到了这个趋势,所

以弄出了一个面向个人消费市场的四不像 iPad,在个人消费领域,极力诠释苹果文化、苹

果技术和苹果体验,向个人消费领域渗透苹果,而放弃了企业市场。这一理念给苹果带来

了革命性的发展,苹果市值增长迅猛,有望在未来超越目前全球市值 高的埃克森美孚公

司。

所以我们要研究电子商务新一代的发展趋势,沿着旧有的规则出牌,就像苹果在计算

机操作系统领域不可能超越微软一样,我们也不可能超越目前现有的任何电子商务公司。

随着电子商务越来越多地渗透到消费者和企业内部,使得每个传统企业都不得不考虑,自

己在一波一波的电子商务冲击下如何参与其中。

对于企业现有的管理软件,由于互联网和电子商务的发展,无论是原来的 C/S 架构,

还是 B/S 架构,由于企业跟企业之间的联系越来越紧密,用户自己去开发一套 MIS 系统,

显然很难满足整合上下游供应商、分销商和终端的需求了。即使一套系统,满足了自己的

需求,在整个大的供应链当中,也是一个孤岛。所以,对于这类 SaaS(Software as a Service)

技术架构需求,未来还是很强劲的。

目前,一些有远见的公司,已经把目光转向了 B2B2C 的平台。所谓 B2B2C 是一种新

的网络通信销售方式,是英文“Business to Business to Consumer”的简称。第一个 B 指广

义的卖方(即成品、半成品、材料提供商等),第二个 B 指交易平台,即提供卖方与买方

的联系平台,同时提供优质的附加服务,C 即指买方。卖方不仅仅是公司,可以包括个人,

即一种逻辑上的买卖关系中的卖方。平台绝非简单的中介,而是提供高附加值服务的渠道

机构,拥有客户管理、信息反馈、数据库管理、决策支持等功能的服务平台。买方同样是

逻辑上的关系,可以是内部也可以是外部的。B2B2C 定义包括了现存的 B2C 和 C2C 平台

的商业模式,更加综合化,可以提供更优质的服务。

对于 B2B2C,我们在后面也有详细论述。我们网上商城的技术架构,是基于 SaaS 和

B2B2C 的,SaaS 与 B2B2C 的理念,也将贯穿我们整个架构的始终。

第 1 章 网上商城与网上商店

·9·

当然,想通过 B2B2C 全面超越阿里巴巴,也是不可能的,B2B2C 的优劣,我们在后

面也会有详细论述。但是每个企业之间、上下游之间的信息系统的互联互通,则是电子商

务下一个必然的发展趋势。

我们的 B2B2C 技术架构如图 1-1 所示。

图 1-1 B2B2C 技术架构图

关于 SaaS 的理解,现有的定义都是 Software as a Service。如果定位 Software 为主体,

则是用数据隔离的技术架构来作为商业模式,这条路是走不通的。比如,截止到 2010 年 9

月份,据国外媒体报道,德国软件巨头 SAP 虽然早已开始努力发展网络软件业务,但是在

三年的发展历程中,却仅吸引了不足 100 家用户。另外,某知名 CRM 厂商,虽然在垂直

的教育行业有所斩获,但是他们的 SaaS 主营业务 CRM,发展得并不好,SAP 幻想先规范、

统一中小企业日常管理,然后再大规模实施 SaaS 管理软件,沿着这个思路做 SaaS 管理软

件,必死无疑。因为 SAP 是传统 ERP 软件的既得利益者,它不会主动去革自己的命。

如果我们把 SaaS 分为两层理解,第一层,从技术架构上,使用 SaaS 技术手段做数据

隔离,即 SaaS 理解为 Software As a Service,以软件公司为主导;第二层,作为商业模式,

个人认为,SaaS 应该理解为 Service as a software,就是服务软件化,即以有能为客户提供

特有或者垂直服务的公司为主导。如果你能为你的客户提供独有的服务,基于互联网,以

第一部分 网上商城简介

·10·

软件为手段展现你的服务,就全面抛弃了已有的传统软件和传统电子商务的定义,从而找

到自己真正的核心竞争力。

1.4 本章小结

本章对市面上现有常见的网上商城和网上商店系统做了一个简单的论述,并对我们即

将开发的系统做了一个定义:我们开发的网上商城系统,与市面上现有的单纯面向独立 B2C

的网店系统是完全不同的,我们开发的是一个 B2B2C 网上商城平台,而不仅仅是一款类似

Shopex、ECshop、Zen Cart 等网上购物网店系统。

第 2 章 网上商城功能需求分析和设计

·11·

第 2 章 网上商城功能需求分析和设计

2.1 网上商城后台功能需求列表和设计

2.1.1 数据字典功能需求列表和设计 一、客户供应商管理

在一个经典的 B2C 网店系统中,是没有客户和供应商的概念的。所谓客户,就是买自

己东西的公司或者个人,供应商是指自己进货的上游厂家或者经销商。而在 B2C 网店系统

中,所有的顾客都可以看做是个人,很少有顾客常年累月在某一家网店中买东西,当然极

少部分淘宝上的大 C 和京东商城、红孩子、卓越亚马逊、当当这类巨无霸企业除外。所以

像 Shopex 也好,Ecshop、Ecmall 也好,它们是没有客户、供应商这类概念的,它们也只是

有品牌的概念,某个商品属于哪个品牌。而我们要做的,不仅是网上商城系统,而且还是

一个轻量的、企业级的完整进销存系统。虽然轻量,但是麻雀虽小,也是五脏俱全啊。

一个典型的客户、供应商信息,包括:

公司名称、邮箱、联系电话、联系地址、邮编、开户行、银行账号、信用额度、是客

户还是供应商的标志位等信息,如图 2-1 所示。

由于客户和供应商信息基本相同,所以在设计上,我们用一张表表示。并用一个字段

作为标志位,表明这个公司是客户还是供应商。或者这个公司,既是客户,又是供应商。

而对于网上商城注册的个人买家,并不在这个表中维护。这个表维护的是企业,并不针对

个人。

第一部分 网上商城简介

·12·

图 2-1 客户、供应商信息

二、货品管理

货品管理,是一个网上商城系统的核心,因为网上商城的一切活动,都是围绕着买和

卖进行的。买和卖的核心,也就是货品(或者叫商品)。货品管理功能的好坏,将直接影响

到一个网上商城或者网上商店系统的易用性。

我们的货品管理包括下面几个功能。

(1)可以动态增加规格

这个特性,对于服装行业尤为重要,因为某一款服装,仅仅是因为颜色、尺寸的不同,

就能衍生出数十类不同规格的衣服,而价格完全相同。如果货品管理不能支持这个特性,

将会使录入程序极其烦琐,每一个规格都要重新录入一次。这给统计查询也会造成很大的

麻烦,每一个货品编码,都会代表不同的货品,哪些货品仅仅是因为规格不同,统计时要

看做同一个商品来统计。如果不同规格算作不同商品,就会有这个麻烦,这个问题不解决,

至少在服装行业,这个网上商城系统,是不好用的,也没有客户愿意使用。动态增加规格

如图 2-2 所示。

第 2 章 网上商城功能需求分析和设计

·13·

图 2-2 为商品动态添加规格

(2)可以动态增加多幅图片

由于是网上商城,顾客可能实际并没有见过这款货品,那么一个货品,它的图片越多,

供顾客参考的内容就越直观,顾客就可以反复比较和揣摩这个货品是否是自己理想中的商

品,购买后反悔的心理就会减弱,从而增加网站黏性,主界面如图 2-3 所示。

图 2-3 商品维护主界面

第一部分 网上商城简介

·14·

(3)可以动态增加属性

不同类别的货品,它们有一些通用的属性,比如货品编码、货号、计量单位、供货商、

品牌、进货价格、销售价格、期初库存等基本属性。对于网上商城来讲,还有“是否精品”、

“是否特价”等属性。但是不同类别的货品,它们的属性差异还是很大的,比如:服装行业

有尺码、颜色、材料等特殊属性,而一款硬盘,则有容量大小(比如 1000G)、转数(比如

7200 转/5400 转)等特殊属性。不同的货品,属性差异很大,所以我们要有动态增加商品

属性的功能。这个功能,我们将在类别管理里面实现,针对某个货品类别,动态设置好它

的属性后,我们就可以在货品管理里面动态设置属性值了。

货品管理基本信息包括如下几方面。

①货品编码:系统自动生成。

②货品名称:货品的实际名称。

③货号:这里重点解释一下货号。像服装之类的商品,由于颜色、尺码不同,造成货

号可能就是不同的,但是却算作一个货品,统计查询起来比较方便。

④供货商:自己进货的、生产厂家或者上一级分销商。

⑤库房:进货后,把这个货品放在哪个仓库(可以是虚拟的)。

⑥是否上架:由于后台是完整进销存系统,用户不仅使用网上商城系统,还可能混合

使用进销存系统,目的是记账用,并不对外销售,所以加了这个参数。如果不上架,则不

应在网上商城前台显示。

⑦显示比例:用户也许不愿意让网上商城前台看到自己的库存,但是在后台,又想录

入实际库存,作为自己日常管理用,所以加了这么一个参数。

⑧本店零售价:即在网上商城的售卖价。

⑨期初库存:如果填写这个库存,那么这个货品会出现在库房台账里,否则,库存就

是 0,不能对外销售。当然,你也可以利用采购管理的采购入库功能,或者库存管理的直

接入库功能,对货品进行入库操作,从而动态监测某个货品日常管理过程。

其他一些基本信息,我就不一一解释了。

三、货品类别

首先,货品类别是为所有的货品分类,比如:服装大类,又可以分为男装,女装/女士

精品,男女内衣/家居服等很多大类和小类。初次做电子商务的读者,如果对所售商品的背

景知识不是很熟悉的话,我的建议是直接看淘宝或者 1688.com 是如何分类的,或者参考慧

第 2 章 网上商城功能需求分析和设计

·15·

聪网。拿到他们的分类后,直接用就可以了。这么做还有一个好处是,日后跟淘宝集成就

很好集成了,不用再转换。

货品类别首先是树状结构的,如图 2-4 所示。

图 2-4 树状结构的货品类别

其次,货品类别是电子商务网站系统软件的核心。系统的灵活性就直接体现在货品类

别是否支持各类自定义属性,就像我们在“货品管理”功能模块所讲的,每一个货品都要

支持动态属性的添加。那么动态属性的设置,是针对某一类货品,而不是某一个货品。所

以,我们针对某类商品,还必须有动态增加属性的功能,如图 2-5 所示。

再次,我们还可以对动态属性分组,比如对于计算机这个类别,我们可以分为硬盘、

显卡、声卡、主机等组别。对于硬盘,又分为 30G、80G、170G 等动态属性值。

后,某类货品还可以有自己的关联品牌。比如某些箱包,既可以有麦包包,也可以

是 Guess、Coach。

四、库房维护

库房管理,我们这里做的相对简单,就是针对库房本身的增加、修改、删除,并没有

跟数据权限关联上,比如某个保管员只能管某个库房的数据,我们这里暂不考虑那么复杂

的情况。

第一部分 网上商城简介

·16·

图 2-5 货品类别关联品牌页面

库房管理的基本信息包括库房编码、库房名称和备注字段信息,界面如图 2-6 所示。

图 2-6 库房管理基本信息

五、计量单位

计量单位是一个说简单也简单,说复杂又巨复杂的一个功能。如果我们像维护库房管

理那样,简单地维护计量单位的增加、修改、删除和查询功能,本身也没什么新意。但是

计量单位涉及到不同计量单位之间的换算功能,比如我们批发一箱牛奶到库房,然后一袋

第 2 章 网上商城功能需求分析和设计

·17·

一袋往外卖,这就牵扯到计量单位的换算。采购的时候,计量单位是箱,而出库的时候,

计量单位是袋(一袋牛奶),那么库存怎么管理?我们为了容易实现我们的目标,暂时不考

虑计量单位之间的换算问题,因为此问题也会给统计查询带来很大的麻烦。

计量单位的基本信息包括计量单位编码、计量单位名称和备注字段信息,如图 2-7 所

示。

图 2-7 计量单位界面

六、品牌管理

品牌管理,就是对品牌本身的增加、修改、删除以及是否推荐到首页等。在维护货品

类别的时候,我们会把品牌跟某个货品类别关联上,如图 2-8 所示。

品牌管理基本信息包括品牌编码、品牌名称、官方网址、LOGO 链接地址等。

如果选择了推荐到首页,则该品牌会出现在网上商城的首页。

七、规格管理

规格管理,我们在货品维护功能里面,已经实现了动态添加规格,不同的规格,代表

不同的货品,但他们大多数数据又都是一致的。动态添加的规格名称,则在这个功能模块

维护。规格管理如图 2-9 所示。

规格管理的基本信息包括规格编码和规格名称。

第一部分 网上商城简介

·18·

图 2-8 品牌管理

图 2-9 规格管理

第 2 章 网上商城功能需求分析和设计

·19·

八、商城信息发布

商城信息发布功能,是指某个网店系统或者网上商城系统,在首页会有一些动态的新

闻和信息发布,这里我们把商城信息分为商城公告和站内信息两大类,如图 2-10 所示。

商城信息发布的基本信息包括标题、信息内容以及信息类别,比如是商城公告还是站

内信息。

图 2-10 商城信息发布

2.1.2 采购管理功能需求列表和设计 采购管理主要是对进销存模块的采购活动进行日常管理的一个功能模块。比较完善的

采购管理包括采购需求、采购订单、采购入库、采购退货、采购付款等功能模块。还有的

集团化公司是专门有个采购中心,由各分公司或者各个部门,先提出自己的采购需求,采

购配送中心进行招投标,统一进行采购或者叫团购,这样可以压低进货价格,降低采购费

用。统一采购后,再根据各个子公司的采购需求统一拆分,中间还可能发生质量不合格退

货、分批进货等业务过程,也是相当的麻烦,都由进销存系统统一管理的话,理论上可行,

实际开发过程中,会遇到太多的实际情况和特殊情况,这样的业务系统,还是上 ERP 系统

第一部分 网上商城简介

·20·

加强大的二次开发比较好。这种情况,也不在咱们这次开发范围。

下面逐个介绍我们关心的几个功能模块的需求。

一、采购订单

采购订单就是指企业与供应商间的一个购销契约,采购订单可视同企业的订货合同

书。单据中记录了对某个供应商订货时间、供应货品的数量、已收货数量等资料。也有企

业直接就把采购入库单当做采购合同,或者把采购合同单当做采购入库单。进销存难就难

在进销存的个性化非常强,每家企业实际业务都有可能不同,采购订单如图 2-11 所示。

图 2-11 采购订单

采购订单应由两部分组成。一部分包括订单编号、采购日期、供货商、经手人、金额

合计以及备注等基本信息,我们叫做主表。还有一张表,包括采购订单的基本货品信息。

一张采购订单,应该可以采购多个货品,我们在这里叫做子表。主表和子表的关系应该是

1∶N 的关系。

二、采购入库

所谓采购入库就是把采购的货品存放到仓库的业务活动。采购入库其实是一个非常复

杂的活动,对于质量要求比较严格的企业,采购入库之前,还有采购验收的过程,对于货

品的数量,也有发货数量、入库数量、合格数量、不合格数量等。尤其是军工行业,要求

“举一反三”,发现某个批次的货品有问题,要全部往回追溯,所以单据上的数据项比较多,

第 2 章 网上商城功能需求分析和设计

·21·

采购入库单如图 2-12 所示。

图 2-12 采购入库单

跟采购订单一样,采购入库也由两部分组成。一部分包括入库编号、入库日期、供货

商、经手人、金额合计以及备注等基本信息,我们叫做主表。还有一张表,包括采购入库

的基本货品信息。一张采购入库单,应该可以入库多个货品,我们在这里叫做子表。主表

和子表的关系也是 1∶N 的关系。还有的采购入库单,是从采购订单生成而来,我们这里

也是支持的。

对于有很多个分公司的大型企业,一般会有自己的采购配送中心,多个采购需求对应

一个采购合同,多个采购合同,又对应一个采购入库单,再加上退换货,这个业务活动是

非常复杂的,全靠系统去解决问题就需要一套复杂的 ERP 系统,我们这里不去介入这么复

杂的业务活动。仅仅是一张采购入库单,只对应一张采购订单,就可以了。

三、采购退货

所谓采购退货,就是采购来的货品,由于某种原因,退回给原供应商的业务过程。它

是采购入库的逆过程。就像在采购入库里讨论的那样,大型企业采购退货的业务过程,也

第一部分 网上商城简介

·22·

比较复杂,我们这里就不多讨论了,我们只关注采购退货单与采购入库单一对一的情形。

即一张采购退货单,只对应一张采购入库单,但是一张采购入库单,可分多次退货,采购

退货单如图 2-13 所示。

图 2-13 采购退货单

采购退货也由两部分组成。一部分包括退货编号、退货日期、供货商、入库单号、金

额合计以及备注等基本信息。还有一张子表,包括所有退货的基本货品信息。主表和子表

的关系也是 1∶N 的关系。

四、采购付款

采购付款,就是指对于采购的货品,财务上进行付款的过程。对于同一个供应商,我

们一张付款单,可能对应多张采购入库单,所以我们这里的付款单,只跟供应商对应上。

一张采购入库单,也可以分批付款,采购付款单如图 2-14 所示。

采购付款的主表信息包括单据编号、付款日期、供货商、金额合计以及备注等基本信

息。子表包括入库单号、入库金额,本次付款金额,未付金额等基本信息。主表和子表的

关系也是 1∶N 的关系。

第 2 章 网上商城功能需求分析和设计

·23·

图 2-14 采购付款单

2.1.3 销售管理功能需求列表和设计 一、销售订单

所谓销售订单,就是指与客户签订的销售合同。也有公司直接用销售出库单作为销售

合同,销售订单如图 2-15 所示。

销售订单主表包括订单编号、订单日期、交货日期、客户、交货地点、经手人、金额

合计以及备注等基本信息。子表包括订单的基本货品信息。一张销售订单,应该可以采购

多个货品。主表和子表的关系应该是 1∶N 的关系。

二、销售出库

所谓销售出库,就是记录货物出库的业务过程。销售出库后,对应库存则相应减少。

销售出库可能由销售订单而来。前面我们讲过采购中心的复杂例子,我们这里也仅关注

一个销售出库单对应一个销售订单的业务。当然,一个销售订单,可由多次销售出库来

完成。销售出库单也可以独立完成,而不用跟销售订单关联上,销售出库单如图 2-16 所

示。

第一部分 网上商城简介

·24·

图 2-15 销售订单

三、销售收款

销售收款单就是对销售的货品进行收款的业务过程。一个销售收款单,可能对应很多

个销售出库单。每个销售出库单,也可以分多次收款,销售收款单如图 2-17 所示。

图 2-16 销售出库单

第 2 章 网上商城功能需求分析和设计

·25·

图 2-17 销售收款单

四、销售退货

销售退货和销售出库是相反的过程。销售退货指的是卖出去的货品被退回的业务场

景。新增销售出库的时候,先选择客户,然后再选择此客户对应的销售出库单。一个销售

退货单,只能对应一个销售出库单。当然,一个销售出库单,可多次分批退货,销售退货

单如图 2-18 所示。

图 2-18 销售退货单

第一部分 网上商城简介

·26·

2.1.4 库存管理功能需求列表和设计 一、入库单

入库单就是直接入库的单据,不是采购入库。主要是用于一些低值易耗品之类的入库。

也可能是盘点的时候,盘多了,找不到对应的单据,就直接入库了,使得库存达到平衡。

所谓盘点,就是企业定期或不定期地对仓库或者店内的商品进行全部或部分的清点,包括

实物和账务的核对。直接入库,就没有供应商这个概念了,入库单如图 2-19 所示。

图 2-19 入库单

二、出库单

出库单就是直接出库的单据,不是销售出库。主要是用于一些低值易耗品之类的出库。

也可能是盘点的时候,盘亏了,账面上有货品,找不到对应的单据,就直接出库了。使得

库存达到平衡。直接出库,就没有客户这个概念了,出库单如图 2-20 所示。

三、调拨单

所谓调拨单,是指货品在企业内部流通,从一个仓库调拨到另外一个仓库。调拨单需

要先选择调出仓库和调入仓库,选择物资要调出的仓库,对该仓库下的物资进行调拨。在

国内,即使是一个公司,很多部门之间也是独立合算的。一次调拨的过程,就是一次买和

卖的过程,调拨单如图 2-21 所示。

第 2 章 网上商城功能需求分析和设计

·27·

图 2-20 出库单

图 2-21 调拨单

第一部分 网上商城简介

·28·

四、库存台账

库存台账,是对当前库存的货品进行查询统计的一个功能。可以按照货号、商品名称、

商品类别等条件进行查询,库存台账如图 2-22 所示。

图 2-22 库存台账

2.2 网上商城前台功能需求列表和设计

后台我们用 Flex 开发,但是对于前台,我们将回归到 Html,我们将采用 Java 的通用

模板技术来做。Flex 适合开发后台管理系统,对于访问量大的前台页面,每个 Flex 编译之

后,都超过 100K,不太适合做网上商城前台的主界面。

2.2.1 用户注册 用户注册是网上商城 基本的功能,用户是商城生存的基础,没有用户的网上商城,

也就失去了商城生存的基本价值,顶多算是一个产品展示平台。回到主题,对于用户注册,

第 2 章 网上商城功能需求分析和设计

·29·

有两类完全不同的观点,有的认为尽可能简单,一个用户名,一个密码即可。其他例如邮

递地址、电话、邮编、E-mail 等邮递时的必填信息,可以在用户下订单后由用户填写,注

册时没必要增加注册者的心理负担,以免丢失这个用户。另外一个观点就是,注册时尽可

能的麻烦,让用户填写尽量多的信息,才允许注册,知道用户的信息越多,才能为这个用

户提供更好的功能。其实每个观点,都能从现实生活中找到支持或者反对的例子。至于究

竟要怎么做,还必须结合自己的实际。比如,你有很紧俏的货源,Levis 美国卖 30 美元,

国内大商场卖 800 元,你 400 元能保证是真品,那么注册时无论多么复杂,消费者也是愿

意的,给客户节省的是真金白银。而如果你卖的是大路货,价格没什么特别优势,那么注

册界面的麻烦,将很容易失去客户。

说到这里,你也许就会看到,用户注册,这个功能貌似很简单,技巧也还是很多的。

我们这里采用中庸的注册策略,除了用户名和密码,多了一个要填写的 E-mail,主要

是为忘记密码,找回密码使用。用户注册界面如图 2-23 所示。

图 2-23 用户注册界面

第一部分 网上商城简介

·30·

2.2.2 用户登录 用户登录没什么好说的,输入用户名和密码,直接登录即可。不过,登录界面 好有

一个注册功能的入口,为的是消费者选择某款商品下订单的时候,需要弹出登录验证窗口,

如图 2-24 所示,这时增加注册链接,减少消费者在网上漫无目地寻找注册界面的障碍,否

则很可能会失去这个消费者。UE(User Experience),即用户体验,也是一门很深的学问。

图 2-24 用户登录界面

2.2.3 购物车 用户选择某款商品后就可以加入购物车了。为什么不直接进入付款功能呢,因为用户

还可能继续购物,选择其他商品。购物车的实现,也有两类实现方式,一类是把用户的购

物车的数据直接存到数据库里,这样,用户虽然当时没有付款,下次登录或者在另外一台

机器登录的时候,就可以看到自己先前的订单,免得下次登录,忘记买什么东西了,也是

增加用户粘性的一种手法。这类方式的缺陷就是会增加服务器的负担,用户只是下订单而

不付款购买,留下一大堆垃圾数据。还有一类实现方式是把订单数据放在 Session 或者

Cookie 里面。放在 Cookie 里面的话,用户在另外一台机器登录就看不到了。如果用户禁止

了 Cookie,也比较麻烦。两类实现方式,各有各的优缺点。我们这里选择把用户选择的商

品放在 Session 里面。

不过,对于网上商城来说,技术上还有一个很大的难点,那就是一个客户可能选择了

第 2 章 网上商城功能需求分析和设计

·31·

多家网店的商品。对于这个特定客户来说,这可能就是一笔订单,但实际上,有几个网店,

就相当于签订了几个购买合同,所以就有几个订单号,订单跟网店是绑定的。这样就有了

订单的分拆,淘宝就是这么做的,购物车如图 2-25 所示。

图 2-25 购物车

2.2.4 用户中心 用户中心的功能包括修改个人资料(例如登录密码、邮寄地址、联系电话等)、订单

状态提醒,查看自己收藏商品、优惠券、团购信息等功能,如图 2-26 所示。

图 2-26 用户中心

第一部分 网上商城简介

·32·

2.2.5 订单管理 订单管理如图 2-27 所示,用户新增自己的订单后,如果商家还没有处理这个订单,则

应该允许用户取消这个订单。

图 2-27 订单管理

2.2.6 商城首页设计 曾看到一个店商高人的关于首页设计的论点,B2C 店商,输赢在首页的首屏上。这句

话是非常有道理的。首页的首屏, 重要的是什么?性价比好的产品、信任感。如果没有

性价比好的产品,光有信任,是很难产生购买冲动的;如果光有性价比好的产品,而用户

担心被骗,也就不敢在你的网站上下单。因此,首页的首屏必须突出两个重点:一是性价

比高的产品,二是信任。好的首页设计如图 2-28 所示。

图 2-28 首页设计

第 2 章 网上商城功能需求分析和设计

·33·

2.3 全程电子商务下的网上商城后台设计

我在我的博客上,曾提出了“一方的采购订单,是另外一方的销售出库单”的全程电

子商务概念,技术架构上和理论上,都是可行的,但在商业运作上,数据的安全性与可控

性,以及是否有公司愿意花大价钱去投入,都是很难保证的。我这里只是在技术架构上,

提出自己的可行性方案。

SaaS 重要的数据隔离方式有 3 种:独立 Schema、独立数据库和每个业务表上都加

一个 tenantid(租户 ID)来区分。全程电子商务,如果一个用户和另外一个用户数据连接

起来的话,那么第 3 种数据隔离

方案是 简单有效的实现方式。

我们这里也采用每个业务表上都

加一个 tenantid 的方式来做。

这个时候,从商城上下一个

订单,那么业务数据流向就很复

杂了。由于购买者和销售商都是

企业,那么他们的进销存模块的

业务数据流向如图 2-29 所示。

这时,一个采购商的采购进

货单,就有可能是另外一个供应商的销售出库单,完成了一个 简单的“全程电子商务”,

我们的系统,也是支撑这种架构需求的。

2.4 网上商城所需软硬件架构分析

如果是面向淘宝大卖家的一个独立的 B2C 网店系统,我们刚开始是不需要太考虑网站

的软硬件架构的,因为刚开始,我们网站的流量不会那么大。而对于像淘宝、京东商城、

凡客、当当网这些每天流量巨大的网站,无论是软件服务器,还是硬件服务器,都不是一

台机器所能解决的。而且,如果我们自己运营的也是一个在线的 SaaS 系统,比如,面向淘

宝卖家提供在线 SaaS 进销存服务的话,一个淘宝大卖家每天订单的流水会在 500 单以上(卖

图 2-29 进销存模块业务流

第一部分 网上商城简介

·34·

包包每天在 3000 单以上),如果你同时为 3000 家大卖家服务的话,即使只有订单数据,每

天的流水就是 150 万条数据,那么在 SaaS 架构上,我们就需要考虑常用针对数据库级别的

水平拆分和垂直拆分。对一些日志文件,我们还需要考虑是否引入 NoSQL 数据库等。相应

的硬件上,也需要很多台服务器支撑。针对这样的需求,我们在做自己的架构设计的时候,

就需要预先规划好。比如 SaaS 架构 简单的解决方案就是,由于每个租户之间的数据是严

格隔离的,除了个别的如商品分类是共享数据外,其余都是每个租户的私有数据,所以根

据这个特点,我们就可以把这些租户分组,按照一定的分配策略垂直分割到不同机器上。

随着租户的增多,我们只需要简单地增加硬件服务器就可以了。一个典型的服务器的配置

如:4 核 CPU,4G 的内存,支持 RAID0 的 简单的 RAID 服务器,Web 服务器用 Tomcat

就可以了,数据库服务器采用 MySQL,并根据实际情况决定是否采用 NoSQL 数据库作为

自己的备份数据库或者日志数据库。

2.5 本章小结

本章对即将开发的 B2B2C 网上商城系统从功能上做了一个简单的介绍,如果从软件

开发的生命周期来看,这个阶段就是需求分析阶段,列出了所有要开发的功能,我们需要

注意的是前台购物功能和后台进销存功能所采用的技术是完全不一样的,在后面的章节都

有详细的介绍。

第 3 章 网上商城架构设计经验谈

·35·

第 3 章 网上商城架构设计经验谈

3.1 软件架构经验总结

任何一款软件,从无到有,从初级到完善,一般都会经历一个漫长的过程。在这个过

程当中,架构师的水平和软件体系架构本身的灵活性,就会处于一个很关键的位置。太多

的软件,因为架构的问题,造成产品发布日期延迟,或者项目交付工期延迟,给测试、实

施、售后等工作等造成一系列的问题。

还有些情况是因为在同期有很多种竞争技术,由于架构师选择了其中一种技术,而这

种技术,在长期发展过程当中,败给了其他的竞争技术,使得基于这种技术的产品不得不

重新开发。比如,我以前基于 Java Swing 做了一款工作流和 SOA 架构的产品,在世界 500

强的公司当中,很多都是我们这款产品的客户,客户对它的评价也相当高,它曾是公司很

赚钱很核心的产品。但是 Java Swing 在跟 Eclipse 的 SWT 竞争过程中败下阵来,使得我们

不得不基于 Eclipse 插件机制,重新开发了一套功能类似的产品,以方便以后升级。在这个

转型过程当中的损失,是很难计算出来的。再比如,现在火热的移动平台开发,是选择

Android 平台,还是选择 iPhone 平台?是选择 Symbian 平台,还是选择 Windows Mobile 平

台?也是一个很恼人的问题,架构师选错了平台,就可能给公司造成难以估算的影响。

而程序员本身,也会给架构师带来压力和困惑。例如:SSH(Spring、Struts、Hibernate)

架构流行很多年了,很多公司和程序员都拿它来开发,而把自己公司的不是基于以上开发

框架的自主开发框架,称之为“山寨框架”(跟 Struts、Hibernate、Spring 比较起来)。他们

一般信奉拿来主义,不重新发明“轮子”的理念深入人心。我自己面试过无数人,也曾被

无数人面试,问到难度稍微大一些的问题,比如线程、Web 服务器负载均衡以及 Java 垃圾

第一部分 网上商城简介

·36·

回收机制等,一般都回答不出来,或者讲不明白,这就是程序员“重商主义”、“拿来主义”

的弊端,只知道如何使用,对其原理一概不知。说实话,不查资料,很多我也不明白,也

是只知道个大概。客观原因是平时很少使用,主观原因是自己的懒惰。我个人是非常不赞

同“不重新发明轮子”这个理念的,不重新发明轮子,你就不知道这个轮子的架构机制。

对很多问题的细节,被人问起来,也只能很含糊地说:也许大概可能是,不过恐怕不见得,

其实这还是没有掌握。而这些机轮子本身,会用到缓存、多线程等很多需要深入研究的问

题。弄明白了别人的轮子,那些很含糊的问题,一般也就解决了。

很多程序员,包括工作十几年的所谓老程序员,都自觉不自觉地遵循着某种理念,比

如他会告诉你“Action 只能调用一个 Manager,所有与数据库打交道的地方,只能写到 DAO

层”,如果你不这么做,他就会告诉你,你的代码不是面向对象的,不友好的,是不符合某

某设计模式的。还有的所谓很牛的程序员,用垒鸡窝和盖大厦,来标榜他的架构是多么多

么符合所谓的范式和架构,或者遵循某大师的 XP 理论。可是他们都忘记了,我们编写程

序代码的 根本的目的是什么?我们开发软件并不是为了让它面向对象化,或遵循某些设

计模式,我们开发软件是为了解决问题。所谓为了以后的扩展或者 10 年之后的需求,你的

架构到时候真的就不用做任何修改吗?我这么说的意思,不是鼓励大家不遵循任何开发的

方法论,不做任何代码的约定,随意随心乱写,毕竟我们是一个团队作业,而不是自己一

个人在根据自己的喜好开发个人软件。我的意思是说,我们不要迷信什么,山寨架构也好,

官方架构也好,我们一定要根据自己项目的实际情况,加以改进,不要生搬硬套。

后摘抄某大侠在 dzone.com(http://java.dzone.com/news/object-orientation-not-goal)一

段原话做结尾:通常,当有些很精明的程序员对我的做法说三道四,却又根本不知道我是在

解决什么问题时,我很苦恼。这就像是在说“我比你更知道你需要什么,所以我们不能用这

个、那个工具/风格/架构”。但事实却是,什么样的工具或思路才是他们解决相应问题所需要

的,这些精明的思想家并不比每个开发人员更清楚。做个类比,这极其类似那些精明的政治

家宣传自己 知道人们究竟应该怎么活着……

3.1.1 经典架构 S(Struts)S(Spring)H(Hibernate)的优

缺点 Struts、Spring、Hibernate 能流行这么多年经久不衰,自然有它的道理。J2EE 先出

第 3 章 网上商城架构设计经验谈

·37·

现的时候,我们一般是采用 JSP+Servlet+JavaBean+EJB 的架构,尤其是 1998 年~2000 年

这段时间,互联网的泡沫从兴起到破裂,其波澜壮阔程度,丝毫不亚于 2008 年开始的这次

经济危机,在那个年代,是否掌握 EJB 开发技术将直接决定你的薪水能否翻一倍或者几倍。

不过,Spring 的作者 Rod Johnson 在 2002 年根据多年经验撰写了《Expert o-ne-on-One J2EE

Design and Development》,其后又发表了著名的《Expert o-ne-on-one J2EE Development

without EJB 》 一 书 , 则 彻 底 地 改 变 了 传 统 的 J2EE 一 统 天 下 的 开 发 架 构 , 基 于

Struts+Hibernate+Spring 的 J2EE 架构也逐渐得到人们的认可,甚至在大型的项目架构中也

逐渐开始应用。下面我们分别说说 Spring、Struts 和 Hibernate 的优缺点。

Spring 是一个开源框架,是为了解决企业应用程序开发复杂性而创建的。框架的主要

优势之一就是其分层架构,使得每个层之间和类与类之间的关系,由原来的强绑定与强耦

合转变为不绑定和松耦合,直接面向接口编程,把设计与实现相分离的原则发挥到极致,

对于单元测试也产生了很深远的影响。在 Spring 出现之前,如果某个模块没有完成,做单

独模块的单元测试还是很困难的。Spring 同时也是 J2EE 应用程序开发的集成框架,因为

J2EE 是讲究分层理念的,Spring 使得 J2EE 每个层之间的模块职能更加清晰。

不过,对于大型项目的开发,Spring 使得原来难以维护的类与类之间的强耦合关系,

转变为更加难以维护的 XML 文件配置,这个工作量也是非常巨大的,而且更容易出错。

而且,随着每个应用模块的升级,这些模块之间的版本,也不会是同步更新的,对于同一

个公共组件,有的模块用的可能是 1.0 版本,而另外一个功能模块用的可能是 2.0 版本,可

怕的是 1.0 版本和 2.0 版本之间,可能还不兼容,Spring 对于这些需求,就无能为力了。所

以,有人说 Spring 不适合大型项目开发,也是有一定道理的。 近 Spring 也加入了 OSGI

标准的实现,也是为了解决不同版本之间同时存在的这些问题。不过,随着 Spring 加入的

功能越来越多,Spring 也就失去了轻量开源框架的特点,变得越来越笨重。

虽然 Spring 现在也支持了所谓的免配置,可以通过@Autowired 标签自行绑定,还可

以通过<property name="packagesToScan" value="com.demo.entity" /> 设置自动绑定加载所

有的 Hibernate 对象,但是如果这些上百个或者数十个中的任何一个 Entity 对象加载失败,

则整个 Spring 服务就启动不起来了,这与难于部署的 EJB 有啥区别呢?而且,令人可笑的

是,由于使用了@Autowired 标签,相当一部分开发人员不再面向接口编程了,对于 Class A

的实例,美其名曰由 Spring 自行绑定,接口也好,实际实现类也好,就在 Spring 配置一下

就可以了。而 Spring 核心的就是面向接口编程和 IOC,没有了面向接口编程,用一个

第一部分 网上商城简介

·38·

A a=new A() 来实例化一个 Class,有什么不可以呢?少写了一行代码,引入了一个重量级

的 Spring,究竟为啥使用 Spring 呢?

对于 Hibernate 的流行,则是由于开发人员和客户,对于 Entity EJB(实体 EJB)臃肿

的身材及部署的困难,是在极度失望情绪下造成的。既然是轻量级解决方案,那么分布式

就不是可选项,没有分布式,那么 EJB 就无用武之地了。话又说回来了,Rod Johnson 前些

年就因为强调绝大部分企业应用是不需要分布式的,从而推出了自己轻量级的 Spring 解决

方案。但是 近一年,随着云计算架构的兴起,架构是否支持分布式,又是必选项了。技

术架构的选型,就跟法国巴黎流行时装一样,今年流行短袖,明年流行下摆,真是典型的

十年河东,十年河西。所以,像 SOA、云计算、SaaS、物联网这些大名词,不仅会给客户

带来很大的困惑,同样也会给程序员、系统分析师、系统架构师、技术总监带来困惑。

从肯定到否定,再到自我否定,真是符合大自然螺旋式上升的发展规律。

而对于 Struts,它一经推出,几乎打败了当时的所有竞争对手。Struts 的伟大之处,在

于它对网页数据的动态绑定。虽然数据绑定不是一个新名词,微软在 1991 年推出 Visual

Basic1.0 的时候,就创造性地发明了让 VB 程序员又爱又恨的数据绑定,但是对于 Web 编

程,Struts 也还是把数据绑定首次应用到了 Web 编程上。它能够让人们用 Set 和 Get 的形式

取得网页数据,而不是单一的黑盒式的 request.getParameter(),从而使得网页数据取值进入

面向对象(OO)化时代。

Struts、Hibernate 以及 Spring 本身都是制作精良的框架,但是对于自己产品和项目的

应用,一经整合在一起,却不一定很适用。比如说,对于数据库相关的 MIS(管理信息系

统)系统中,增加、修改、删除、查询功能是 基本、 常见、必不可少的。对于这些

基本的功能,不同的架构师,则会做出不同的选择。有的架构师,选择了自动生成的理念,

做一个代码自动生成器,设计好数据库表结构,单击一个脚本,或者用 Eclipse 插件的形式,

做个图形化生成界面,自动生成 SSH 框架,完成数据库的增加、修改、删除、查询操作。

这么做的好处是数据库修改了,代码自动生成就可以了,使得程序员不用再维护这些无聊

的代码。不过缺陷也是致命的,一是随着 Struts、Hibernate、Spring 的升级,这个工具也不

得不跟着升级,而做这个工具的程序员,可能早就离开公司了,后续版本无法维护;二是

如果有的业务逻辑跟这些生成的代码有交叉,数据库变更后,代码也无法再次生成了。三

是公司的系统架构,则被严重限制在 SSH 架构基础上,再也无法改变。有人会问:即使限

制在这三种架构上,有何不好吗?假设有客户问,你的框架支持云计算吗?你总不能说“由

第 3 章 网上商城架构设计经验谈

·39·

于 Struts、Hibernate、Spring 不支持云计算架构,所以我也不支持”以此取得客户谅解吧。

因此,依赖于第三方架构的产品体系架构,随着时间的推移,受到的限制会越来越大。

3.1.2 构建自己的 Framework(银弹) 《没有银弹》(No Silver Bullet)是 IBM 大型电脑之父佛瑞德·布鲁克斯(Fred Brooks)

在 1987 年发表的一篇关于软件工程的经典论文。该论文中强调由于软件的复杂性本质,使

得真正的银弹并不存在;所谓的没有银弹是指没有任何一项技术或方法可使软件工程的生

产力在十年内提高十倍。布鲁克斯 为人所知的是在 1975 年所出版的《人月神话》(The

Mythical Man-Month)一直被称为软件工程圣经。

虽然说布鲁克斯大师早已经给我们下了结论:软件开发,没有银弹。但是我们还是想

设计出一套经典的架构,使得以后随着技术的升级和客户需求的变化架构的代码变动量

低。如何做到这一点呢?那就是你做的架构,目的一定要明确,要有明确的取舍,它的确

不是万能的,不可能满足所有的需求,但一定要能满足某种目的和某类需求,并且随着时

间的推移,有很强的生命力和很强的可扩展性,就像 Eclipse 插件开发体系架构一样,经久

不衰,历久弥新,并形成自己一套完整商业生态链。

顺便说一下,架构(Architecture)和框架(Framework)是完全不同的两个概念。框

架是一种特殊的软件,它并不能提供完整无缺的解决方案,而是为你构建解决方案提供良

好的基础。框架是半成品,典型的框架是系统或子系统的半成品;框架中的服务可以被

终应用直接调用,而框架中的扩展点是供应用开发人员定制的“可变化点”。一句话:框架

是软件。而架构不是软件,而是关于软件如何设计的重要决策。软件架构决策涉及如何将

软件系统分解成不同的部分、各部分之间的静态结构关系和动态交互关系等。经过完整的

开发过程之后,这些架构决策将体现在 终开发出的软件系统中;当然,引入软件框架之

后,整个开发过程变成了“分两步走”,而架构决策往往会体现在框架之中。架构与框架的

区别如图 3-1 所示。

从图中可以看出,引入软件框架之后,整个开发过程变成了“分两步走”,而架构决

策往往会体现在框架之中。或许,人们常把架构和框架混为一谈的原因就在于此吧。

第一部分 网上商城简介

·40·

图 3-1 架构与框架的区别

3.2 网上商城后台架构设计

3.2.1 视图层选型 关于视图层技术的选择,很多年来,也是争议颇多的一个话题。对于选择.NET 技术的

公司来说,这个问题还是很好选择的,跟着微软就可以了。微软阵营的问题是选择太少,

出了问题不知道怎么办,只能网上找控件,碰到收费的控件,又爱又恨, 后还是放弃,

不了了之。痛恨微软和别人的不开源,自己的代码却从不给人看,这是微软阵营的特点。

如果说微软阵营抱怨封闭不开源,选择太少的话,那么 J2EE 阵营 大的问题则是选

择太多了,不仅普通程序员无法从纷繁复杂的各类开源框架中做出正确的选择,即使对于

系统架构师这类老手,从数十种开源框架中,找到符合自己公司和项目特点的那个,也是

相当挠头的。比如说现在 流行的 Ajax 开源框架 JQuery,仅 基本 常用的 DataGrid 控

件,解决方案就有十几个。如何从这十几个 DataGrid 控件中选择 合适的一个,也就是所

谓的“银弹”,是相当的痛苦啊。当然,开源的源代码也多得是,可惜的是绝大部分程序员

从来不看别人的源代码,虽然张口闭口大家都在谈着 Struts、Spring、Hibernate,有几个人

把他们的源代码读过一遍?甚至开发文档阅读过一遍?

我教给大家一个框架的秘诀,如果自己实在不知道如何选择,那就遵循两个原则:一

第 3 章 网上商城架构设计经验谈

·41·

是是否得到 IBM、Oracle、微软这类大型软件公司的支持;二是别问为什么,就选择大家

常用的,随大流就可以了。

对于 UI 这一层的选择,J2EE 阵营有 3 类选择。

(1)Ajax:包括 JQuery、ExtJs 以及 ZK 等 Ajax 框架。国内的用友、金蝶、阿里软件

等和其他一些传统管理软件转过来的公司,一般会采用 ExtJs 作为自己视图层解决方案。

(2)Flex:Flex 终生成的还是 Flash,Flex 的出现,是真正的富客户端解决方案(Rich

Internet Application)。界面基于标准的 XML 标签,非常华丽,再加上功能强大的

ActionScript,不仅适用于传统 MIS 开发,而且还可以开发网络游戏,像著名的网络游戏开

心农场,就是基于 Flash 开发的。不过,Flash 也面临巨大的挑战。首先是没有得到风头正

猛的苹果公司的支持,Flash 在移动开发领域,也遇到了强大的阻力。如果说苹果公司只是

给了 Adobe 背后一刀,尚未伤其筋骨的话,那么微软在 新的技术路线图中,则明确表示

IE9 也将不再支持 Flash,那么微软会不会给 Adobe 致命的一击?微软和苹果的理由是它们

都将只支持 HTML5.0。

(3)JSF:JSF 一般在大公司使用,比如 Oracle 经典的 J2EE 开发框架。不过,在国内

由 JSP 转向 JSF 的公司并不多见。

我这里给个建议,对于进销存 MIS 系统的开发,如果考虑跟以前的兼容,首选是 ExtJS,

其次是 Flex, 后是 ZK 和 JBoss 的企业级开发框架 Seam,而对于网站类开发,则只选择

JQuery 就可以了。

为什么我们还选择 Flex 作为自己的 UI 层技术解决方案呢?首先我们可以排除的是

JSF,JSF 仅有少数公司在用,JSF 是用 Swing 的解决思路去解决 Web 难题,Swing 首先就

在桌面领域败给了 SWT,在 Web 领域,这个失败的技术架构,同样也没有得到大家的认

可。那么在 ExtJs 和 Flex 之间如何选择呢?我之所以选择 Flex,是因为 Flex 支持 Restful

风格的技术架构。Flex 跟后台的通信机制,事实上有 3 种:Remote Object、Web Service 和

HttpService 组件。第三种 HttpService 组件,就是 Restful 风格的。采用 Restful 后,后台接

收数据的代码跟前台技术无关,这套代码同样可适用于 ASP、JSP、PHP,只要它们支持

Restful 就可以了,真正可以做到 mushup(混合语言)编程,这就是我所说的自己的“银弹”。

当然,选择 ExtJS 你可以选择 JSON 格式的数据进行前后台通信。

如果 IE 以后真的不支持 Flash 了,这个还真的是一个问题,好在我们的架构,跟视图

层无关,视图层是随时可以替换掉的,目前来看,只有 Flex 对 Restful 支持得 好,所以

第一部分 网上商城简介

·42·

我们选择 Flex 作为自己的技术解决方案,而且不存在浏览器不兼容的问题。IE、Chrome

以及 Firefox 等主流浏览器都是支持 Flash 的,而且 Adobe 也做了一个 Flash 转化为 HTML

格式的工具,Flash 一样可以在 iPAD 上运行。

3.2.2 构建自己的 Struts 本节我们将模仿 Struts 解决问题的思路,设计出自己的一套 MVC 框架。

首先,我们先提出自己的需求,在满足这个特定需求前提下,再设计自己的“山寨轮

子”。在提出自己的需求之前,我们要先明白使用我们这个框架的用户是谁。明确了自己的

用户,才能更好地满足他们的需求,为他们服务。在这里,我们假设框架的用户就是自己

公司或者对这个框架感兴趣的程序员。在明确了这个框架的使用者之后,我们再来看看它

要满足的功能上的需求。

Struts 是一个 MVC 框架,视图层没有疑问了,指的是 JSP、ASP、PHP 这类可以动态

生成 HTML、XML 或其他格式文档的 Web 网页的语言。上面我们提到,Struts 的伟大之处,

在于它用 Set 和 Get 这类面向对象的技术来存取网页数据,而不是以前的统统都是黑盒式

的 request.getParameter()方法。我们采用的是 Restful 技术架构,所以,对于 View 层,我们

是没有自己特定选择的,JSP、ASP、PHP、Flex,理论上都可以满足,前提是这类脚本语

言必须得有 Restful 风格的框架支持。Flex 内置实现了 Restful,所以我们这里采用 Flex 作

为自己视图层的解决方案。对于 Model 层,我曾经面试过上百位程序员,问 Struts 的 Model

层指的是什么,回答正确的不到 30%,Struts 的 Model 层,其实就是 ActionForm,Struts 的

MVC 仅仅是经典 J2EE 框架 Web 层的 MVC 框架,所以它的 Model 层并不是指的 J2EE 的

多层框架中的 Model 层。

我们要重点介绍的是控制(Control)层,控制层是我们这个框架的核心。Struts 的控

制层其实是一个过渡层,它把网页数据自动转化为 ActionForm,拿到了数据,我们再写自

己的商业逻辑,通过多种运算,把结果保存到数据库当中。

由于我们要做的是多租户的 SaaS 架构的 MVC 框架,所以与 Struts 要满足的用户需求

还是非常不一样的。多租户意味着用户业务逻辑和界面有可能是不一样的。标准的进销存

和网店系统,往往屏蔽掉了 终客户的特性需求和行业特点,试图一套软件走遍天下,这

是不符合国情的,也是目前 SaaS 软件难以普及的一个重要原因。软件公司试图引导客户标

准化作业,我认为是行不通的。我们将从另外一个角度,从技术架构上解决 SaaS 架构下客

第 3 章 网上商城架构设计经验谈

·43·

户个性化定制的问题。

对于视图层,我们并不限制客户个性化定制自己的页面,所以,我们的 Model 层,一

定不是有确认属性的 ActionForm,我们必须支持动态的属性添加。这里我们选择 HashMap。

读者看到这里,可能会有疑惑,Struts 的 ActionForm 把我们从黑盒中解放出来,我们这次

为啥又进入了一个黑盒 HashMap 当中?这句话问得好,这叫有破有立,为了要满足个性化

的需求,所以我们要屏蔽确认的属性方法,只能用 HashMap。否则,我们就不能满足这个

特性。

基于以上需求和原则,我们设计出自己的 MVC 架构。先看增加、修改、删除功能的

实现,代码如下所示。

<?xml version="1.0" encoding="UTF-8"?>

<mappings>

<url-mapping action="simplecreate.do" class="com.softbnd.jetblue.saas.

webframework.action.SimpleCreateAction"/>

<url-mapping action="simpleupdate.do" class="com.softbnd.jetblue.saas.

webframework.action.SimpleUpdateAction"/>

<url-mapping action="create.do" class="com.softbnd.jetblue.saas.webframework.

action.CreateAction"/>

<url-mapping action="update.do" class="com.softbnd.jetblue.saas.webframework.

action.UpdateAction"/>

<url-mapping action="suspend.do" class="com.softbnd.jetblue.saas.webframework.

action.SuspendAction"/>

<url-mapping action="simplesuspend.do" class="com.softbnd.jetblue.saas.

webframework.action.SimpleSuspendAction"/>

<url-mapping action="remove.do" class="com.softbnd.jetblue.saas.webframework.

action.RemoveAction"/>

<url-mapping action="querycountry.do" class="com.softbnd.jetblue.saas.

webframework.action.LogonQueryAction">

<query SQL="select * from country "/>

</url-mapping>

<url-mapping action="queryprovince.do" class="com.softbnd.jetblue.saas.

webframework.action.LogonQueryAction">

<query SQL="select * from province order by seq "/>

</url-mapping>

<url-mapping action="querycity.do" class="com.softbnd.jetblue.saas.

第一部分 网上商城简介

·44·

webframework.action.LogonQueryAction">

<query SQL="select * from city where provinceno='?' order by seq"

/>

</url-mapping>

</mappings>

其中,url-mapping 标签表示我们把 http 的 URL 转换为 Action。我们也要满足增加、

删除、查询和修改动作的自动化。对于单表的增删和修改,我们定义几个缺省的类。

(1)CreateAction 类:满足单表的数据新增功能。

(2)UpdateAction 类:满足所有的修改功能,包括主子表连带的修改。

(3)RemoveAction 类:满足所有的删除功能,包括主子表连带的删除。

再看查询功能的实现,代码如下。

<url-mapping description="查询入库单" action="queryfeedin.do" class="com.

softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select feedin.*,DATE_FORMAT(entrydate,'%Y-%m-%d') entrydate2,

warehousename from feedin,warehouse where feedin.warehouseno= warehouse.

warehouseno and feedin.tenantno in ('#tenantno#') and warehouse. tenantno in

('#tenantno#','0000000000')">

<subquery SQL="select feedinlineitem.*,goodsname,gtname ,puname

from feedinlineitem,goods,packageunit,goodstype

where feedinlineitem.goodsno=goods.goodsno and goods.goodsunitno=

packageunit.puno and goods.goodstypeno=goodstype.gtno

and feedinlineitem.feedinno='?' and feedinlineitem.tenantno in

('#tenantno#') and goods.tenantno in ('#tenantno#') and packageunit.tenantno

in ('#tenantno#') and goodstype.tenantno in ('#tenantno#','0000000000')"/>

<parameter requestname="date1" name="entrydate" type="Date" op="

&gt;=" or=" feedin.status='0' "/>

<parameter requestname="date2" name="entrydate" type="Date" op=

"&lt;=" or=" feedin.status='0' "/>

<parameter requestname="feedinno" name="feedinno" type="String"/>

<parameter requestname="status" name="feedin.status" type="String"

op="="/>

</query>

</url-mapping>

第 3 章 网上商城架构设计经验谈

·45·

这是一个满足单表查询功能的标签。其中 parameter 是跟 UI 层查询参数密切相关的。

UI 层有多少个查询条件,这里就有多少个查询标签一一对应。Class 则代表这个查询 action

的实现是默认的 LogonQueryAction 类,LogonQueryAction 类能满足 80%的查询需求。

Query 标签定义的是查询 SQL 语句,查询条件能跟 parameter 自动匹配上,从而动态

生成 SQL 语句传给后台,并把结果放在一个 HashTable 表里。其类图如图 3-2 所示。

图 3-2 查询类图

以上这些类只是标签的具体实现,我们还需要一个 runtime 引擎把这些标签串起来,

其类图如图 3-3 所示。

其中,ControlServlet 类负责截获所有后缀是 abc.do 或者 abc.action 的请求,并通过

URLMappingsXmlDAO 类,把 XML 文件解析为 SubQuery、Link、Parameter、Query 等标

签,跟具体的 SQL 对应起来,并把请求转换到 LogonQueryAction 类,LogonQueryAction

类负责动态解析 SQL,把 Parameter 标签里面的参数动态转换为具体的查询语句,并交给

后台的 DAO 处理,还包括分页查询等功能,从而自动实现一次完整的查询动作。

由于我们是 SaaS 架构的,面向的是多租户,所以还有一个数据隔离的问题。对于数

据隔离的实现,也有好几种架构可供选择。比如独立 Schema,或者每个业务表增加一个租

第一部分 网上商城简介

·46·

户 ID 等。如果你使用的是 Oracle 数据库,你的应用程序可能不需要做什么改变,只需要

利用 Oracle 数据库的特性,就可以实现多租户下数据隔离的策略,前提必须是 Oracle9i 以

后的版本,并且绑定到 Oracle 数据库之上。在这里,我们用 简单的方法实现数据隔离,

就是每个业务表增加一个租户 ID(tenantid)。所以,我们每个 SQL 语句里面,每个表都会

多出一个 tenantid。也许有的读者会问,这么做个性化是满足了,但是性能上会不会有影响?

是的,我们又回到了上面所述的“银弹”原则,当我们满足某个特定需求的时候,必然是

以牺牲另外的特性为代价的,解析 XML 文件,动态调用 Java 类,肯定没有直接的 Java 静

态编译和调用速度快。

图 3-3 runtime 引擎

我们可以用增加机器来解决这个问题,由于每个租户之间的数据,理论上没有任何关

联的可能(当然,现在流行全程电子商务,也许你的采购订单,恰好是另外一个系统的销

第 3 章 网上商城架构设计经验谈

·47·

售出库单,我们暂时不考虑这种可能)。所以,只要我们的机器足够多,服务器的成本是有

限的,在经济规模上,我们的架构也是可行的。

我们再看一个复杂的例子,支持主子表的查询的例子,其 UI 界面如图 3-4 所示。

图 3-4 查询 UI 界面

对于主子表的支持,是进销存等系统中 常见的一个功能。比如一张采购入库单,

一个主单据,可能会采购多项商品,下面的代码将展示我们的架构是如何支持类似这样

的需求的。

<url-mapping action="querypo.do" description=" 查 询 采 购 订 单 " class="

com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select purchaseorder.*,custsupp.csname as custsuppname

from purchaseorder,custsupp where purchaseorder.custsuppno=custsupp.csno order

by purchaseorder.pono and purchaseorder.tenantno in('#tenantno#') and custsupp.

tenantno in('#tenantno#')">

<subquery fk="pono" SQL="select polineitem.*,gtname, puname,

goodsname

from polineitem,goods,packageunit,goodstype where polineitem.

第一部分 网上商城简介

·48·

goodsno=goods.goodsno

and goods.goodsunitno=packageunit.puno and goods.goodstypeno=

goodstype.gtno

and polineitem.pono='?' and polineitem.tenantno in('#tenantno#')

and goods.tenantno in('#tenantno#') and packageunit.tenantno in('#tenantno#')

and goodstype.tenantno in('#tenantno#','0000000000') " />

<parameter requestname="podate1" name="podate" type="Date"

op="&gt;=" or=" purchaseorder.status='0' "/>

<parameter requestname="podate2" name="podate" type="Date"

op="&lt;=" or=" purchaseorder.status='0' "/>

<parameter requestname="pono" name="pono" type="String"/>

<parameter requestname="csname" name="custsupp.csname" type=

"String"/>

</query>

</url-mapping>

请读者注意代码里的 subquery 的标签,这个 subquery 就是子表的 SQL 语句,通过关

键字 fk="pono"跟主表绑定到了一起。

3.2.3 打造自己的存储框架 Hibernate 的出现,使得程序员可以使用轻量级的面向对象的方法来存取数据库。跟

Struts 使用面向对象的方法存取网页的创新一样,虽然 Hibernate 不能说是创新,因为 EJB

才是 先采用面向对象的方法存储数据到数据库的,但是 Hibernate 给出了轻量级的实现,

它还有自己的缓存和懒加载机制。

不过,Hibernate 要求每个实体表都必须对应一个实体 JavaBean 和一个.hbm XML 文

件,虽然 JDK1.5 以后,由于 JDK1.5 支持 annotation 元数据注释,.hbm XML 不是必需的

了,但是一个实体表对应一个实体 JavaBean 还是必需的,因为它要支持缓存,没有实体

JavaBean,缓存就很难准确匹配。

但是我们要支持多租户,支持每个租户定义自己的 UI、数据库和业务表逻辑,即理论

上,我们支持每个租户的数据库设计可以完全不一样,但是中间层代码不会发生任何变化,

这就要求我们不能采用 Hibernate 作为自己的存储方案。所以我们采用了 HashMap 作为前

后台数据传输的载体,这就屏蔽了数据库表结构不一致带来的问题。这时读者可能会发现

如下的问题。

第 3 章 网上商城架构设计经验谈

·49·

(1)我们一直说采用面向对象方法存取网页、存储数据库的好处,你这里采用

HashMap 这样一个黑盒子,不是又回到了原始社会了吗?

(2)采用 HashMap,你是如何解决缓存加载机制的?

说实话,采用目前这种解决方案,是会有以上两个问题的,我们解决了多租户架构与

数据库无关的问题,但是却牺牲了面向对象存储特性,造成单元测试和整体测试的黑盒,

数据库表结构修改的话,也会很难检查错误。对于缓存机制,由于个人时间和精力有限,

一直也没有找到很好的缓存机制。

不过我们的系统是进销存,数据要求必须实时更新,而不像论坛系统,可以读写分离,

所以,对于这种实时性要求很高的系统,并且使用者独立查询操作又不是很多(针对同一

个租户的数据来说),缓存是否必需以及如何增加缓存,这都是多租户架构与传统网站架构

以及 MIS 系统架构不同的地方。

3.2.4 打造自己的业务流程控制框架 Spring 是一个通用性的 J2EE 框架,而我们做的进销存系统,是一个业务特征很明显

的业务系统。不同租户的业务逻辑可能完全不同,比如有的租户是在新增采购订单的时候

就会有应付账款,而有的租户是在采购入库的时候才会有应付账款。还有的租户,是在采

购入库的时候货到付款,同时有付款的业务过程。这些我们都是要支持的,允许不同租户

有自己不同的业务逻辑,而我们的程序框架又不会发生任何变化,这是传统 Spring 所解决

不了的,我们的框架是解决具体问题的,两个框架,解决的场景也完全不同。

3.2.5 加入自己的标签 对于通过 XML 文件来做配置文件,我们是又爱又恨,比如 Spring 框架,虽然自己号

称是 IOC 容器,使得类和类之间的强耦合变成面向接口的松耦合,不过,你却不得不维护

一大堆没有实际意义的 XML 文件,使得类和类之间的关系可能更复杂,原来你还可以通

过编译期间直接发现问题,如果项目一大,类足够多,类与类之间关系足够复杂,一旦出

错,你想找到究竟哪个 XML 文件配置错了,也不是很容易的事情。

但是 XML 也给我们带来足够的便捷和强大,使得我们的框架具有理论上的无限灵活

性。这里我们也采用 XML 文件作为配置文件来实现我们的以下目标。

(1)把查询 SQL 语句从各类 DAO 中拿出来,放在 XML 文件里面,并使查询条件可

第一部分 网上商城简介

·50·

以动态配置,也就是说,我们通过一个基类,来处理 95%以上的查询需求,而不用修改任

何代码。

(2)实现各个租户之间的业务逻辑分离的动态配置,支持每个租户差异化的 UI 和业

务逻辑,动态指向自己的 UI,动态调用自己的业务逻辑代码。

(3)支持主子表关联查询。

(4)缺省新增代码的实现,我们用一个基类,完成 70%以上新增存储自动化,包括主

子表存储自动化,我们大部分 Flex 界面,直接调用这个标签就可以实现存储自动化了。对

于 JSP 界面,目前只能是单表实现存储自动化,主子表尚不能实现。

对于一个采购入库单,我们用到了以下标签。

<url-mapping action="querypurchaserk.do" description="查询采购入库单"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select purchaserk.*,custsupp.csname as custsuppname,

warehousename from

purchaserk,custsupp,warehouse where purchaserk. custsuppno= custsupp.

csno and

purchaserk.warehouseno=warehouse.warehouseno and purchaserk. tenantno

in

('#tenantno#') and custsupp.tenantno in ('#tenantno#') and

warehouse. tenantno in

('#tenantno#','0000000000') order by purchaserkno ">

<subquery SQL="select a.*,specvalues from (select

purchaserklineitem.*,goodsname,goodstype.gtname,packageunit.

puname

from purchaserklineitem,goods,packageunit,goodstype

where purchaserklineitem.goodsno=goods.goodsno and goods.

goodsunitno=packageunit.puno and

goods.goodstypeno=goodstype.gtno

and purchaserklineitem.purchaserkno='?' and purchaserklineitem.

tenantno in ('#tenantno#') and goods.tenantno

in ('#tenantno#') and packageunit.tenantno in ('#tenantno#',

'0000000000') and goodstype.tenantno in

('#tenantno#','0000000000') ) as a left join goodspecreference on

a.goodsno=goodspecreference.goodsno and

a.goodsbn=goodspecreference.goodsbn and goodspecreference.tenantno

第 3 章 网上商城架构设计经验谈

·51·

in ('#tenantno#') "/>

<parameter requestname="date1" name="purchaserkdate" type="Date" op=

"&gt;=" or=" purchaserk.status='0' "/>

<parameter requestname="date2" name="purchaserkdate" type="Date"

op="&lt;=" or=" purchaserk.status='0' "/>

<parameter requestname="purchaserkno" name="purchaserkno" type=

"String"/>

<parameter requestname="custsuppname" name="custsupp.csname" type=

"String" />

<parameter requestname="status" name="purchaserk.status" type="String"

op="="/>

</query>

</url-mapping>

标签的解释如下。

(1)query:表示这是一个查询操作。

(2)class:表明这个查询具体实现类是 LogonQueryAction,我们 95%以上的查询都将

使用这个类,查询基本做到自动化。

(3)SQL:表示查询采购入库单主表对应的 SQL 语句。

(4)subquery:表示查询采购入库单子表对应的 SQL 语句。

(5)parameter:表示查询的参数对。

还有其他参数,例如 requestname 和 name 等,在相应的章节,我们还会详细解释,这

里就不多说了。

这个查询,对应的 UI 界面如图 3-5 所示。

我们再看一个新增采购入库单的例子,看看如何定制自己的业务逻辑。

<url-mapping action="createpurchaserk.do" description="新增采购入库"

class="com.softbnd.jetblue.saas.scm.purchase.action.PurchaseRkAction"> <businessprocess description="新增采购入库">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.

PurchaseChckinStorageLedgerAction"/> <component name="updatePurchaseOrder" step="2" description="修改

采购订单到货数量"

第一部分 网上商城简介

·52·

class="com.softbnd.jetblue.saas.scm.purchase.action.

UpdatePurchaseOrderArrivedQTYAction"/> <component name="purchasepayment" step="3" description="新增应付款"

class="com.softbnd.jetblue.saas.scm.finance.action.

PurchasePaymentAction"/>

</condiation>

</businessprocess>

</url-mapping>

图 3-5 查询 UI

在这个例子中,对于“新增采购入库单”这个业务活动,新增采购入库单后,我们还

将继续以下几个步骤。

(1)修改库存台账,货物入库了,库存量当然会增加;

(2)一张采购入库单,可能分多次到货,我们此时回填采购入库单上已经到货数量,

方便以后日常统计查询;

(3)新增应付账款,买了货物,应付账款当然要增加。

有的读者会说,如果有的客户,没有新增应付账款这一步,该如何办?我们看到,这

些业务逻辑都包含在一个 businessprocess 的标签里面,而一个 businessprocess 标签又对应

一个唯一的 Action,而一个 Action 又可以在菜单中和视图层中配置,菜单就可以对应租户,

第 3 章 网上商城架构设计经验谈

·53·

不同的租户有不同的菜单,这样,对于有独特业务的租户,我们可以配置他自己的菜单,

从而 终形成他自己的 Action。比如,我们有一个客户叫 ABC 公司,我们就可以为它配置

一个 createABCpurchaserk.do,对应业务逻辑如下。

<url-mapping action="createABCpurchaserk.do" description="新增采购入库"

class="com.softbnd.jetblue.saas.scm.purchase.action.PurchaseRkAction"> <businessprocess description="新增采购入库">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.

PurchaseChckinStorageLedgerAction"/> <component name="updatePurchaseOrder" step="2" description="修改

采购订单到货数量"

class="com.softbnd.jetblue.saas.scm.purchase.action.

UpdatePurchaseOrderArrivedQTYAction"/>

</condiation>

</businessprocess>

</url-mapping>

这时,新增采购入库单后,我们只有两个步骤,修改库存台账和修改采购到货数量,

对应的 UI 如图 3-6 所示。

图 3-6 修改台账的 UI

第一部分 网上商城简介

·54·

3.3 网上商城前台架构设计及选型

3.3.1 网上商城前台模板设计及选型 我们做的网上商城系统,也是支持模板技术的。对于一般的网站首页,一般会分为上

中下结构,中间部分,又分为左中右或者左右结构,上面的部分是 head.html,下面的部分

是 foot.html 或者 bottom.html,一般都会作为公共部分被包含在其他网页当中。而中间的部

分,左面一般叫 left.html,也可能作为公共部分共享。

这里,我选用的是使用 多的 Ecmall 模板。首先,Ecmall 模板对于网上商城常见的操

作,都已经封装得很好了;其次,它的 JavaScript 功能确实很强大;第三,Ecmall 模板是

支持 if else 以及 for 循环之类的代码嵌套的,它有自己的一套模板解析技术。而我们将沿着

Ecmall 的设计思路编写自己的 Java 模板解析技术。

3.3.2 网上商城前台模板解析技术选型 Ecmall 是用 PHP 语言编写的。而 PHP 是有很成熟的模板技术的,毕竟它是为网站而

生。反观 Java 和 J2EE,它是为企业应用而生的,比如讲究什么应用服务器、消息中间件和

负载均衡。对于 Web 应用,它的发展是很迟缓的。对于基于 Java 面向 web 应用的模板技术,

2000 年左右,仿照 PHP,还真出过几个开源的模板引擎,不过以后就没怎么发展了。至今依

然不断升级版本的,著名的只有 Velocity 和 FreeMarker 两家了。但我并没有选择它们,主要

是我想找更轻量级的模板解决方案,他们架构都太大了。经过千挑万选,我选择了 JDynamiTe。

选择之后,才发现它不支持 include,郁闷我好几天,在它基础之上,只好自己开发了功能不

完美的 include 标签,留给读者自己去完善了。

Ecmall 模板是支持 if else 以及 for 循环之类的代码嵌套的,后来发现 JDynamiTe 却不

支持,所以我只好写在 Java 代码里。打算以后采用新的模板解析技术,来重写模板功能。

这里,我要重点推荐一下 Apache 的 Click 项目(http://click.apache.org/)。Apache Click 的

HTML 模板也是由 Apache Velocity 模板引擎处理,但在 Velocity 基础之上,又大大作了

简化处理。

第 3 章 网上商城架构设计经验谈

·55·

3.4 本章小结

本章是对开发面向 MIS 应用的开发框架的个人经验总结。需要说明的是,它仅仅是个

人经验的阐述,写出来与大家分享,但它毕竟不是真理,每个项目都有自己的特殊性,所

以,写出来也仅能供各位读者参考而已。其实,本人在和团队成员一起开发项目的时候,

也 常常是 极其 郁闷的 ,因 为很多 时候 ,并不 是自 己说了 算的 。很多 读者 ,纠结于

Action->Manager->DAO 的固定模式和固定思路,并且认为事务只能从 Manager 发起,每个

Action 只能做控制用,所有的业务逻辑和事务的发起只能写在 Manager 类里,他们说的对

吗?其实,我都工作十几年了,我也是从经典的 IBM/Sun 的分层 J2EE 架构学起的,早在

2003 年,我就在 IBM DeveloperWorks 网站上发表了如何简化 Struts 开发的文章,谁还不了

解这些所谓原则问题?不是说这些理论不对,而是我们要具体情况具体分析。我也为此曾

苦恼了很久,为啥别人非得死搬这些这些经典理论呢?如果你不按照这些经典理论写,他

们就给你扣帽子,说你写的不对,不会写程序,呵呵。后来,我也终于想明白了,我是为

自己写代码,花的是自己的精力,自己的钱;而很多人则是为公司写代码,为客户写代码,

花的是公司的钱,所以,他们会更纠结于理论上和书面上的东西。

第二部分 进销存 UI 开发技术概述

·56·

第二部分 进销存UI开发技术概述

因为我们的进销存程序的 UI 都是基于 Flex 开发的,而绝大部分程序员并不熟悉 Flex

开发,所以这部分我们主要讲解 Flex 相关的开发技术,为后面开发完整的软件打下良好的

基础。第四章主要讲解 Flex 开发环境和 Flex 如何与 Java 通讯;第五章主要讲解 Flex 常用

的高级控件以及这些控件的组合使用,并演示他们在进销存中的开发实例。

第 4 章 Flex 实用开发概述

·57·

第 4 章 Flex 实用开发概述

对于网上商城后台的进销存部分,我们是用 Flex 技术来实现的。Flex 终也会编译为

SWF 的 Flash 格式的文件。本书并不是 Flex 的开发指南,所以基于 Flex 技术的开发方法,

将不作为本书讨论的重点。本书重点讲述一下进销存部分中所用到的一些界面是如何用

Flex 来实现的。

4.1 Flex 与 Web 应用程序开发技术概览

对于 MIS(管理信息系统)类的应用,比如 ERP、进销存、人力资源管理、档案管理

等企业应用软件,前些年 常见的 Web 应用程序开发技术一般就是 JSP 加一些自定义的

JavaScript 开发包,来处理常见的比如 DataGrid、Tree、ComboBox 等常见控件的需求。不

过,由于是自己开发的 JavaScript 库,或者从网上找到的是一些只解决部分问题的 JavaScript

开发库,所以 Web 性能和开发过程中出现的 Bug 都很难克服。

近几年,ExtJS 在面向 MIS 类的应用中异军突起,远远把其他比如 Yahoo 的 YUI 等

抛在了后面。由于它是 UI 解决技术 JavaScript 的统一解决方案(Total Solution),所以就慢

慢处于垄断地位了,国内绝大部分公司,例如用友、金蝶等面向 Web 类的应用,都选用了

ExtJS。不过,由于其商业许可证是需要收费的,而且性能上,也有加载慢的问题,所以,

其他 Ajax 解决方案,例如 ZK、jQuery,也有不少人在用。事实上,jQuery 在面向 Web 类

应用的程序(但不是面向 MIS 类应用)中,由于其轻量和易于上手,性能也不错,所以也

基本处于垄断地位。但在面向 MIS 类企业应用类型场景中,jQuery 其实并不是很适用,虽

然,jQuery 有模仿 ExtJS 的代码库 jQuery easyUI 代码库,经过我们在一个项目中的实战,

第二部分 进销存 UI 开发技术概述

·58·

证明它是很不成熟的,不建议在自己的产品和项目中大规模使用。

Flex 天生就是为企业应用开发的。它不仅有成熟的 DataGrid 控件,对于 Tree、

ComboBox、DataWindow 等企业开发应用常用控件,以及他们之间的组合使用,也是异常

稳定的(使用了 jQuery esay UI 之后的感悟),我自己的经验是,Flex 更加适合 ERP、进销

存、CRM、人力资源管理、网上分销系统这类企业管理类应用软件,而 jQuery 适合交互性

不强的 Web 站点类的开发,比如 B2C 网上商城前台、B2B 电子商务网站前台、网站流量

统计查询等 Web 应用。

4.2 使用 SDK 开发包开发 Flex 应用程序

4.2.1 编译 Flex 应用程序为 SWF 文件 很多程序员喜欢使用 IDE 工具来开发 UI 应用,比如使用 MyEclipse 来开发 JSP 应用,

使用 FlashBuilder 来开发 Flex 应用等。如果是出于调试的目的,使用这些 IDE 工具也是未

尝不可的,毕竟能够带来很大的便利,不但能节省开发、调试时间,还可以使用快捷键弹

出 API,方便 API 的理解和调用、上下文查询等。这些都是可以理解的。但我个人很不喜

欢庞大的集成 IDE 工具,我建议除了使用基本的 Eclipse 作为 Java 代码开发外,另外可以

使用一个只有几百 K 的 Sysdeo 公司出版发行的 Tomcat 插件单步跟踪调试我们的代码。对

于 Flex 的编译,我建议使用免费的 SDK 开发包,以命令行的方式,编译 Flex 为 SWF 文件,

因为只有手动去处理编译、打包、发布等一系列过程,才能真正明白 Java 和 Flex 到底是如

何结合在一起,如何协作的。

下面谈谈如何用命令行的方式编译 Flex 文件为 SWF 文件。

(1)从网上下载 Flex SDK 开发包。 Flex SDK 新版本是 4.0,下载网址是

http://www.adobe.com/cfusion/entitlement/index.cfm?e=flex4sdk。

我们还可以从这个网址下载和 Flex SDK 相关的一些有趣的工具和开发包,下载网址

是 http://www.adobe.com/devnet/flex/?view=downloads。

(2)下载完 Flex SDK 后,直接解压到一个目录就可以了。本书中的代码还是使用的

3.3 版本,主要是 Flex 4.x 版本编译出来的.swf 文件比 3.x 编译出来的.swf 文件大不少,而

第 4 章 Flex 实用开发概述

·59·

本书中未用到 4.x 的特性。

(3)修改 flex-config.xml 文件,指定 Action Script 源文件编译目录。flex-config.xml

文件在 Flex sdk 包里的 frameworks 目录下,主要修改的地方如下。

<source-path>

<path-element>F:/web/tomcat-6.0.18/webapps/eshop/WEB-INF/as</path-element>

<path-element>F:/web/tomcat-6.0.18/webapps/eshop/console/components

</path-element>

</source-path>

<static-link-runtime-shared-libraries>false</static-link-runtime-shared-

libraries>

source-path 指明的是我们编译 Flex 文件的时候所用到的 ActionScript 源代码路径。这

里我设置了两个路径:WEB-INF/as 路径和 console/components 路径。console/components

路径下的文件,主要是编译公共的.mxml 文件,如选择商品、选择供应商之类的公共的界

面。而且 Flex 也是可以作为公共组件一起编译的。

WEB-INF/as 一般是 ActionScript 脚本,一般是工具类的 ActionScript 源代码,例如日

期转换函数、Flex 分页代码、ComboBox、DataGrid、Tree 等常用 Flex 内置组件的扩展。

<static-link-runtime-shared-libraries>false</static-link-runtime-shared-libraries>说明是否使

用 RSL(Runtime Share Library)。false,则表明我们使用 RSL,这可以大大减少把 Flex 编译

为.swf 后文件尺寸的大小,一般来说至少可以减少三分之二以上。当然,我们把它设置为 false

之后,则我们的运行环境,则必须把 framework_3.3.0.4852.swz、framework_3.3.0.4852.swf 文

件带上,发布在我们的 Flash 相应的目录下。对于 Flex SDK 4.x,则需要 framework_4.x.swz、

framework_4.x.swf 文件。

(4)修改系统环境变量 Path 文件路径,把 mxmlc.exe 文件加到 PATH 路径下。

C:\Java\jdk1.6.0_20\bin;F:\web\flex_sdk_3\bin;%PATH%

(5) 后一步,就是编写.bat 脚本了。

mxmlc -o=Warehouselist.swf Warehouselist.mxml

用 mxmlc 把 Warehouselist.mxml 文件编译为 Warehouselist.swf。

第二部分 进销存 UI 开发技术概述

·60·

使用 SDK 一样很简单吧。知道了 SDK 的编译过程,能使我们在使用 Flex 开发的时候,

不仅知其然,而且知其所以然。图 4-1 就是一个功能模块的目录结构。

图 4-1 功能模块的目录结构

其中 build.bat 的内容是:

mxmlc -o=Warehouselist.swf Warehouselist.mxml

顺便说一下,对于 Flex 文件,我使用的编辑器是 Edit Plus,小巧,简单。对于.mxml

文件,语法一样可以高亮,把 mxml 文件选择为 jsp 语法就可以了。

对于初学者,我建议使用 Flex SDK,按照上述步骤搭建自己的 SDK 开发环境。而对

于 MXML 语法和 MXML 组件,我推荐 Adobe 的官方文档:Adobe_Flex_2.0.1_Language_

Reference.chm,虽然是 2.01 的版本,但是对于绝大部分 Flex 组件已经足够用了。为啥不推

荐 Flex SDK 4.x 版本呢?当然,版本越高,代表功能越强,还能修复以前版本的 bug,

关键的就是 4.x 版本编译出来的 SWF 文件,体积太大,让人有点儿抵触,抵消了升级新版

本带来的好处。

4.3 Flex 与 J2EE 后台交互的三种方式

Flex 与 J2EE 后台进行交互的方式主要有三种:Remote Object、Web Service 和内置

的 HttpService 组件。比较流行的 Flex Framework,一般都会采取 Remote Object 的方式。

第 4 章 Flex 实用开发概述

·61·

Remote Object 调用速度快,还可以像 DWR 那样,与 Java Object 直接交互,还可以与 Struts、

Spring 进行整合。但我没有选择 Remote Object,因为咱们做的是一个 SaaS 系统,我希望

这个架构足够灵活,与具体的表结构无关,而 Remote Object 则要求绑定到一个明确的 Entity

Java 类,例如一般都会选择跟 Hibernate 的 Entity 绑定起来。

对于 Web Service,针对 Flex 项目,则没看到大规模与后台交互的使用案例。一谈起

Web Service,第一印象就是调用复杂,占用带宽,性能有问题。不过,据我所知,一些大

的通信公司的短信收发的对外接口也只有 Web Service 一种方式,所以,Web Service 在性

能上也许并没有大家想象得那么慢。

本书中采用了 Flex 内置的 HttpService 组件与后台交互。HttpService 支持 XML 格式

与后台进行数据交互,而 XML 格式则屏蔽了具体的实体类信息。我们只需要传递 XML

格式的数据到后台,后台有专门一套框架去解析这个 XML 格式的数据,然后再转换为我

们自定义的 TableObject 对象, 后把 TableObject 对象序列化到数据库,其流程如图 4-2

所示。

我们的框架,其实是与任何 UI 技术无关的,理论上,也是支持 JSP、ASP 与 PHP 的。Flex

传递 XML 格式数据过来,JSP、PHP、ASP 传递 JSON 数据过来,统一解析为 TableObject

对象,再把 TableObject 对象序列化到数据库。当然,Flex 也可以传递 JSON 数据到后台。

而且这个框架,对 JSP 程序的支持已经测试通过,也已经商用到一个公司的网站上

(http://www.hahadai.com),是一个做个人信贷消费的网站。

图 4-2 HttpService 后台交互流程图

HttpService 对象的用法一般如下: <mx:HTTPService id="srv" url="querypo.do" method="POST" resultFormat="e4x"

第二部分 进销存 UI 开发技术概述

·62·

result="resultHandler(event)" fault="faultHander(event)">

<mx:request xmlns="">

<podate1>{DateUtil.getStringDate(podate1.selectedDate)}</podate1>

<podate2>{DateUtil.getStringDate(podate2.selectedDate)}</podate2>

<pono>{qpono.text}</pono>

<csname>{qcustsuppname.text}</csname>

<currentpage>{currentpage.value}</currentpage>

<pagesize>19</pagesize>

</mx:request>

</mx:HTTPService>

<mx:HTTPService id="createaction" url="createpo.do" contentType= "application/

xml" result="refresh(event)"/>

<mx:HTTPService id="updateaction" url="update.do" contentType= "application/

xml" result="refresh(event)"/>

<mx:HTTPService id="removeaction" url="remove.do" contentType="application/

xml" result="refresh(event)"/>

ID 为 srv 的 HttpService 对象,代表一个查询操作,参数为 podate1、podate2、pono、

csname、currentpage、pagesize,一共六个参数。分别代表的意思是:订单开始日期、结束

日期、订单号、供应商名称、当前页(分页用)、每页显示多少条记录(这里缺省设置每页

显示 19 条记录)。

ID 为 updateaction 的 HttpService 对象,代表新增操作。ID 为 createaction 的 HttpService

对象,代表修改操作,ID 为 removeaction 的 HttpService 对象,代表删除操作。

url=" querypo.do"代表查询操作的实现是 querypo.do 来完成的,而 querypo.do 的定义,

则用到了第 3 章第 3.2.2 节“构建自己的 Struts”的知识,定义如下。

<url-mapping action="querypo.do" description="查询采购订单" class= "com.

softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select purchaseorder.*,custsupp.csname as custsuppname

from purchaseorder,custsupp where purchaseorder.custsuppno=custsupp.csno order

by purchaseorder.pono and purchaseorder.tenantno in('#tenantno#') and

custsupp.tenantno in('#tenantno#')">

<subquery fk="pono" SQL="select polineitem.*,gtname, puname,

goodsname

from polineitem ,goods,packageunit,goodstype where polineitem.

第 4 章 Flex 实用开发概述

·63·

goodsno=goods.goodsno

and goods.goodsunitno=packageunit.puno and goods.goodstypeno=goodstype.

gtno

and polineitem.pono='?' and polineitem.tenantno in('#tenantno#') and

goods.tenantno in('#tenantno#') and packageunit.tenantno in('#tenantno#') and

goodstype.tenantno in('#tenantno#','0000000000') " />

<parameter requestname="podate1" name="podate" type="Date" op="&gt;="

or=" purchaseorder.status='0' "/>

<parameter requestname="podate2" name="podate" type="Date" op="&lt;

=" or=" purchaseorder.status='0' "/>

<parameter requestname="pono" name="pono" type="String"/>

<parameter requestname="csname" name="custsupp.csname" type="String"/>

</query>

</url-mapping>

从中不难看出,querypo.do 的实现类是 LogonQueryAction,参数的定义是 parameter

标签,而且还支持子查询,分别表示一条采购订单主记录和多条采购订单子记录。

createpo.do、update.do、remove.do 定义如下。

<url-mapping action="createpo.do"

class="com.softbnd.jetblue.saas.webframework.action.CreateAction" description="新增采购订单"/>

<url-mapping action="update.do"

class="com.softbnd.jetblue.saas.webframework.action.UpdateAction"/>

<url-mapping action="remove.do"

class="com.softbnd.jetblue.saas.webframework.action.RemoveAction"/>

这些增加、修改、删除操作,我们都是用的通用的新增、修改、删除类实现的。这就

是从 TableObject 序列化到数据库的实例。

通过我们自己的 Struts 框架和 HttpService 对象,我们就把前后台联系起来了。

4.4 Flex 模块

我们的进销存模块,全部是由 Flex 开发的,其中有一些功能模块,比如供应商的选择、

第二部分 进销存 UI 开发技术概述

·64·

商品的查询等公共功能,都是可以作为公共的模块拿出来,放在一个目录下共享。比如采

购订单的供应商的选择,界面如图 4-3 所示。

图 4-3 采购订单供应商的选择

在采购入库模块中供应商的选择,我们用到了同样的界面和功能,如图 4-4 所示。

图 4-4 采购入库中的供应商选择

第 4 章 Flex 实用开发概述

·65·

所以,我们可以把“选择供应商”作为公共模块,以模板的形式共享出来。在采购订

单和采购入库中“选择供应商”的代码如下。

<mx:HBox width="100%"> <mx:Label text="供应商(*)" width="10%"/>

<mx:PopUpButton id="custsuppno" data="{dataObject.custsuppno}" label=

"{dataObject.custsuppname}" openAlways="true" width="35%" textAlign="left">

<mx:popUp>

<common:Custsupp/>

</mx:popUp>

</mx:PopUpButton>

</mx:HBox>

而对于<common:Custsupp/>的定义,我们可以用命名空间:

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" horizontalAlign= "center"

width="100%" height="100%" fontSize="12" creationComplete=" onCreationComplete();"

xmlns:common="common.*" >

来指定。xmlns:common="common.*" 就是指定 common 开头的命名空间是在 common 目录

下。而我们在 4.2.1 小节编译 Flex 应用程序为 SWF 文件,指定了我们 Flex 源代码的其中一

个编译目录为:

<path-element>F:/web/tomcat-6.0.18/webapps/eshop/console/components</path-el

ement>

其目录结构如图 4-5 所示。

mxmc 程序会自动去 F:/web/tomcat-6.0.18/webapps/eshop/console/components 目录下寻

找 common 目录,并把 common 目录下的 flex 源文件编译到采购订单和采购入库单的源文

件当中去,就像 JSP 程序的 include 指令一样。

common 的目录结构如图 4-6 所示。它有四个共享功能模块,分别是选择客户功能模

块、选择供应商公共模块、通用选择商品公共模块和为销售模块定制的选择商品功能模

块。

第二部分 进销存 UI 开发技术概述

·66·

图 4-5 编注目录结构图

图 4-6 Common 的目录结构

4.5 流行 Flex MVC 框架综述

基于 Flex 的开发,其实也已经有很多年了,所以基于 Flex 的框架,开源的和商业的

解决方案,也是层出不穷,其混乱程度就跟 J2EE 和 JQuery 面临的问题一样,解决方案太

多,令人无法下手。

基于 Flex 的框架,常见的有 Blaze、LCDS(LiveCycle Data Services)、Cairngorm、Mate、

第 4 章 Flex 实用开发概述

·67·

PureMVC、Model-Glue 和 EasyMVC 等。对于这些 Flex 框架,估计很少有人每个框架都去尝

试一遍。

(1)关于 Blaze 和 LCDS 的区别,InfoQ(http://www.infoq.com/cn/articles/Blaze-LiveCycle)

上有一篇文章介绍的很清晰:

Blaze Data Services——免费、开源的版本。

LiveCycle Data Services 社区版——Blaze DS 的一个支持版。

LiveCycle Data Service 单 CPU 协议版——商业版的一个免费版本,具有一些额外特性,

但只能用在单个 CPU 上。

LiveCycle Data Services——商业版的付费版本。

有兴趣的读者可以去那个网址详细比较一下这 4 个版本的区别。

(2)关于 Cairngorm,Cairngorm 也是 Adobe 的一个广为人知的老牌 Flex 框架,看到

没有,Adobe 自己维护了 Blaze、LCDS、Cairngorm 三个完全不同的 Flex 框架。需要写大

量的类应该是 Cairngorm 多的负面评论了。

(3)Mate 是一个基于标签的、事件驱动的 Flex 框架。

(4)Pure MVC 顾名思义,肯定是借鉴了 Struts 的 MVC 框架的思路,分为 Model、View

和 Command 三部分。总的来说,Pure MVC 是把传统的 MVC三层,利用 Observer和 Command

模式进行了解耦,View、Control、Model 也细化成 UI、Mediator、Command、ModelProxy、

Model。另外 ApplicationFacade 负责配置模块之间的映射关系和初始化。

其他的 Flex 框架我就不细细描述其区别了,大家看到这里,是不是头大了?不知道到

底该选择哪个框架了吧?我也是,比较了好半天,也不知道哪个 Flex 框架是自己的“银弹”。

其实,以我的经验,差钱的,我们只需要掌握 Blaze 就好了,不差钱的,可以关注 LiveCycle

Data Services Enterprise Suite 版本,其余版本的 Flex 框架,大家就不要关注了。我之所以

没有选择 Blaze,是因为所有这些框架,几乎都是基于 ActionScript 消息格式(AMF)使用

远程过程调用和消息与服务器端进行通信,使用了 Remote Object,而我的目标是通信的时

候与具体的实体类无关,不打算绑定任何对象,走的全部是 Restful Http 协议,所以把这些

框架全部放弃了,而单单基于 HttpService 组件,做了一套适合 SaaS 架构的自己的 Flex 框

架,也就是本书中所讲的网上商城解决方案的框架。

Flex 数据通过 Restful Http 协议传输 XML 格式数据到后台,解析为 TableObject 对象,

然后在 Java 层面把 TableObject 对象序列化到数据库。查询操作则是一个逆过程,把数据从

第二部分 进销存 UI 开发技术概述

·68·

数据库中查询出来,把 SQL 转化为通用的 TableObject 对象,再把 TableObject 对象序列化

为 XML 数据,然后在 Flex 界面显示出来。

4.6 本章小结

本章对基于 Flex 开发所必需的技能做了一个简单的介绍,因为是为网上商城后台进销

存服务的,所以我们并没有过多地讲解 Flex 本身的开发方法和基本概念。

第 5 章 Flex 高级控件使用

·69·

第 5 章 Flex 高级控件使用

这一章我们主要讲述网上商城后台进销存程序所用到的一些 Flex 高级控件。例如:

DataGrid、Tree、自定义的 ComboBox,自定义的带 Checkbox 的 Tree 等控件。还是那句话,

本书并不是 Flex 的开发指南,我们只讲我们所用的一些控件的属性、方法、技巧性的东西,

以及使用这些控件的属性、方法的背后商业场景。

5.1 DataGrid

DataGrid 是做管理软件的必备控件,也是管理软件的核心控件。夸张点儿说,DataGrid

控件直接关系到一个管理软件开发的成败。Flex 为我们提供了强大的 DataGrid 控件,这也

是我选择 Flex 的主要原因。 近几个月,一直在做一款商业模式是 B2B2C 的电子商务管

理软件,团队选择了 JQuery Easy UI 作为核心 UI 技术,用了几个月之后,我发现选 JQuery

是没问题的,但是如果选择基于 JQuery 技术的 UI 做管理软件的开发,好比是拿着鬼头大

刀去砍百年老树,鬼头大刀虽锋利无比,那是砍人的,却不是拿来砍树的。而且基于 JQuery

Ajax 框架的几十个 UI 解决方案,包括各类 DataGrid,夸张点儿说,几乎都是不成熟的,

Bug 太多。如果真的是要做管理软件 UI 方面的解决方案,建议在 Flex 和 ExtJS 之间选择一

个。

比如一个商品的维护界面就是一个典型的 DataGrid,如图 5-1 所示。

第二部分 进销存 UI 开发技术概述

·70·

图 5-1 商品维护界面

在 Flex 中,一个 DataGrid 的定义如下。

<mx:DataGrid id="maindatagrid" width="100%" height="240" headerStyleName=

"DataGridHeaderStyle" resizableColumns="true"

dataProvider="{saleOutstockEntrys}" horizontalScrollPolicy=

"on" rowCount="10" doubleClickEnabled="true"

itemClick ="maindatagridClickEvent(event);" itemDoubleClick=

"showDetail(event);" keyDown="maindatagridKeyDownEvent(event);">

<mx:columns>

<mx:DataGridColumn dataField="saleoutstockentryno" headerText="订单号" sortable="true" width="130"/>

<mx:DataGridColumn dataField="usrno" headerText="收

货人" width="80"/>

<mx:DataGridColumn dataField="eshopconsignee" headerText= "姓名" width="80" />

<mx:DataGridColumn dataField="v_orderdate" headerText= "下单日期" />

<mx:DataGridColumn dataField="v_saleoutstockentrydate"

第 5 章 Flex 高级控件使用

·71·

headerText="出库日期" />

<mx:DataGridColumn dataField="totalmoney" headerText="应

收金额(元)" sortable="false" width="120"/>

<mx:DataGridColumn dataField="warehousename" headerText= "仓库"/>

<mx:DataGridColumn dataField="receivedmoney" headerText="已收金额" sortable="false"/>

<mx:DataGridColumn dataField="orderstatus_caption" headerText= "订单状态" sortable="false"/>

</mx:columns>

</mx:DataGrid>

dataProvider="{ saleOutstockEntrys }"定义了 DataGrid 的数据源。dataProvider 一般是一

个 ArrayCollection 对象。例如,本例中,saleOutstockEntrys 的定义如下。

[Bindable] private var saleOutstockEntrys :ArrayCollection = new

ArrayCollection();

private function resultHandler(event: ResultEvent): void {

saleOutstockEntrys = new ArrayCollection();

for each (var item: XML in event.result.vo) {

var saleOutstockEntry: EShopOrder = new EShopOrder();

saleOutstockEntry.saleoutstockentryno=item.saleoutstockentryno;

saleOutstockEntry.saleoutstockentrydate=DateUtil.parse(item.saleoutstock

entrydate2);

saleOutstockEntry.orderdate=DateUtil.parse(item.orderdate2);

saleOutstockEntry.v_saleoutstockentrydate=item.saleoutstockentrydate2;

saleOutstockEntry.v_orderdate=item.orderdate2;

saleOutstockEntry.usrno=item.usrno;

saleOutstockEntry.eshopconsignee=item.eshopconsignee;

saleOutstockEntry.orderstatus=item.orderstatus;

saleOutstockEntry.deliveryadd=item.deliveryadd;

saleOutstockEntry.totalmoney=Number(item.totalmoney);

saleOutstockEntry.receivedmoney=item.receivedmoney;

saleOutstockEntry.returnedmoney=item.returnedmoney;

saleOutstockEntry.warehouseno=item.warehouseno;

saleOutstockEntry.warehousename=item.warehousename;

saleOutstockEntry.remark=item.remark;

第二部分 进销存 UI 开发技术概述

·72·

saleOutstockEntry.status=item.status;

saleOutstockEntry.empno=item.empno;

saleOutstockEntry.ispaied=item.ispaied;

saleOutstockEntry.postcode=item.postcode;

saleOutstockEntry.shiptel=item.shiptel;

saleOutstockEntry.shipmobile=item.shipmobile;

if(saleOutstockEntry.status=="0"){ saleOutstockEntry.status_caption="存盘";

}else{ saleOutstockEntry.status_caption="提交";

}

if(saleOutstockEntry.ispaied=="0"){

saleOutstockEntry.ispaied_caption=false;

}else{

saleOutstockEntry.ispaied_caption=true;

}

saleOutstockEntrys.addItem(saleOutstockEntry);

for each (var child: XML in event.result.vo.children){

if(child.saleoutstockentryno==saleOutstockEntry.saleoutstockentryno)

{

saleOutstockEntry.lineitems.addItem(child);

}

}

}

5.2 Tree

Tree 也是我们经常用到的一个 Flex 控件。例如,在商品管理中,货品类别的维护如图

5-2 所示。

货品类别就是一个典型的树形控件。在 Flex 中,定义一棵树,是非常简单的,代码如

下。

<mx:Tree id="myTree" width="50%" height="100%" labelField="@label"

showRoot="false" dataProvider="{goodstypes}"

第 5 章 Flex 高级控件使用

·73·

itemClick="treeChanged(event)"/>

图 5-2 货品类别

其中 dataProvider 定义了这棵树的数据源。树的数据源一般用 XMLList 来表示,我们

对这棵树的数据源定义如下。

[Bindable] public var goodstypes:XMLList = new XMLList();

private function resultHandler(event: ResultEvent): void {

goodstypes =event.result.vo as XMLList;

}

一般是由查询语句查出来后,对变量 goodstypes 进行初始化,goodstypes 要求的是

XMLList,所以我们只需要在程序中,输出 XML 格式文件,然后利用“event.result.vo as

XMLList”语句,就可以自动把 XML 文件转换为 XMLList 了。本例中,我们程序输出的

XML 文件格式如图 5-3 所示。

需要注意的是,树的显示是通过 labelField="@label"来显示的,所以我们在 XML 中,

要定义 label 变量并给它赋值。

第二部分 进销存 UI 开发技术概述

·74·

图 5-3 xml 文件格式

5.3 TabNavigator

TabNavigator 也是一个业务系统中 常用的控件之一。TabNavigator 通常与 VBox、

Accordion 控件组合使用。组合出来的效果如图 5-4 所示。

图 5-4 TabNavigator 效果图

第 5 章 Flex 高级控件使用

·75·

TabNavigator 在 Flex 中的用法如下。

<mx:TabNavigator id="tn" width="100%" height="100%" horizontalGap="1"> <mx:VBox label="查询">

<mx:HBox width="100%"> <mx:Label text="订单日期(起)" width="80"/><mx:DateField

id="podate1" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="订单日期 (止 )" width="80"/><mx:DateField id=

"podate2"

editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="单据编号" width="80"/><mx:TextInput id="qpono"

width="130"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text=" 供 应 商 名 称 " width="80"/><mx:TextInput id=

"qcustsuppname"

width="130"/>

</mx:HBox>

<mx:HBox width="100%">

</mx:HBox>

<mx:HBox horizontalAlign ="left" width="100%">

<mx:Button id="queryButton" icon="@Embed('../../images/ asterisk_

orange.png')" label="查询"

labelPlacement="right" color="#993300" click="query();"/>

</mx:HBox>

</mx:VBox> <mx:VBox label="批发直达">

<mx:Accordion id="accordion_supply" width="100%" height="100%"

headerStyleName="AccordionHeaderStyle"> <mx:VBox label="服装服饰批发">

</mx:VBox> <mx:VBox label="小商品批发">

第二部分 进销存 UI 开发技术概述

·76·

</mx:VBox> <mx:VBox label="小五金">

</mx:VBox> <mx:VBox label="酒店用品">

</mx:VBox> <mx:VBox label="其他">

</mx:VBox>

</mx:Accordion>

</mx:VBox>

对于 Tab 和 Accordion 控件这类需求,可能是任何一个系统 常见 普通的需求,每

个人做起来,都会想当然的认为很容易实现,无论是纯 CSS 实现也好,结合 JQuery 实现

也好,网上类似的源代码多如牛毛。的确,网上类似的源代码是多如牛毛,但是如果你随

便从网上找到一个 Tab 的实现、又找到一个 Accordion 的实现和一个 DataGrid 的实现,你

想把他们组合在一起,几乎是不可能完成的任务。不要说这三个控件分别由三家实现,即

使是同一家的解决方案,比如 JQuery EasyUI,如果把 DataGrid 放在 Tab 控件上,单独放是

没问题的,但是如果再加上 Text 和 Label 之类的控件,那么 Tab 控件的布局,整个就乱套

了。在网上也看到过有人用 JQuery EasyUI 的 DataGrid、Text 和 Label 同时共存的截图,我

一直也没找到解决方法。

跟公司同事使用 JQuery 做管理软件的开发后,感觉自己当初选择 Flex 还是正确的,

JQuery UI 在类似这些复杂布局的问题上,至少在目前还是不太成熟。

5.4 组合使用 TitleWindow 和 DataGrid

对于像查询供应商,选择商品这类应用来讲,传统的解决方案,是在界面上加一个叫

做“查询供应商”或者“查询商品”的按钮(Button),单击这个按钮后,则弹出一个新的

页面。操作者在这个弹出的新的页面上加上查询条件,然后再把操作者选中的数据带入到

前页面。事实上,组合使用 TitleWindow 和 DataGrid,这两个控件合在一起,就可以采取

不弹出页面的方式来模拟这类需求。例如:用户查询供应商时的界面如下图 5-5 所示。

第 5 章 Flex 高级控件使用

·77·

图 5-5 供应商信息界面

它们在 Flex 中定义的源代码如下。

<?xml version="1.0" encoding="UTF-8"?>

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml"

title=" 请 选 择 供 应 商 " height="300" width="100%" showCloseButton="true"

verticalScrollPolicy="off"

horizontalScrollPolicy="off" borderAlpha="1" alpha="1" backgroundColor

="0xDCDCDC" close="parentDocument.custsuppno.close();">

<mx:Script>

<![CDATA[

import mx.rpc.events.ResultEvent;

import mx.collections.ArrayCollection;

[Bindable] private var custsupps:ArrayCollection = new ArrayCollection();

private function queryCustResultHandler(event: ResultEvent): void {

custsupps = new ArrayCollection();

for each (var item: XML in event.result.vo){

custsupps.addItem(item);

}

}

private function querySupp():void{

querycustsupp.send();

第二部分 进销存 UI 开发技术概述

·78·

}

private function selectsupp():void{

if(custsupp_datagrid.selectedItem!=null){

var item:Object = custsupp_datagrid.selectedItem;

parentDocument.custsuppno.label=item.csname;

parentDocument.custsuppno.data=item.csno;

parentDocument.custsuppno.close();

}

}

]]>

</mx:Script>

<mx:HTTPService id="querycustsupp" url="querysupp.do" method="POST"

resultFormat= "e4x" result="queryCustResultHandler(event)">

<mx:request xmlns="">

<q_csname>{csname.text}</q_csname>

</mx:request>

</mx:HTTPService>

<mx:VBox label="查询" height="100%">

<mx:HBox width="100%" borderStyle="solid">

<mx:Label text="供应商名称" width="20%"/><mx:TextInput id="csname"

width="60%" textAlign="left" />

<mx:Button id="querySuppButton" icon="@Embed('../../images/ asterisk_ orange.png')" label="查询"

labelPlacement="right" color="#993300" click="querySupp()"/>

</mx:HBox>

<mx:DataGrid id="custsupp_datagrid" width="100%" height="100%"

headerStyleName= "DataGridHeaderStyle"

dataProvider="{custsupps}" click="selectsupp()">

<mx:columns>

<mx:DataGridColumn id="dgColFollowUp" width="45" textAlign=

"left"

headerText="序号" sortable="false" dataField="row"/>

第 5 章 Flex 高级控件使用

·79·

<mx:DataGridColumn dataField="csno" headerText="供应商编码 "

sortable="true" width="140" textAlign="left" />

<mx:DataGridColumn dataField="csname" headerText="供应商名称"

sortable="true" width="260" textAlign="left" />

</mx:columns>

</mx:DataGrid>

</mx:VBox>

</mx:TitleWindow>

5.5 在 DataGrid 上加 Checkbox

在 DataGrid 上加 Checkbox 的使用场景,一般是用于多选,比如:为某个采购订单选

择自己的商品,可能会一次选择多个商品,添加到自己采购列表上,界面如图 5-6 所示。

Flex 中的源代码实现如下。

<IJetBlueUI:DataGridEx id="querygoodsdatagrid" width="100%" height="100%"

headerStyleName="DataGridHeaderStyle" dataProvider="{goods}"

resizableColumns="true" horizontalScrollPolicy="on"

verticalScrollPolicy="on"

allowMultipleSelection="true" selectionColor="haloGreen" itemClick=

"itemClickEvent (event);">

<IJetBlueUI:columns>

<mx:DataGridColumn dataField="isChecked" headerText="请选择" width=

"100" textAlign="center" sortable="false" editable="false">

<mx:itemRenderer>

<mx:Component>

<IJetBlueUI:MyCheckBox click="data.isChecked =checked "/>

</mx:Component>

</mx:itemRenderer>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="goodsno" headerText="货品编码"

第二部分 进销存 UI 开发技术概述

·80·

sortable= "true" width="150"/>

<mx:DataGridColumn dataField="goodsbn" headerText="货号" sortable=

"true" width="150"/>

<mx:DataGridColumn dataField="goodsname" headerText="货品名称"

sortable="true" width="200"/>

<mx:DataGridColumn dataField="gtname" headerText="货品类别"/>

<mx:DataGridColumn dataField="specvalues" headerText="规格"

sortable= "false"/>

<mx:DataGridColumn dataField="puname" headerText="计量单位"

sortable="false"/>

<mx:DataGridColumn dataField="retailprice" headerText="价格"

sortable="false"/>

<mx:DataGridColumn dataField="taxrate" headerText="税率"

sortable="false"/>

<mx:DataGridColumn dataField="batchno" headerText="批次" sortable=

"false"/>

</IJetBlueUI:columns>

</IJetBlueUI:DataGridEx>

本节中我们使用了自定义的控件 IJetBlueUI:DataGridEx,下一节将讲述如何扩展 Flex

现有的控件,满足产品或者项目的特殊需求。

图 5-6 Checkbox 使用场景

第 5 章 Flex 高级控件使用

·81·

5.6 扩展 Flex 控件

在上一节中我们使用了自定义的控件 IJetBlueUI:DataGridEx。其中,IJetBlueUI 是自

定义的命名空间,在 Flex 中的定义如下。

xmlns:IJetBlueUI="org.bizsolution.jetblue.ui.*"

下面我们主要讲述如何扩展 ComboBox、DataGrid 和 Tree。

扩展 DataGrid 的起因是由于任何一个和数据库打交道的控件,都会面临着从数据库中

取值并显示出来的问题,例如 DataGrid、ComboBox、Tree 等控件。对于这些控件,传统的

Flex 赋值方式就是通过 dataprovider 赋值。而 dataprovider 的初始化,则是通过 HttpService

控件,通过 Ajax 异步连接到数据库中取值,也就是说,所有和数据绑定有关的组件,都会

通过 HttpService 发请求,连接数据库取值。我们索性就把 HttpService 作为 DataGrid、

ComboBox、Tree 的属性之一。例如,我们扩展的 DataGrid 控件 DataGridEx 定义如下。

package org.bizsolution.jetblue.ui{

import mx.controls.DataGrid;

import flash.events.Event;

import mx.controls.Alert;

import mx.rpc.http.HTTPService;

import mx.rpc.events.ResultEvent;

import mx.rpc.events.FaultEvent;

import mx.collections.ArrayCollection

import mx.collections.XMLListCollection;

public class DataGridEx extends DataGrid {

[Bindable] private var records:XMLListCollection = new XMLListCollection;

private var _URL: String;

private var httpservice:HTTPService

public function DataGridEx() {

super();

records = new XMLListCollection();

dataProvider=records;

}

public function httpResult(event:ResultEvent):void {

第二部分 进销存 UI 开发技术概述

·82·

var result:Object = event.result;

for each (var item: XML in event.result.vo) {

records.addItem(item);

}

}

public function set service (value:String):void{

_URL=value;

httpservice = new HTTPService();

httpservice.url = _URL;

httpservice.method = "POST";

httpservice.resultFormat="e4x";

httpservice.addEventListener("result", httpResult);

}

public function get service():String{

return _URL;

}

public function query(obj:Object=null) :void{

httpservice.send(obj);

}

public function addLineItem(src:DataGrid):void{

records.addItem(src.selectedItem);

}

}

}

可以看出我们的 DataGridEx 多了一个 service 属性,在使用的时候,直接把 service 属

性赋值,则 DataGridEx 会自动填充数据。

相应的,扩展的 ComboBox 定义如下。

public class BaseComboBox extends ComboBox {

[Bindable] public var records: Array ;

private var _URL: String;

private var _label: String;

第 5 章 Flex 高级控件使用

·83·

private var _data: String;

private var _type:String;

private var httpservice:HTTPService

public function BaseComboBox() {

super();

records=new Array();

_label="rname"; //default

_data="rno"; //default _type="0"; //不带 "请选择" 选项 1,带"请选择" 选项。

}

public function httpResult(event:ResultEvent):void {

var result:Object = event.result;

var ret:String=new String();

var i:int=0;

records=new Array();

if(_type=="1"){ records[i]={label:"请选择", data:"NA"};

i++;

}

for each (var item: XML in event.result.vo){

records[i]={label:item.elements(_label).toString(),

data:item. elements(_data).toString()};

i++;

}

dataProvider=records;

selectedIndex=0;

}

public function set value2 (value:String):void{

for (var i:Number = 0; i < records.length; i++) {

if(records[i].data==value) {

第二部分 进销存 UI 开发技术概述

·84·

selectedIndex=i;

break;

}

}

}

public function set type (value:String):void{

this._type=value;

}

public function set service (value:String):void{

_URL=value;

httpservice = new HTTPService();

httpservice.url = _URL;

httpservice.method = "POST";

httpservice.contentType="application/xml";

httpservice.resultFormat="e4x";

httpservice.addEventListener("result", httpResult);

httpservice.send();

}

public function set labelfield (value:String):void{

_label=value;

}

public function set datafield (value:String):void{

_data=value;

}

public function get service():String{

return _URL;

}

public function get labelfield():String{

return _label;

}

public function get datafield():String{

第 5 章 Flex 高级控件使用

·85·

return _data;

}

override public function close(trigger:Event = null):void{

super.close(trigger);

}

}

}

我们使用的时候,则只简单地给 ComboBox 赋 service 就可以了。例如在我们的实例中,

BaseComboBox 引用如下。

<IJetBlueUI:BaseComboBox width="20%"

id="warehouseno" labelfield= "warehousename"

datafield="warehouseno"

data="{dataObject.warehouseno}"

service="querywarehouses.do"

value2="{dataObject.warehouseno}"/>

相应的界面如图 5-7 所示。

5.7 本章小结

本章对开发后台进销存模块所必备的几个控件,如 DataGrid、Tree、TabNavigator 等

做了简单的介绍,并介绍了他们的组合使用方法。Flex 作为非常成熟的技术,控件之间组

合使用产生的 Bug 特别少,而基于 JQuery 的 UI,由于没有一个官方的 UI 发布机构,所以

各个厂商之间的控件,即使底层都是基于 JQuery 的,但如果想把他们整合在一起也几乎是

不可能的,甚至是同一个厂家之间的控件组合在一起,也常常会出现莫名其妙问题的,如

jquery-easyui,如果在它的 Tab 控件上同时放 DataGrid 和 Textbox 和 Label 控件,那么它的

布局就会出现问题。

图 5-7 ComboBox 界面

第三部分 构建自己的 SSH 架构

·86·

第三部分 构建自己的 SSH架构

每个架构师,都有自己的开发情结和原则,有时候甚至无缘无故掺杂很多个人的情感

和好恶,而与技术本身的优劣无关。比如笔者就是不喜欢极限编程理论和原则,不喜欢 Ruby

语言,而有些工程师就极其推崇 Ruby。所以,关键是如何把自己的思想和理论付诸实践,

而不是跟人纸上论剑。本部分就是笔者对 SSH 架构方面的理论的实践。第六章主要讲解如

何创建自己的 Struts 框架;第七章主要讲解如何创建自己的 Hibernate;第八章主要讲解如

何打造自己的 Spring。通过这部分的学习,读者将学会如何构建自己的 SSH 架构。

第 6 章 创建自己的 Struts 框架

·87·

第 6 章 创建自己的 Struts 框架

6.1 创建背景

Struts 是一个通用的 J2EE 三层架构中表示层 MVC 的解决方案。我们反复阐述了 Struts

学习 Visual Basic 创造性地采用数据绑定(Data Binding)的形式,通过 ActionForm 类把网

页数据传递给了 Struts 的 Action,在 Struts1.1 版本时,我们必须要继承 ActionForm 类才能

接收网页数据,Struts 2 版本之后就没有这个要求了。所以,在 Struts1.1 的时候,我们有如

下原则。

(1)FormBean:继承 Struts 的 ActionForm。

(2)PO(Persistent Object)持久对象,通常指 Hibernate 要求的实体 Java 类,与数据

库表结构一一对应。

(3)POJO(Plain Ordinary Java Object),无规则简单 Java 对象,一个中间对象,可以

转化为 PO、DTO、VO。

例如 POJO 持久化之后转化为 PO,在运行期,由 Hibernate 中的 cglib 动态把 POJO 转

换为 PO,PO 相对于 POJO 会增加一些用来管理数据库 entity 状态的属性和方法。POJO 传

输过程中则转化为 DTO;如果 POJO 用作表示层,POJO 又可转化为 VO。

(4)DTO(Data Transfer Object)数据传输对象,用在需要跨进程或远程传输时,它不

应该包含业务逻辑。

(5)VO(Value Object)值对象,用于表现层对象,主要对应 Web 页面显示的数据对

象。在 Struts 中,用 ActionForm 做 VO,需要做一个转换,因为 PO 是面向对象的,而

ActionForm 是和 View 对应的,需要将几个 PO 要显示的属性合成一个 ActionForm。

第三部分 构建自己的 SSH 架构

·88·

(6)BO(Business Object)业务对象。封装业务逻辑为一个对象,可以包括多个 PO,

通常需要将 BO 转化成 PO,才能进行数据的持久化,反之,从 DB 中得到的 PO,需要转

化成 BO 才能在业务层使用。关于 BO 主要有三种概念:只包含业务对象的属性;只包含

业务方法;两者都包含。

大家一下看到这么多 XXO(CEO、CTO、CFO、COO…)是不是都晕了呢?说实话,

我也晕了,这就是 J2EE 编程太讲究设计模式和架构的弊病,所以单个开发网站应用的话,

效率太差,所以就有了 ROR(Ruby on Rails)编程的兴起。

用传统 Struts 的话,与其在 FormBean、PO、POJO、DTO、VO、BO 之类的概念上进

行反复折腾和推敲,与其在与别人辩论是否符合某种设计模式和某某规范,不如就做个自

己的 Struts,来满足以下需求。

(1)Action 在 XML 文件中可配置,这个参照 Struts 实现;

(2)对于查询,Action 对应一个 Query 标签,Query 标签对应一个 SQL 语句,复杂的

主子表关系,可配置两个 SQL 语句,分别表示对主表的查询和对子表的查询。

Struts 会把 SQL 写在 DAO 类里面,然后又用 Service 类去调用这个 DAO, 后又在

Action 类里面调用这个 Service 类。一个简单的查询,至少要 3 个类来实现,并美其名曰自

己是在盖大楼,不按照这种模式去做的,是在垒鸡窝,可是你盖的大楼,生命力到底有多

久呢?是不是百年工程呢?这里我们就直接把 SQL 语句写在 XML 里面,大家会看到,针

对于查询,我们会利用这个方式,满足 80%以上查询需求,而不用编写任何代码,包括带

多个复杂参数的查询;

(3)对于查询,Action 可以有多个 Parameter 标签,动态配置多个查询参数;

(4)由于网页上的字段对应的字段名和实际数据库的表结构对应的字段名可能完全不

同,所以我们还要实现自动转换数据库表结构字段名与网页上字段名的映射;

(5)对于新增和修改操作,我们还要支持 condiation 标签,动态判断是否执行某个

component 组件;

(6)支持 component 组件标签,动态执行一段业务逻辑;

(7)支持 businessprocess 标签,可把多个 component 组件组装为一个 businessprocess,

表示一个事务,或者一个整体业务过程。比如:新增采购入库单带来的连锁反应 修改库

存台账 修改采购订单到货数量 新增应付款;

(8)缺省简单新增 Action 标签的实现,无须编程,实现单表存盘;

第 6 章 创建自己的 Struts 框架

·89·

(9)缺省复杂新增 Action 标签的实现,满足绝大部分主子表和一个主表,多个子表存

盘;

(10)缺省修改 Action 标签的实现,无须编程,实现单表存盘;

(11)缺省复杂修改 Action 标签的实现,满足绝大部分主子表和一个主表,多个子表

存盘;

(12)缺省删除 Action 标签的实现,无须编程,实现存盘。

我们还可以给这个框架增加无限多的标签来满足我们特定的需求,这就是自造框架的

好处,根据需求,可以随意增加自己的标签库,与 Struts 有本质的差异。

6.2 实现自己的 MVC

6.2.1 实现通用的 MVC 框架 我们先看一个 简单的单表查询例子。

<?xml version="1.0" encoding="UTF-8"?>

<mappings>

<url-mapping action="queryroles.do"

class=" com.softbnd.jetblue.saas.webframework.action.LogonQueryAction ">

<query SQL="SELECT * FROM role where 1=1 order by roleno ">

<parameter name="rolename" type="String" op="="/>

</query>

</url-mapping>

</mappings>

把这段 XML 文件命名为 scm-system-mappings.xml,我们后面还有 scm-inventory-

mappings.xml 文件处理所有的库存管理,scm-purchase-mappings.xml 文件处理所有的采购

管理,scm-sales-mappings.xml 文件处理所有的销售管理等。我们把这些所有的 XML 配置

文件名,集中在一个叫做 jetblue-mappings.xml 里面,代码如下。

<?xml version="1.0" encoding="UTF-8"?>

<jet-mappings>

第三部分 构建自己的 SSH 架构

·90·

<parse-name>eshop-system-mappings.xml</parse-name>

<parse-name>scm-basedata-mappings.xml</parse-name>

<parse-name>scm-global-mappings.xml</parse-name>

<parse-name>scm-purchase-mappings.xml</parse-name>

<parse-name>scm-sales-mappings.xml</parse-name>

<parse-name>scm-inventory-mappings.xml</parse-name>

<parse-name>scm-stat-mappings.xml</parse-name>

<parse-name>scm-system-mappings.xml</parse-name>

<parse-name>web-template-mappings.xml</parse-name>

<parse-name>ecshop-template-mappings.xml</parse-name>

<pub-exception-mapping exception-class="java.lang.Exception"

view=" login.jsp"/>

</jet-mappings>

这就是我们所有的 XML 配置文件的基本框架。url-mapping action="queryroles.do" 即

对应 Struts 的 Action,这里并没有像 Struts 那样,明显标记出 Model 和 View,因为我们目

前都会采用 Ajax 的方式去调用各类 Action,所以,我们的 Action 的返回方式是谁调用它,

它就返回到哪个页面,而不用再调用 response.sendRedirect(),所以我们可以没有明显的

View,当然我们也是可以支持 View 标签的。对于 Model,我们总是返回一个固定的

TableObject 类,这个类采用 HashMap 的方式,返回所有的字段属性。

和 Struts 对比,我们明显地多了一个 Query 标签,这个标签就是这个 Action 对应的查

询 SQL,并可以有多个查询参数。我们通过这种方式处理绝大部分查询。

下面我们讲讲如何实现这样一个 MVC 框架。

首先我们要做一套通用的解析 XML 的框架,来解析 jetblue-mappings.xml 文件以及

它包含的 scm-purchase-mappings.xml、cm-inventory-mappings.xml、scm-sales-mappings.xml

等各类面向具体业务的配置文件。其次,我们要解析自定义标签,比如 query、parameter

等。

我们把所有的请求分为两大类。

1.不需要验证的 Action,比如前台商品展示和查询,我们用类 ControlServlet 来处理此

类请求。在 web.xml 文件里定义如下。

<servlet>

<servlet-name>action</servlet-name>

第 6 章 创建自己的 Struts 框架

·91·

<servlet-class>com.softbnd.jetblue.saas.webframework.action.

ControlServlet</servlet-class>

</servlet>

<servlet-mapping>

<servlet-name>action</servlet-name>

<url-pattern>*.action</url-pattern>

</servlet-mapping>

我们用 ControlServlet 类来处理所有后缀是*.action 的请求。

2.需要验证的 Action,比如后台商品数据录入、查询、修改、删除的操作;前台下订

单的操作,我们用类 LoginControlServlet 来处理此类请求。在 web.xml 文件里定义如下。

<servlet>

<servlet-name>do</servlet-name>

<servlet-class>

com.softbnd.jetblue.saas.webframework.action.LoginControlServlet

</servlet-class>

</servlet>

<servlet-mapping>

<servlet-name>do</servlet-name>

<url-pattern>*.do</url-pattern>

</servlet-mapping>

我们用 LoginControlServlet 类来处理所有后缀是*.do 的请求,其类图如图 6-1 所示。

对 于 queryroles.do , 其 后 缀 是 “ do ”, 所 以 我 们 用 LoginControlServlet 截 获 ,

LoginControlServlet 截获后,如何继续传递给 queryroles.do 对应的 Java 类 LogonQueryAction

呢?所以我们还需要以下几个类先把 scm-system-mappings.xml 文件进行解析,找到

queryroles.do 对应的类后,然后用类加载机制,动态加载类 LogonQueryAction。

其序列图和类图如图 6-2、图 6-3 所示。

第三部分 构建自己的 SSH 架构

·92·

图 6-1 LoginControlServlet 类图

图 6-2 序列图

第 6 章 创建自己的 Struts 框架

·93·

图 6-3 类图

(1)我们在 ControlServlet 类的 public void init(ServletConfig config)方法里,初始化

jetblue-mappings.xml 文件,初始化的同时初始化它所包含的所有其他的 XML 配置文件。

其代码如下。

package com.softbnd.jetblue.saas.webframework.action;

import java.io.IOException;

import java.util.HashMap;

import javax.servlet.ServletConfig;

import javax.servlet.ServletContext;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.softbnd.jetblue.webframework.util.RequestHandler;

import com.softbnd.jetblue.webframework.util.URLMappingsXmlDAO;

import com.softbnd.jetblue.webframework.util.ViewFlowManager;

import com.softbnd.jetblue.webframework.util.WebKeys;

第三部分 构建自己的 SSH 架构

·94·

//所有入口的控制类

public class ControlServlet extends HttpServlet {

public ServletContext context;

protected HashMap urlMappings; //初始化 jetblue-mappings.xml配置文件,把所有的配置信息读入到内存中。

public void init(ServletConfig config) throws ServletException {

this.context = config.getServletContext();

String requestMappingsURL = null;

try {

requestMappingsURL =

context.getResource("/WEB-INF/jetblue-mappings.xml").

toString();

urlMappings =

URLMappingsXmlDAO.loadRequestMappings(requestMappingsURL,

context);

context.setAttribute(WebKeys.URL_MAPPINGS, urlMappings);

} catch (Exception e) {

e.printStackTrace();

}

getViewFlowManager();

getRequestHandler();

}

public void doGet(HttpServletRequest req, HttpServletResponse resp)

throws ServletException, IOException {

doProcess(req, resp);

}

public void doPost(HttpServletRequest req, HttpServletResponse resp)

throws ServletException, IOException {

doProcess(req, resp);

} //根据请求传回的 xxx.do和已经读入到内存当中的配置文件进行匹配,转发到相应的 Action

对应的 class,和 Struts原理是类似的。

public void doProcess(HttpServletRequest request, HttpServletResponse

response)

第 6 章 创建自己的 Struts 框架

·95·

throws IOException, ServletException {

request.setCharacterEncoding("UTF-8");

getRequestHandler().handleRequest(request, response);

}

private ViewFlowManager getViewFlowManager() {

ViewFlowManager viewFlowManager = (ViewFlowManager)

context.getAttribute(WebKeys.VIEW_FLOW_MANAGER);

if (viewFlowManager == null) {

viewFlowManager = new ViewFlowManager();

viewFlowManager.init(context);

context.setAttribute(WebKeys.VIEW_FLOW_MANAGER,

viewFlowManager);

}

return viewFlowManager;

}

private RequestHandler getRequestHandler() {

RequestHandler rh = (RequestHandler)

context.getAttribute(WebKeys.REQUEST_HANDLER);

if (rh == null) {

rh = new RequestHandler();

rh.init(context);

context.setAttribute(WebKeys.REQUEST_HANDLER, rh);

}

return rh;

}

}

我们先用 context.getResource("/WEB-INF/jetblue-mappings.xml").toString()方法,加

载 jetblue-mappings.xml 文件,然后把这个文件放到类 URLMappingsXmlDAO 去解析。

(2)我们再看看 URLMappingsXmlDAO 是如何解析这些 XML 文件的。

package com.softbnd.jetblue.webframework.util;

import java.net.MalformedURLException;

import java.net.URL;

第三部分 构建自己的 SSH 架构

·96·

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import javax.servlet.ServletContext;

import javax.xml.parsers.DocumentBuilder;

import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;

import org.w3c.dom.Element;

import org.w3c.dom.Node;

import org.w3c.dom.NodeList;

import org.xml.sax.InputSource;

import org.xml.sax.SAXException;

import org.xml.sax.SAXParseException;

//解析 XML配置文件对应的类

public class URLMappingsXmlDAO { //对应 XML文件中的各类标签

private static final String PARSE_NAME = "parse-name";

private static final String EXCEPTION_MAPPING = "pub-exception-mapping";

private static final String URL_MAPPING = "url-mapping";

private static final String COMPOSITE = "composite";

private static final String WEB_ACTION_CLASS = "web-action-class";

private static final String ACTION_SQL = "action-SQL";

private static final String ACTION = "action";

private static final String VIEW = "view";

private static final String CLASS = "class";

private static final String QUERY = "query";

private static final String SUBQUERY = "subquery";

private static final String SQL = "SQL";

private static final String FK = "fk";

private static final String PARAMETER = "parameter";

private static final String NAME = "name";

private static final String TYPE = "type";

private static final String OP = "op";

private static final String OR = "or";

第 6 章 创建自己的 Struts 框架

·97·

private static final String NOREQUEST = "norequest";

private static final String REQUESTNAME = "requestname";

private static final String CONDIATION = "condiation";

private static final String KEY = "key";

private static final String VALUE = "value";

private static final String COMPONENT="component";

private static final String STEP="step";

private static final String DESCRIPTION="description";

private static final String PLUGIN="businessprocess";

private static final String LINK="link";

private static final String DEBUG="debug";

private static final String TEMPLATEPATH="templatepath";

//把 XML读入到内存中,并使用 org.w3c.dom.Document方式去解析

public static HashMap loadRequestMappings(String url, ServletContext

context) {

Element root = loadDocument(url);

NodeList exceptionNodes =

root.getElementsByTagName(EXCEPTION_MAPPING);

if (exceptionNodes != null && exceptionNodes.getLength() > 0) {

Node exceptionNode = exceptionNodes.item(0);

if (exceptionNode instanceof Element) {

Element element = ((Element) exceptionNode);

String exceptionView = element.getAttribute("view");

context.setAttribute(WebKeys.EXCEPTION_MAPPING_VIEW,

exceptionView);

}

}

return getRequestMappings(root, context);

} //遍历所有的 XML格式文件,并按照指定的标签,转化为对应的类

private static HashMap getRequestMappings(Element root, ServletContext

context) {

HashMap urlMappings = new HashMap();

NodeList nodes = root.getElementsByTagName(PARSE_NAME);

for (int loop = 0; loop < nodes.getLength(); loop++) {

第三部分 构建自己的 SSH 架构

·98·

Node node = nodes.item(loop);

if (node != null) {

String xmlmappings = "";

if (node instanceof Element) {

Element element = ((Element) node);

xmlmappings = element.getFirstChild().getNodeValue();

try {

xmlmappings = context.getResource("/WEB-INF/" +

xmlmappings).toString();

Element jetroot = loadDocument(xmlmappings);

NodeList jetNodes =

jetroot.getElementsByTagName(URL_MAPPING);

for (int j = 0; j < jetNodes.getLength(); j++) {

Node jetnode = jetNodes.item(j);

if (jetnode != null) {

if (jetnode instanceof Element) {

Element jetelement = ((Element) jetnode); //找到 View标签

String view = jetelement.getAttribute(VIEW);

String action = //找到 Action标签

jetelement.getAttribute(ACTION);

String debug =

jetelement.getAttribute(DEBUG);

String templatepath=

jetelement.getAttribute

(TEMPLATEPATH); //找到 Action对应的 Class

String actionclass =

jetelement.getAttribute(CLASS);

String composite = getSubtagValue(jetnode,

COMPOSITE);

String web_action_class =

getSubtagValue(jetnode, WEB_ACTION_CLASS);

String action_SQL =

getSubtagValue(getSubNode(jetnode,

WEB_ACTION_CLASS), ACTION_SQL);

第 6 章 创建自己的 Struts 框架

·99·

Node subNode = getSubNode(jetnode,QUERY);

String SQL=getAttributeValue(subNode,SQL);

Query query = new Query();

query.setSql(SQL);

Link link=getLink(subNode, LINK);

query.setLink(link);

Node linkNode = getSubNode(subNode, LINK);

Parameter[] parameters =

getParameters(linkNode, PARAMETER);

link.setParameters(parameters); //找到查询参数对应的标签

parameters=getParameters(subNode,PARAMETER);

query.setParameters(parameters);

SubQuery subQuery=getSubQuery(subNode,

SUBQUERY);

query.setSubQuery(subQuery);

Node subQueryNode = getSubNode(subNode,

SUBQUERY);

Parameter[] subparameters =

getParameters(subQueryNode, PARAMETER);

subQuery.setParameters(subparameters);

Node bpNode = getSubNode(jetnode,PLUGIN);

BusinessProcess businessProcess=new

BusinessProcess();

String description= getAttributeValue

(bpNode,DESCRIPTION);

businessProcess.setDescription(description); //动态构建所有的查询条件

Condiation[] condiations=

getCondiations(bpNode,CONDIATION);

businessProcess.setCondiations(condiations);

Component[] components=

getComponents(bpNode,COMPONENT);

businessProcess.setComponents(components);

JetMappings jetm = new JetMappings(action,

第三部分 构建自己的 SSH 架构

·100·

view, composite, web_action_class, action_SQL,debug,

templatepath);

jetm.setActionclass(actionclass);

jetm.setQuery(query);

jetm.setBusinessProcess(businessProcess);

if (!urlMappings.containsKey(action)) {

urlMappings.put(action, jetm);

} else {

}

}

}

}

} catch (MalformedURLException e) {

e.printStackTrace();

}

}

}

}

return urlMappings;

} //根据标签,找到所有的参数设置

private static Parameter[] getParameters(Node node, String subTagName) {

Parameter[] p;

List<Parameter> paras = new ArrayList<Parameter>();

if (node != null) {

NodeList children = node.getChildNodes();

for(int innerLoop=0;innerLoop<children.getLength(); innerLoop++){

Node child = children.item(innerLoop);

if ((child != null) && (child.getNodeName() != null)

&& child.getNodeName().equals(subTagName)) {

Parameter parameter = new Parameter();

String name = getAttributeValue(child, NAME);

String type = getAttributeValue(child, TYPE);

String op = getAttributeValue(child, OP);

String or = getAttributeValue(child, OR);

第 6 章 创建自己的 Struts 框架

·101·

String norequest = getAttributeValue(child, NOREQUEST);

String requestname=getAttributeValue(child,REQUESTNAME);

parameter.setName(name);

parameter.setType(type);

parameter.setOp(op);

parameter.setRequestName(requestname);

parameter.setOr(or);

parameter.setNorequest(norequest);

paras.add(parameter);

}

}

}

p=new Parameter[paras.size()];

p=paras.toArray(p);

return p;

}

//找到主子表中子表对应的 SQL语句

private static SubQuery getSubQuery(Node node, String subTagName) {

SubQuery query = new SubQuery();

if (node != null) {

NodeList children = node.getChildNodes();

for (int innerLoop=0;innerLoop<children.getLength();innerLoop++){

Node child = children.item(innerLoop);

if ((child != null) && (child.getNodeName()!=null)&&child.

getNodeName().equals(subTagName)) {

String SQL = getAttributeValue(child, SQL);

String fk = getAttributeValue(child, FK);

query.setSql(SQL);

query.setFk(fk);

break;

}

}

}

return query;

}

private static Node getSubNode(Node node, String subTagName) {

第三部分 构建自己的 SSH 架构

·102·

Node returnNode = null;

if (node != null) {

NodeList children = node.getChildNodes();

for (int innerLoop = 0; innerLoop < children.getLength();

innerLoop++){

Node child = children.item(innerLoop);

if ((child != null) && (child.getNodeName() != null) && child.

getNodeName().equals(subTagName)) {

returnNode = child;

break;

}

}

}

return returnNode;

}

}

我 们 在 URLMappingsXmlDAO 类 的 loadRequestMappings() 方 法 里 加 载 jetblue-

mappings.xml 文件,然后再调用 getRequestMappings()方法。getRequestMapping()方法先逐

个解析 scm-purchase-mappings.xml、scm-sales-mappings.xml、scm-inventory- mappings.xml

等配置文件,然后对每个配置 XML 文件再逐个解析 action、query、parameter、subquery

等标签,并把这些标签作为对象 JetMappings 的属性存放起来,

一个 JetMappings 对象对应一个 <url-mapping> </url-mapping> 标签。一个 scm-

system-mappings.xml 可以有很多个<url-mapping> </url-mapping> 标签,所以我们就把所有

的 JetMappings 对象放在 HashMap 里面,key 就是 abc.do 或者 abc.action。 后再把所有的

HashMap 放在 ServletContext 全局变量里。这样,每当我们调用一个 abc.do 或者 abc.action

的时候,就去这个 HashMap 里找到对应的 JetMappings 类,通过 JetMappings 类,找到 abc.do

或者 abc.action 对应的 class=”com.softbnd.xxx.ABCAction”,然后动态加载这个 action 类。

这样,我们就实现了自己的 MVC 框架。

6.2.2 Query 标签 Query 标签,能实现无编码单表自动查询,我们看段 简单的单表查询例子。

第 6 章 创建自己的 Struts 框架

·103·

<?xml version="1.0" encoding="UTF-8"?>

<mappings>

<url-mapping action="queryroles.do"

class=" com.softbnd.jetblue.saas.webframework.action.LogonQueryAction ">

<query SQL="SELECT * FROM role where 1=1 order by roleno ">

<parameter name="rolename" type="String" op="="/>

</query>

</url-mapping>

</mappings>

6.1 节中,我们讲了如何把 queryroles.do 和具体的实现类 LogonQueryAction 关联上,

每当我们请求 queryroles.do 的时候,ServletContext 先找 queryroles.do 对应的 JetMappings

类,JetMappings 类有两个 重要的属性 class 和 Query,class 是 queryroles.do 对应的实现

类,Query 属性则表示 queryroles.do 对应的 SQL,它的值是“SELECT * FROM role where 1=1

order by roleno”,对于查询参数,Query 还有属性 Parameter 与之对应。一个 Query 标签

可以有多个 Parameter。

现在我们就分析 LogonQueryAction 是如何解析 Query 和 parameter 标签的。

我们先看 LogonQueryAction 的类图,如图 6-4 所示。

图 6-4 LogonQueryAction 类图

第三部分 构建自己的 SSH 架构

·104·

类 LogonQueryAction 的序列图如图 6-5 所示。

当 系 统 检 测 到 调 用 queryroles.do 的 时 候 , 系 统 转 到 类 LogonQueryAction , 类

LogonQueryAction 先判断用户是否登录了,如果没有登录,或者登录时间太长了,Session

失效了,则直接退回到系统登录页面,如果登录成功了,则继续执行这个查询语句。

LogonQueryAction 先执行父类的 Query 方法,Query 方法又执行 getSQL()方法,得到要查

询的 SQL 语句:“SELECT * FROM role where 1=1 order by roleno”,如果是主子表,则查询

还会继续往下执行,getSubSQL()方法,得到子表的查询 SQL,然后类 LogonQueryAction

会继续调用类 BaseService 的 query()方法,得到所有的查询记录,并保存为 TableObject 数

组,代表多条记录。查询到符合条件的记录后,再次调用父类的 println()方法,输出自己的

XML 格式的数据流,前台通过支持 restful 风格的框架,得到这个 XML 格式数据流,并在

前台显示出来。相应的源代码如下。

图 6-5 LogonQueryAction 序列图

第 6 章 创建自己的 Struts 框架

·105·

public class LogonQueryAction extends BaseQueryAction{

public LogonQueryAction(){

super();

}

@Override

public void perform(HttpServletRequest request, HttpServletResponse

response) {

loginUser=(LoginUser) request.getSession().getAttribute

("loginUser");

if(loginUser==null){

try {

response.sendRedirect(context.getAttribute(WebKeys

.EXCEPTION_MAP

PING_VIEW).toString());

} catch (IOException e) {

logger.error(e.getStackTrace().toString());

}

}else{

super.perform(request, response);

}

}

}

public abstract class BaseQueryAction extends AbstractAction {

private JetMappings urlMapping;

public BaseQueryAction() {

super();

}

@Override

public void perform(HttpServletRequest request, HttpServletResponse

response) {

urlMapping = webUtil.getJetMappings(request, urlMappings);

query(request, response);

}

public void query(HttpServletRequest request, HttpServletResponse

第三部分 构建自己的 SSH 架构

·106·

response) {

try {

request.setCharacterEncoding("UTF-8");

} catch (UnsupportedEncodingException e) {

}

String SQL = getSql(request);

String subSQL = getSubSql(request);

String currentpage = request.getParameter("currentpage");

String pagesize = request.getParameter("pagesize");

if (pagesize == null) {

pagesize = "19";

}

if (subSQL == null) {

if (pagesize != null && currentpage != null) {

PageObject pageObject=service.query(SQL,loginUser.getTenant(),

Integer.parseInt(currentpage), Integer.parseInt(pagesize));

println(response, pageObject.getTableObjects(), pageObject.

getTotalpage(),

pageObject.getRecordcount());

} else {

TableObject[] vos = service.query(SQL, loginUser.

getTenant());

println(response, vos);

}

} else {

if (pagesize != null && currentpage != null) {

PageObject pageObject=service.query(SQL,subSQL,loginUser.

getTenant(),

Integer.parseInt(currentpage), Integer.parseInt(pagesize));

println(response, pageObject.getTableObjects(), pageObject.

getTotalpage(),

pageObject.getRecordcount());

}else {

TableObject[] vos = service.query(SQL, subSQL, loginUser.

getTenant());

println(response, vos);

}

第 6 章 创建自己的 Struts 框架

·107·

}

}

protected String getSql(HttpServletRequest request) {

Parameter[] parameters = urlMapping.getQuery().getParameters();

String SQL = urlMapping.getQuery().getSql();

Link link = urlMapping.getQuery().getLink();

return parseSql(SQL, request, parameters, link);

}

protected String getSubSql(HttpServletRequest request) {

String SQL = urlMapping.getQuery().getSubQuery().getSql();

Parameter[] parameters =

urlMapping.getQuery().getSubQuery().getParameters();

if (SQL != null) {

SQL = parseSql(SQL, request, parameters, null);

}

return SQL;

}

6.2.3 SubQuery 标签 SubQuery 标签,能实现无编码主从表关联查询,从 6.2.2 节的类 LogonQueryAction 代

码中可以看到,类 LogonQueryAction 通过 context 得到 JetMappings 类,通过 JetMappings

类得到 SQL,如果是主子表关系,还可以拿到子表 SubSQL,我们再举一个主子表查询的

例子。

<url-mapping action="querysaleoutstock.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction" description="查询销售出库单">

<query SQL="select DATE_FORMAT(saleoutstockentrydate,'%Y-%m-%d')

saleoutstockentrydate2,DATE_FORMAT(orderdate,'%Y-%m-%d')

orderdate2 ,saleoutstockentry.*,csname,warehousename from

saleoutstockentry join

warehouse on saleoutstockentry.warehouseno=warehouse.warehouseno left

join custsupp on saleoutstockentry.custsuppno=custsupp.csno and custsupp.

第三部分 构建自己的 SSH 架构

·108·

tenantno in ('#tenantno#') where saleoutstockentry.tenantno in ('#tenantno#')

and warehouse.tenantno in ('#tenantno#','0000000000')and

iseshopsaleorder='0'" >

<subquery SQL="select saleoutlineitem.*,goodsname,gtname,packageunit.

puname as packageunitname

from saleoutlineitem,goods,packageunit,goodstype

where saleoutlineitem.goodsno=goods.goodsno and

goods.goodsunitno=packageunit.puno and goods.goodstypeno=goodstype.

gtno

and saleoutlineitem.tenantno in ('#tenantno#') and goods.tenantno in

('#tenantno#') and packageunit.tenantno in ('#tenantno#',

'0000000000')and goodstype.tenantno in('#tenantno#')and saleoutlineitem.

saleoutstockentryno='?'"/>

<parameter requestname="date1" name="saleoutstockentrydate" type=

"Date" op="&gt;=" or=" saleoutstockentry.status='0' "/>

<parameter requestname="date2" name="saleoutstockentrydate" type=

"Date" op="&lt;=" or=" saleoutstockentry.status='0' "/>

<parameter requestname="saleoutstockentryno" name="saleoutstockentryno"

type="String"/>

<parameter requestname="csname" name="csname" type="String"/>

<parameter requestname="status" name="saleoutstockentry.status"

type="String" op="="/>

</query>

</url-mapping>

其对应的 UI 界面如图 6-6 所示。

在这个例子中,主表是销售出库单,子表是此销售出库单对应的货品。无论是主表还

是子表,这个例子的 SQL 语句都是很复杂的。我们先看主表。

select DATE_FORMAT(saleoutstockentrydate,'%Y-%m-%d')

saleoutstockentrydate2,DATE_FORMAT(orderdate,'%Y-%m-%d')

orderdate2 ,saleoutstockentry.*,csname,warehousename from saleoutstockentry

join

warehouse on saleoutstockentry.warehouseno=warehouse.warehouseno left join

第 6 章 创建自己的 Struts 框架

·109·

custsupp on saleoutstockentry.custsuppno=custsupp.csno and custsupp.tenantno

in ('#tenantno#') where

saleoutstockentry.tenantno in ('#tenantno#') and warehouse.tenantno in

('#tenantno#','0000000000' ) and iseshopsaleorder='0'

图 6-6 主子表查询 UI

主表是出库单表 saleoutstockentry 和库房表 warehouse 左关联,带的参数有五个:

(1)生成出库单开始日期(2)生成出库单结束日期(3)出库单单据编号(4)出库

单队赢得客户名称(5)单据状态,例如新增、确认、删除等状态。

我们再看子表。 <subquery SQL="select saleoutlineitem.*,goodsname,gtname ,packageunit.

puname as packageunitname

from saleoutlineitem,goods,packageunit,goodstype

where saleoutlineitem.goodsno=goods.goodsno and

goods.goodsunitno=packageunit.puno and goods.goodstypeno=goodstype.gtno

and saleoutlineitem.tenantno in ('#tenantno#') and goods.tenantno in

('#tenantno#') and packageunit.tenantno in ('#tenantno#','0000000000') and

goodstype.tenantno in ('#tenantno#') and

saleoutlineitem.saleoutstockentryno='?'"/>

子表 SQL 的标签用 subSQL 表示,这个 SQL 语句中,出库单子表 saleoutlineitem 和其

第三部分 构建自己的 SSH 架构

·110·

他 4 个表进行关联,分别是:货品表 goods,计量单位 packageunit,货品类别表 goodstype,

后 再 通 过 saleoutlineitem.saleoutstockentryno='?' 中 的 问 号 , 把 主 子 表 之 间 通 过

saleoutstockentryno 关联上。找到主子表的 SQL 语句后,剩下的就跟单表查询的过程是一样

的,程序会判断 subSQL 是否为 NULL 值,如果不是,则继续执行 BaseService 的 query()

方法,为主表的每一个 TableObject 值再次填充 children。

6.2.4 requestname 和 name 标签 requestname 和 name 标签,实现表单域数据库字段分离,对于界面表单上的每一个字

段名,他们通常情况下都是跟数据库的字段名一致的,这么做的好处是不用再一一匹配了,

尤其是当一个表的字段值超过 20 个以上的时候,一一匹配既耗时,又很容易出错,但是有

时候我们又必须使表单中的字段名和数据库中的字段名不一致。例如在同一个界面中,我

们查询出库单的起止日期,出库单的出库日期的数据库字段名字是 saleoutstockentrydate,

查询条件包括开始日期和结束日期,在同一个 UI 中,UI 组件的 id 值是不可以一致的,代

码如下所示。

<mx:VBox label="查询">

<mx:HBox width="100%"> <mx:Label text="出库日期(起)" width="80"/><mx:DateField id="date1" editable=

"true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="出库日期(止)" width="80"/><mx:DateField id="date2"

editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="单据编号" width="80"/><mx:TextInput

id="qsaleoutstockentryno" width="130"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="客户名称" width="80"/><mx:TextInput id="csname"

width="130"/>

</mx:HBox>

<mx:HBox width="100%">

第 6 章 创建自己的 Struts 框架

·111·

</mx:HBox>

<mx:HBox horizontalAlign ="left" width="100%">

<mx:Button id="queryButton" icon="@Embed('../../images/asterisk _orange.png')" label="查询" labelPlacement="right" color="#993300"

click="query();"/>

</mx:HBox>

</mx:VBox>

对于查询起止日期,不可以使用同一个 id: saleoutstockentrydate,所以用 data1 代表出

库单开始日期,data2 代表出库单结束日期,用 requestname 标签代表 UI 界面上的字段名称,

用 name 标签代表数据库字段实际名称,这样就解决了 UI 字段名和数据库字段名不一致的

问题,代码如下所示。

<parameter requestname="date1" name="saleoutstockentrydate" type="Date"

op="&gt;=" or=" saleoutstockentry.status='0' "/>

<parameter requestname="date2" name="saleoutstockentrydate" type="Date"

op="&lt;=" or=" saleoutstockentry.status='0' "/>

6.3 本章总结

本章通过简单和复杂的查询标签,详细讲解了如何构建自己的 MVC 架构,如何动态

生成查询 SQL,如果动态组合主子表完成联合查询,如何动态添加查询参数,同样的,我

们可以利用类似的技术,完成通用的新增、修改、删除的标签,在大部分情况下,我们利

用这些通用的标签库,就可以完成基本的新增、修改、查询、删除等对数据库的基本操作,

由于采用了 restful 风格的架构,这些操作与 UI 采用什么技术无关,JSP 也好,ASP 也好,

PHP 也好,理论上都是可以应付的,我们的框架的生命力也就得到了展现。当然,这么做

下来也不是没有问题的,又回到了那句老话:“没有银弹”。任何技术,满足了一些特性,

必然会牺牲另外一些特性。比如我们采用反射机制,使得 action 与实现类相分离,实现了

动态加载,却是以牺牲部分性能为代价。

对于新增、修改、删除的标签,他们的实现技术难度,是要小于查询标签的技术难度的,

感兴趣的读者可以从网上下载具体的例子,来研究如何实现这些新增、修改、删除标签。

第三部分 构建自己的 SSH 架构

·112·

第 7 章 打造自己的 Hibernate

在 3.2.3“打造自己的存储框架”一节,我们详细讲述了为什么要做自己的存储框架,

而不用现有的轮子 Hibernate,下面几个小节,我们将讲述如何实现自己的存储框架。

7.1 创建存储框架

虽然 restful 架构风格的框架,理论上支持 JSP、ASP、PHP 与 Flex 等任何 UI 技术,

这些 UI 上的字段数量与数据库字段数量是一一对应的,但是一个 UI 上到底有多少字段是

不确定的,比如用户注册界面可能只有用户名、密码、邮箱三个基本字段,而一个销售出

库单,包括子表,会有数十个字段需要存储。Hibernate 要求实体 Bean 与数据库字段名一

一对应,这就要求每一个 action 只能对应一个实体 Bean,并把这个实体 Bean 一直传到 DAO,

再利用 Hibernate 进行存储。我们这里则不这么做,我们的设计如图 7-1 和图 7-2 所示,无

论前台界面是什么技术,无论前台是什么功能,包含多少字段,用户注册、销售出库,采

购入库,我们先统统转化为 TableObject 对象,然后再利用 DAO 技术,直接存储唯一的

TableObject 对象,而不是利用 Hibernate 存储跟数据库中每个表字段一一对应的多个实体

Bean 对象。

我们再把 TableObject 对象进行分解,如图 7-3 所示。

每个 TableObject 对象代表数据库每个表的一行记录。Fields 属性对应每个表到底有多

少字段。每个字段用 FieldObject 对象来表示,FieldObject 对象 主要有两个属性,key 表

示字段名,value 表示字段值。每个 TableObject 由多个 FieldObject 组成,复杂的还包括多

个子表。一个新增动作的存储过程如图 7-4 所示。

第 7 章 打造自己的 Hibernate

·113·

图 7-1 XML 格式传输数据图

图 7-2 XML 与 TableObject 格式转化图

对于每一个新增动作,我们先调用 BaseService 类的 insert()方法,BaseService 类再调

用 IDAO 的 insert()方法,IDAO 的 insert()方法把 TableObject 作为参数,通过 DAOHelper

类,形成通用的“insert into table name”之类的 JDBC SQL 语句,然后再通过 execute()方

法执行这个 SQL 语句。

这个步骤跟任何数据库无关,跟每个表有多少个字段也无关,因为我们把每个字段都

封装在了 FieldObject 对象里,它是一个 key/value 对象,而多个 FieldObject 对象又被动态

地装在了 TableObject 对象里面,通过 TableObject 再生成通用的“insert into JDBC SQL”语

句,这样,无论是什么表结构,无论界面如何变化,我们存储的这段代码不会发生任何变

化。当然,每个数据库都有自己的特性,比如对于日期型字段,MySQL、DB2、SQL Server

缺省当做字符型存储就可以了,但是 Oracle 必须要用 to_date()函数转换一下,对于极个别

的情况,我们在程序里动态判断一下数据库类型就可以了。

第三部分 构建自己的 SSH 架构

·114·

图 7-3 TableObject 分解图

图 7-4 新增动作的存储过程

第 7 章 打造自己的 Hibernate

·115·

跟 Hibernate 比较起来,这么做的好处是很明显的,那就是我们再也不用管那些复杂的

表结构了,有多少字段都无所谓,一个 TableObject 打遍天下,也不用再生成那些实体类了,

再不用一个一个.hbm 文件与 XML 文件对应着,表结构任意变化也与我无关。当然,缺陷

也是明显的,牺牲了面向对象特性,也没有合适的缓存。

7.2 自动实现多租户

7.2.1 SaaS 管理软件的酸甜苦辣 做项目的时候,我们都有这样的体会,前期并不与客户做深入的沟通,感觉那样做成

本太高,而且客户也抱着不切实际的工期进度与期望值。前期,在工期和快速回款的压力

下,匆忙做出一个版本,结果造成后期需求不断改变,架构和程序也不得不跟着发生剧烈

变化,客户与公司都不满意,程序员流失严重,形成了做项目的一个无底洞。

我认为,SaaS 管理软件之所以从 2008 年的高潮走到 2010 年的低谷,从厂商的角度来

讲,就是抱着跟做项目的心态一样,前期并不与客户沟通,先替客户想会有哪些需求,会

如何操作。即使是 SAP 这样的国际大牌 ERP 厂商,在对待 SaaS 管理软件的态度上,依然

是在幻想着客户先规范自己管理的基础上,做出一套标准的 SaaS 管理软件,试图让用户适

应自己,而不是自己去主动适应客户。这里也许会有一个悖论,如果 SaaS 不讲究统一界面,

统一需求,还叫 SaaS 吗?SaaS 的优势何在?但是如果你不去跟用户沟通,又何必花费这

么多的时间、金钱和精力去做出一套用户并不需要的 SaaS 管理软件呢?

SaaS 管理软件的客户其实是中小企业,而中小企业 迫切的需求是如何生存下来,如

何把东西卖出去,管理也是需要的,但不是第一位的需求。所以,在国内为中小企业做营

销的电子商务的公司,如阿里巴巴、中国化工网、中国制造网一个一个都在香港和国内主

板上市了,而国内做中间件做了很多年的比较大的中间件生产商上海普元,2009 年年底才

融资了 1 亿人民币,并幸福地憧憬 2011 年在国内创业板上市。仅仅是商业模式的不同,却

有如此巨大的差异。

从商业模式上,2010 年比较流行的是 B2B2C(Business to Business to Consumer),

B2B2C 是一种新的网络通信销售方式,第一个 B 指广义的卖方(即成品、半成品、材料提

第三部分 构建自己的 SSH 架构

·116·

供商等),第二个 B 指交易平台,即提供卖方与买方的联系平台,同时提供优质的附加服

务,C 即指买方。卖方不仅仅是公司,可以包括个人,即一种逻辑上的买卖关系中的卖方。

平台绝非简单的中介,而是提供高附加值服务的渠道机构,拥有客户管理、信息反馈、数

据库管理、决策支持等功能的服务平台。买方同样是逻辑上的关系,可以是内部的也可以

是外部的。B2B2C 定义包括了现存的 B2C 和 C2C 平台的商业模式,更加综合化,可以提

供更优质的服务。阿里巴巴在 2010 年展开代号“伏特加”的收购计划,2010 年 6 月 25 日,

阿里巴巴收购美国电子商务 SaaS 提供商 Vendio,Vendio 事实上就是一个 B2B2C 的公司。

那么 B2B2C 为什么那么火呢?跟 SaaS 是什么关系呢?B2B2C 技术架构上,后台不就

是 SaaS 架构吗?换了一个名词而已,不过内涵发生了极其重大的变化。狭义的 SaaS 是从

管理软件的角度来看的,而 B2B2C 是从电子商务的角度来看的。B2B2C 有自己的客户,

就是商城买家,解决了供销一条龙问题,而 SaaS 只解决了中小企业并不是强需求的管理功

能,而 B2B2C 正是从营销的角度来理解 SaaS。

从营销的角度来理解 SaaS,思路转变已经是很不容易的,做起来就更难了。

7.2.2 多租户的实现方式 关于多租户的实现方式,从架构上来讲有如图 7-5 所示的五种启用多租户的代表性方法。

方法一是所有租户共享单一应用程序实例,也就是相同的服务器、中间件和应用程序。

方法五是租户在单独的服务器上运行自己的应用程序实例(当前许多 Application Service

Provider(ASP)采用这种方法)。在这两种方法之间,还有至少三种主要方法,它们具有

不同的资源共享程度和开发复杂性。每种方法在可伸缩性和运营效率方面提供不同的收益,

在开发复杂性和投入市场的时间方面需要不同的成本。

方法二是包含多个应用程序实例和共享地址空间的共享中间件,即与多租户相关的操

作系统、数据库、中间件实例是共享的,如计费等功能,但应用程序是不同的租户有各自

独立的实例。同样,在数据库层,对于租户 A 和 B,把应用程序表 App_table 分别复制

为 App1_table 和 App2_table。与租户相关的定制(例如 CSS 文件和表模式)添加到与租

户相关的应用程序和表复制中。这个模型要求在中间件层维持租户隔离。

方法三是包含多个应用程序实例和单独地址空间的共享中间件;租户使用应用程序的

不同实例,这些实例部署在中间件的不同实例中。租户共享操作系统和服务器。因为中间

件实例是不同的,所以每个租户有自己的操作系统进程(地址空间)。因此,这个模型要求

第 7 章 打造自己的 Hibernate

·117·

在操作系统层维持租户隔离。这种方法在相同的物理服务器上支持的租户数量比方法一和二

少。在三种共享中间件方法中,这种方法提供 强的租户隔离。但是,在操作系统和服务器

层仍然有隔离问题,例如一个租户的用户有可能占用物理服务器中的所有 CPU 和内存。

图 7-5 五种启用多组户的方法

方法四是用与租户相关的虚拟映像实现虚拟化;即使用虚拟化技术在共享服务器上运

行多个操作系统分区,对于每个租户分配专用的应用程序和中间件实例。

从数据库的表结构设计上,多租户与单租户的表结构设计差异也是很大的。在现有的

实现中,主要有三种常见的模型。

(1)独立 Schema 模型。它是 简单的扩展模式,是为每个租户创建一个 Schema,在

每个独立的 Schema 里面自定义数据创建自己的新表。优点是简单,缺点是涉及高成本的

DDL 操作,并且它的整合度不高。

(2)核心的业务数据还是采用独立 Schema,但是像用户登录、菜单权限等功能,放在

一个公共的 Schema 里面,被多个租户共享。好处是比第一种模型有更高的整合度和更少

的 DDL 操作,但是在架构上比私有表更复杂。

(3)所有租户公用一个 Schema。主要通过一个通用表来存放所有自定义信息,里面有

第三部分 构建自己的 SSH 架构

·118·

租户栏位和许许多多统一的数据栏位(比如 500 个)。像这种统一的数据栏位会使用非常灵

活的格式来转储各种类型的数据,比如 VARCHAR。由于在每一行中的数据栏位都会以一

个 Key 对应一个 Value 的形式存放所有自定义数据,导致通用表的行都会很宽,而且会出

现很多空值,所以通用表这种方式也被称为"Sparse Column"。好处是极高的整合度并避免

了 DDL 操作,但是在处理数据方面难度加大。

在数据库的表结构上,我们首先选择模型三——公用 Schema,但也不会做的那么复杂,

对于个性化需求比较强的,我们采用模型一,为他们设置独立的 Schema,而我们的中间件,

将自动适应模型一与模型三,使得我们的架构基本上不用做较大变化就能满足个性化与通

用性的双重需求。这种架构的缺陷是对于独立性比较强的租户,由于采用了独立的 Schema,

虽然对租户本身的使用没有任何影响,但是对于平台本身进行综合数据的查询,处理数据

方面难度设计是一样大的。

7.2.3 自动实现多租户的数据隔离 对于查询和删除操作的数据隔离代码如下所示。

<url-mapping action="querysaleoutstock.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction" description="查询销售出库单">

<query SQL="select DATE_FORMAT(saleoutstockentrydate,'%Y-%m-%d')

saleoutstockentrydate2,DATE_FORMAT(orderdate,'%Y-%m-%d')

orderdate2,saleoutstockentry.*,csname,warehousename from saleoutstockentry

join warehouse on saleoutstockentry.warehouseno=warehouse.warehouseno

left join custsupp on saleoutstockentry.custsuppno=custsupp.csno and

custsupp. tenantno in ('#tenantno#') where

saleoutstockentry.tenantno in ('#tenantno#') and warehouse.tenantno in

('#tenantno#','0000000000' ) and iseshopsaleorder='0'" >

<subquery SQL="select saleoutlineitem.*,goodsname,gtname,packageunit.

puname as packageunitname

from saleoutlineitem,goods,packageunit,goodstype

where saleoutlineitem.goodsno=goods.goodsno and goods.goodsunitno=

packageunit.puno and goods.goodstypeno=goodstype.gtno

and saleoutlineitem.tenantno in ('#tenantno#') and goods.tenantno in

('#tenantno#') and packageunit.tenantno in ('#tenantno#',

'0000000000') and goodstype.tenantno in ('#tenantno#') and

第 7 章 打造自己的 Hibernate

·119·

saleoutlineitem.saleoutstockentryno='?'"/>

<parameter requestname="date1" name="saleoutstockentrydate" type=

"Date" op="&gt;=" or=" saleoutstockentry.status='0' "/>

<parameter requestname="date2" name="saleoutstockentrydate" type=

"Date" op="&lt;=" or=" saleoutstockentry.status='0' "/>

<parameter requestname="saleoutstockentryno" name=

"saleoutstockentryno" type="String"/>

<parameter requestname="csname" name="csname" type="String"/>

<parameter requestname="status" name="saleoutstockentry.status"

type="String" op="="/>

</query>

</url-mapping>

每一个 SQL 语句,我们都会有“#tenantno#”这样的标签,用户只要登录,我们就会

知道这个用户是属于哪个租户,从而也就知道租户的 tenantno,那么在动态拼凑这个 SQL

语句的时候,我们只需要替换掉相应的 tenantno 就可以了,无须程序员写额外的代码。

SQL = SQL.replace("#tenantno#",loginUser.getTenant().getTenantno());

而对于新增和修改数据,我们 终也会生成对应的 SQL 语句:

insert into table1(col1,col2,tenantno) values(“val1”,“val2”,“tenantno”);

update table1 set col1=“val1”,col2=“val2” where tenantno=“tenantno”;

我们这里没有使用 Hibenate,通过自己写代码,实现多租户的技术框架,从而使得程

序员不用再关心多租户是如何实现的。

7.3 本章小结

本章讲述了创建一个自己存储框架的原理,并和 SaaS 数据隔离技术结合起来。SaaS

本身的数据隔离技术原理并不神秘和高深,但要达到企业应用的级别,像 Salesforce 能为

数千万企业用户服务,对于个人开发者来讲,也几乎是不可能完成的任务,它牵扯到的东

西太多,不单纯是数据库的设计问题,还有数据库分区、分表原则,读写分离技术等,都

不是靠一个人的力量就能够完成的,所以从这个角度来说,SaaS 的门槛还是很高的,毕竟

它是基于网站的运营,而不是单套 License 往外卖安装软件。

第三部分 构建自己的 SSH 架构

·120·

第 8 章 打造自己的 Spring

8.1 创建简单的业务流程管理框架

对于以项目为主的公司来说, 想解决的就是在面对每个类似业务的项目时,如何

大程度地共用以前的代码,使得以后的开发成本降到 低,所以,每个公司都想拥有自己

的一套类似“银弹”性质的框架。对于用友、金蝶这样的规模大的公司来说,他们都有自

己的系统架构师和业务架构师一起合作,做出一套公共接口,方便自己的合作伙伴和业务

人员二次实施和开发。但是对于绝大部分实力很弱的中小企业来讲,学习使用 Spring 则是

他们的首选了。不过 Spring 毕竟只是通用的框架,并且 Spring 使得你面向接口编程,实现

代码和实现相分离的的同时,不得不依赖于 Spring 自己的接口,架构不好的话,所有代码

几乎到处都充斥着 Spring 相关的类和接口,即使如此,当面对下面的问题:一个销售出库

单,有的客户是不形成应收账款,直接现金交易,一手交钱,一手交货,而有的客户是货

到付款,或者在京东商城、当当网这样的商城上卖东西,有几周甚至几个月的账期,肯定

是要形成自己的应收账款的,甚至一个客户本身,有的商品是销售后有应收账款,而有的

没有,怎么办?这种和业务密切相关的业务流程管理框架,是 Spring 解决不了的。下面我

们就详细谈谈如何打造自己的业务流程管理框架,使得业务流程“插件式无侵入”按顺序

执行。

例如对于一个以 MIS 系统为主的产品或者项目,比较郁闷的就是每个业务之间的高度

耦合。比如在一个连锁超市为核心业务的作业系统当中,连锁店里 简单的下采购订单业

务,都会有门店增减额度(定金)、增减商品在途数量、生成对账单之类的业务交织在一起。

我们对付这类高耦合的业务通常的办法就是用一个所谓的 Façade 门面设计模式。

第 8 章 打造自己的 Spring

·121·

我做 Eclipse 插件开发,也做了好几年了,在架构上深深被 Eclipse 插件机制所吸引。

如果每个业务之间,也能用插件机制,把以前紧紧耦合的业务逻辑松散一下,就像 Spring

的控制反转(IOC)一样,那该多好啊。

基于这个思路,我根据目前的需求,做了一个这样的框架,代码如下。

<url-mapping action="savecart.action" description="订单存盘" view=

"index.action"

class="com.softbnd.jetblue.saas.eshop.ecshop.action.

CreateCartAction"> <businessprocess description="订单存盘">

<component name="CreatePurchaseOrder" step="1" description="新

增商品订单" class="com.softbnd.jetblue.saas.eshop.ecshop.action.

CreatePurchaseOrderAction"/>

</businessprocess>

</url-mapping>

这是一个 简单的业务耦合的例子,只有两个业务操作,一个是新增销售出库,一个

是新增采购入库。

这个需求是针对商家对商家来的,即 B2B 的需求。当一个网上用户下了采购订单,这

个网上用户也是一个采购商,在网上下自己订单的时候,网上平台本身多了一张销售出库

单,即是 CreateCartAction 干的事情,而对于采购商来讲,他自己本身又多了一张采购订单,

也即 CreatePurchaseOrderAction 干的事情。我们通过 businessprocess 标签,把相对紧密耦

合的两件事情,通过 XML 配置文件分成了相对独立的两部分。CreateCartAction 类和

CreatePurchaseOrderAction 类之间互相独立,彼此在代码这一层没有任何关系,也没有互相

依赖。当然,他们在逻辑上是有紧密联系的。

其类图如图 8-1 所示。

第三部分 构建自己的 SSH 架构

·122·

图 8-1 采购订单类图

其序列图如图 8-2 所示。

图 8-2 采购订单序列图

第 8 章 打造自己的 Spring

·123·

8.2 创建复杂的业务流程管理框架

8.2.1 复杂自定义业务流程框架实例 我们先看一段新增销售出库单的业务流程 XML 业务流程代码。

<url-mapping description="新增销售出库单" action="createsaleoutstockentry.do"

class="com.softbnd.jetblue.saas.scm.sales.action.SaleOutstockEntryAction"> <businessprocess description="新增销售出库单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.SaleOutUpdateStorageLed

gerAction"/>

<component name="updatePurchaseOrder" step="2" d escription="修改销售订单提货数量"

class="com.softbnd.jetblue.saas.scm.sales.action.

UpdateSaleOutOrderCheckOutQTYAction"/>

<component name="purchasepayment" step="3" description="修改销售收款台账"

class="com.softbnd.jetblue.saas.scm.finance.action.

UpdateSaleOutBalanceMoneyAction"/>

<component name="updatePurchaseOrder" step="4" description="为可登录本系统的供货商生成销售出库单"

class="com.softbnd.jetblue.saas.scm.sales.action.

SaleOutstock2SuppEntryAction"/>

</condiation>

</businessprocess>

</url-mapping>

第一步:销售出库单本身数据的添加,我们用类 SaleOutstockEntryAction 来实现;

第二步:新增销售出库单后,修改库存台账,既然是出库了,对应的商品库存量肯定

是减少了,我们用类 UpdateSaleOutOrderCheckOutQTYAction 来实现;

第三步:修改销售订单提货数量,如果这个销售出库单是根据销售订单来的,则出库

后还要修改对应销售订单的出库数量。

第四步:修改销售收款台账,我们用类 UpdateSaleOutBalanceMoneyAction 来实现。既

第三部分 构建自己的 SSH 架构

·124·

然是销售出库,卖出去东西了,肯定会形成应收账款,如果有的客户是先收款,比如已经

用银行转账到商城或者已经用支付宝支付了,则不会形成应收账款,这时我们应该怎么办

呢?系统本身是决定不了这个具体的单据是否会形成应收账款的,除非系统规定所有的销

售出库单都形成应收账款或者都不形成。至少这个租户的规则就是这么规定的,系统才会

自动使用这个默认规则,无须用户参与。但事实上不同租户之间,甚至一个租户的每个单

据之间,业务流程和业务规则都是有可能变化的。有的系统,比如 Oracle 或者 SAP 的 ERP

系统,针对这类需求,也会有自己专门的 BPM(业务流程管理)系统或者业务规则编排引

擎,JBPM 也有自己的业务规则引擎,感兴趣的读者可以去 http://www.jboss.org 下载相应

的版本,我们这里采用人工参与的办法,直接在界面上加一个选择框,由用户去决定这张

单据是否形成应收账款单,界面如图 8-3 所示。

图 8-3 销售出库单界面

如果用户没有选择那个“已付款”选择框,则会形成应收账款,否则,就不会形成应

收账款。当然,用户也有可能会选错,该选择的没有选择,不该选择的选择了,选择错了,

就跟价格、数量录入错误一样,只能用单据红冲了。这时,架构上如何处理“已选择”这

类条件,即我们是否会执行类 UpdateSaleOutBalanceMoneyAction,可以用程序实现判断,

第 8 章 打造自己的 Spring

·125·

加一些 if else 之类的判断,我们也可以用 condiation 条件,让架构自己去判断,而不用每

个特殊情况都用代码来实现。

第五步:为可登录本系统的供货商生成销售出库单。我们开发这个系统的目标既然是

支持“全程电子商务”,那么当我们销售出库时,如果我们的上游批发商也使用这个系统的

话,就可以为他们自动创建一个销售出库单,这样就可以跟他们实现实时财务和库存的自

动更新,大大缩短账期和供货周期。

其类图如图 8-4 所示。

图 8-4 自动创建上游批发商出库单类图

类SaleOutstockEntryAction、SaleOutUpdateStorageLedgerAction、SaleOutstock2SuppEntryAction、

UpdateSaleOutOrderCheckOutQTYAction 以及 UpdateSaleOutBalanceMoneyAction 都 继 承 自

IRequestAction 接口。其序列图如图 8-5 所示。

我们从上面的序列图可以看出,这些应用逻辑之间是没有任何耦合的,代码之间都是

彼此独立的。如果参数 TableObject 之间值的改变没有先后顺序,比如非得是先出库才能减

少库存,那么他们之间的顺序,也是可以任意调整的。

讲解完这个框架的使用场景,下面我们仿照 6.2 节“实现自己的 MVC”来详细描述如

何实现这个框架。解析 XML 还是一样的,我们要有 Businessprocess、Condiation、Component

三个类与之对应,类图如图 8-6 所示。

第三部分 构建自己的 SSH 架构

·126·

图 8-5 自动创建上游批发商出库单序列图

图 8-6 三个重要类图

8.2.2 condiation 标签 所谓 condiation 标签,也就是应用程序执行的条件,如果这个 condiation 成立了,则其

对应的组件 component 就会执行,否则,就不会执行。比如新增销售出库单,只有 status

第 8 章 打造自己的 Spring

·127·

的值为“1”的时候,才会执行下面的流程,否则,只会增加销售出库单本身,修改库存等

业务不会执行。这样设计是由于有的单据在存盘且非审核的状态下,是可以修改出库数量、

价格等数据的,审核后,原始数据不允许再修改了,这时业务数据才会正式出账单。对于

是否形成应收账款这类情况,下面我们通过 XML 配置文件方式去配置,而无须修改我们

的代码,其 XML 文件修改如下。

<url-mapping description="新增销售出库单" action="createsaleoutstockentry.do"

class="com.softbnd.jetblue.saas.scm.sales.action.SaleOutstockEntryAction"> <businessprocess description="新增销售出库单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.SaleOutUpdateStorageLed

gerAction"/>

<component name="updatePurchaseOrder" step="2" escription="修改销售订单提货数量"

class="com.softbnd.jetblue.saas.scm.sales.action.

UpdateSaleOutOrderCheckOutQTYAction"/>

<condiation key="ispaid " type="String" value="0" op="==">

<component name="purchasepayment" step="3" description="修改销售收款台账"

class="com.softbnd.jetblue.saas.scm.finance.action.

UpdateSaleOutBalanceMoneyAction"/>

</condiation>

<component name="updatePurchaseOrder" step="4" description="为可登录本系统的供货商生成销售出库单"

class="com.softbnd.jetblue.saas.scm.sales.action.

SaleOutstock2SuppEntryAction"/>

</condiation>

</businessprocess>

</url-mapping>

Condiation 类是支持嵌套调用的,即 Condiation 条件里面,可以再放 Condiation 条件,

我们将利用这个功能修改新增销售出库单 XML 配置文件。

我们在外层 condiation 条件 status 的值为“1”的情况下,再加一个条件,ispaid 的值

是否为“1”,即用户是否选择了“已付款”那个选择框,如果选择了,则值为 1,就不执

第三部分 构建自己的 SSH 架构

·128·

行销售收款台账了,否则就执行。这样,我们通过配置文件和人工参与编排的方式,实现

了简单的业务流程编码,使得我们的架构有更好的通用性。

8.2.3 component 标签 至于 component 标签,它代表一个实体业务的实现类,比如,

<component name="updatestock"

step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.

SaleOutUpdateStorageLedgerAction"/>

它表示的业务是修改库存台账,实现类是 SaleOutUpdateStorageLedgerAction。其中,

name 和 step、decription 属性,只具有参考意义,程序执行过程当中,它不会影响任何程序

流程,仅供单步调试的时候,为程序员提供信息。

本章详细讲解了如何实现自己的 SSH(Struts、Spring 与 Hibernate)框架,并给出实

现原理和部分源代码,感兴趣的读者可以参考使用。就像法无定法一样,任何框架,都是

有其优点、缺点与适用范围的。我在网上,也会经常浏览一些介绍各类开源框架的网站,

有时间的话,也会去尝试一下,又回到了那句老话:微软阵营遇到问题,常常苦恼找不到

对应的源代码,J2EE 阵营则苦恼类似问题的源代码和解决方案太多,真的不知道如何选择。

条条大路通罗马,但是在去往罗马的道路上,道路还是很曲折和漫长的。

8.3 本章小结

本章是打造自己的 SSH 框架的 后一章,Spring 只是一个通用的框架,而我们在开发

MIS 系统当中,该如何解决业务和业务之间的逻辑耦合(Spring 只是通过控制反转 IOC 解决

了类和类之间的耦合)呢?所以,我们使用了 condiation 和 component 标签,以后还会继续

引入 BusinessProcess 标签以及 Query 和 SubQuery 标签,这就是自己创建框架的好处,可以

根据业务逻辑和产品需求,有目的地更改自己的框架,去适应所要解决的业务需求。

第 8 章 打造自己的 Spring

·129·

第四部分 网上商城完整实现

这部分是一个网上商城的完整实现,包括后台进销存部分和前台模板实现技术。我们

会深入讲解 SSH,讲解网上商城后台进销存的一个完整实现;第 13 章我们讲解如何跟淘宝

互动,以及如何同步淘宝订单的开发技术;第 14 章讲解网上商城前台模板技术和 B2C 网

上商城下订单技术的实现。

第四部分 网上商城完整实现

·130·

第 9 章 网上商城后台之采购管理

9.1 采购订单的实现

所谓采购订单,就是指企业与供应商间的一个购销契约。一个典型的采购订单主界面

如图 9-1 所示。

图 9-1 采购订单主界面

采购订单新增和修改界面如图 9-2 所示。

一张采购订单一般由主子表构成。主表由采购单号、采购订单日期、交货日期、供应

商、交货地点等基本信息组成。子表表示一张采购订单由多个商品组成,即一张采购订单

可以购买多个商品,每个商品都有自己的货号、价格、采购数量等不同信息。

第 9 章 网上商城后台之采购管理

·131·

图 9-2 新增和修改界面

9.1.1 采购订单的数据库设计 采购订单的数据库设计包括主表和子表,如表 9-1 和表 9-2 所示。

表 9-1 采购订单主表(purchaseorder)

字段名称 编码 字段类型 是否主键 是否允许为空

采购订单编码 pono varchar(20) Y(自增) N

订单日期 podate DATE N N

供应商编码 custsuppno varchar(22) N N

交货日期 deliverydate DATE N Y

交货地点 deliveryadd varchar(100) N Y

金额合计 totalmoney numeric(11,2) N N

业务员 empno varchar(10) N Y

其他费用 freight numeric(11,2) N N

备注 Remark varchar(500) N Y

状态(0 新增 1 提交) Status char(1) N N

租户编码 tenantno varchar(22) N N

第四部分 网上商城完整实现

·132·

表 9-2 采购订单子表(polineitem)

字段名称 编码 字段类型 是否主键 是否允许 NULL

采购订单编码 pono varchar(20) N N

子表 ID itemid Integer Y(自增) N

货品编码 goodsno varchar(20) N N

货号 goodsbn varchar(30) N N

规格 specvalues varchar(500) N Y

订单数量 quantity numeric(11,2) N N

到货数量 arrivedquantity numeric(11,2) N Y

价格 Price numeric(11,2) N N

金额 money numeric(11,2) N N 租户编码 tenantno varchar(22) N N

9.1.2 采购订单界面的 Flex 实现 一个完整的采购订单实现的 Flex 源代码由三部分组成:UI 主界面、响应 ActionScript

事件的类、与采购订单主子表对应的 Model 类。

我们先对采购订单 UI 主界面做一下规划,把可以重复利用的组件提取出来,作为公

共组件使用。

(1)选取供应商组件。在整个采购模块,都会用到这个组件。

图 9-3 采购订单 UI 主界面

第 9 章 网上商城后台之采购管理

·133·

其 Flex 代码实现如下。

<?xml version="1.0" encoding="UTF-8"?>

<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" title="请选择供应商" height="300" width="100%" showCloseButton="true"

verticalScrollPolicy="off"

horizontalScrollPolicy="off" borderAlpha="1" alpha="1" backgroundColor

="0xDCDCDC" close="parentDocument.custsuppno.close();">

<mx:Script>

<![CDATA[

import mx.rpc.events.ResultEvent;

import mx.collections.ArrayCollection;

[Bindable] private var custsupps:ArrayCollection = new

ArrayCollection();

private function queryCustResultHandler(event: ResultEvent): void {

custsupps = new ArrayCollection();

for each (var item: XML in event.result.vo){

custsupps.addItem(item);

}

}

private function querySupp():void{

querycustsupp.send();

}

private function selectsupp():void{

if(custsupp_datagrid.selectedItem!=null){

var item:Object = custsupp_datagrid.selectedItem;

parentDocument.custsuppno.label=item.csname;

parentDocument.custsuppno.data=item.csno;

parentDocument.custsuppno.close();

}

}

]]>

</mx:Script>

<mx:HTTPService id="querycustsupp" url="querysupp.do" method="POST"

resultFormat="e4x" result="queryCustResultHandler(event)">

<mx:request xmlns="">

<q_csname>{csname.text}</q_csname>

第四部分 网上商城完整实现

·134·

</mx:request>

</mx:HTTPService> <mx:VBox label="查询" height="100%">

<mx:HBox width="100%" borderStyle="solid"> <mx:Label text="供应商名称" width="20%"/><mx:TextInput id="csname"

width="60%" textAlign="left" />

<mx:Button id="querySuppButton" icon="@Embed('../../images/ asterisk_orange.png')" label="查询"

labelPlacement="right" color="#993300" click="querySupp()"/>

</mx:HBox>

<mx:DataGrid id="custsupp_datagrid" width="100%" height="100%"

headerStyleName="DataGridHeaderStyle"

dataProvider="{custsupps}" click="selectsupp()">

<mx:columns>

<mx:DataGridColumn id="dgColFollowUp" width="45" textAlign=

"left" headerText="序号" sortable="false" dataField="row"/>

<mx:DataGridColumn dataField="csno" headerText="供应商编码"

sortable="true" width="140" textAlign="left" /> <mx:DataGridColumn dataField="csname" headerText="供应商名称"

sortable="true" width="260" textAlign="left" />

</mx:columns>

</mx:DataGrid>

</mx:VBox>

</mx:TitleWindow>

(2)选择商品的界面。当我们新增一个商品的时候,我们按照货品类别、货品名称等

条件查询出现有的商品,然后选择已有的货品,界面如图 9-4 所示。

图 9-4 货品选择界面

第 9 章 网上商城后台之采购管理

·135·

其 Flex 代码实现如下。

<?xml version="1.0" encoding="UTF-8"?>

<mx:VBox width="100%" height="100%" xmlns:mx=http://www.adobe.com/2006/mxml

xmlns:IJetBlueUI="org.bizsolution.jetblue.ui.*" >

<mx:Script>

<![CDATA[

import mx.rpc.events.ResultEvent;

import mx.collections.ArrayCollection;

import mx.events.ListEvent;

import mx.controls.Alert;

[Bindable] private var goods:ArrayCollection = new ArrayCollection();

private function tree_itemClick(evt:ListEvent):void {

var item:Object = evt.currentTarget.selectedItem;

qugoodstypeno.label=item.label;

qugoodstypeno.data=item.data;

}

private function itemClickEvent(event:ListEvent):void {

var rowIndex:int=event.rowIndex;

//selectItem(,true,true);

}

private function popquerygood() : void {

if((goodsname.text==null||goodsname.text=="")&&qugoodstypeno.

data==null){

Alert.show("请选择货品类别或者输入货品名称再查询!");

}else{

popquerygoodService.send();

}

}

private function confirmselectgoods():void{

第四部分 网上商城完整实现

·136·

var items:Array=querygoodsdatagrid.selectedItems;

items=items.sortOn("goodsbn");

var found:int=0;

for each (var it:Object in items){

found=0;

for(var i:int =

0;i<parentDocument.dataObject.lineitems.length;i++) {

var goodsbn:String =

parentDocument.dataObject.lineitems.getItemAt(i)["goodsbn"];

if(goodsbn==it.goodsbn){

found=1;

break;

}

}

if(found==0){

var purorder:Object=new Object();

purorder.quantity=0;

purorder.goodsno=it.goodsno;

purorder.goodsbn=it.goodsbn;

purorder.gtname=it.gtname;

purorder.goodsname=it.goodsname;

purorder.specvalues=it.specvalues;

purorder.puname=it.puname;

purorder.price=it.retailprice;

purorder.taxrate=it.taxrate;

purorder.batchno=it.batchno;

//purorder.purchasepono="";

purorder.money=0;

parentDocument.dataObject.lineitems.addItem(purorder);

}

}

parentDocument.cancelquerylineitem();

第 9 章 网上商城后台之采购管理

·137·

}

private function queryGoodsResultHandler(event: ResultEvent): void {

goods = new ArrayCollection();

for each (var item: XML in event.result.vo){

var good:Object=new Object();

if(item.dtlgoodsbn!=null){

good.goodsbn=item.dtlgoodsbn;

}else{

good.goodsbn=item.goodsbn;

}

good.gtname=item.gtname;

good.goodsno=item.goodsno;

good.goodsname=item.goodsname;

good.specvalues=item.specvalues;

good.puname=item.puname;

good.retailprice=item.retailprice;

good.taxrate=item.taxrate;

good.batchno=item.batchno;

goods.addItem(good); //TODO 货号不同的话,dtlgoodsbn和 goodsbn

}

}

]]>

</mx:Script>

<mx:HTTPService id="popquerygoodService" url="queryallgoods.do" method=

"POST" resultFormat="e4x" result="queryGoodsResultHandler(event)">

<mx:request xmlns="">

<goodstypeno>{qugoodstypeno.data}</goodstypeno>

<goodsname>{goodsname.text}</goodsname>

<status>{0}</status>

</mx:request>

</mx:HTTPService>

<mx:HBox width="100%" borderStyle="solid">

第四部分 网上商城完整实现

·138·

<mx:Label text="货品类别" width="10%"/>

<mx:PopUpButton id="qugoodstypeno" width="160" textAlign="left">

<mx:popUp>

<IJetBlueUI:ExtTree showRoot="false" width="180" rowCount="10"

service="querygoodsType.do" itemClick="tree_itemClick(event);"/>

</mx:popUp>

</mx:PopUpButton>

<mx:Label text="" width="10%"/>

<mx:Label text="货品名称" width="10%"/><mx:TextInput id="goodsname"

width="30%"/>

</mx:HBox>

<mx:HBox>

<mx:Button id="savedtlBtn" label="查询" click="popquerygood()" icon=

"@Embed('../../images/asterisk_orange.png')"/>

<mx:Button id="confirmBtn" label="确认" click="confirmselectgoods()"

icon="@Embed('../../images/asterisk_orange.png')"/>

<mx:Button label="取消" click="parentDocument.cancelquerylineitem()"

icon="@Embed('../../images/asterisk_orange.png')"/>

</mx:HBox>

<IJetBlueUI:DataGridEx id="querygoodsdatagrid" width="100%" height="100%"

headerStyleName="DataGridHeaderStyle" dataProvider="{goods}"

resizableColumns="true"horizontalScrollPolicy="on"verticalScrollPolicy=

"on"

allowMultipleSelection="true" selectionColor="haloGreen" itemClick=

"itemClickEvent(event);">

<IJetBlueUI:columns>

<mx:DataGridColumn dataField="gtname" headerText="货品类别"/>

<mx:DataGridColumn dataField="goodsno" headerText="货品编码"

sortable="true" width="150"/>

<mx:DataGridColumn dataField="goodsbn" headerText="货号" sortable=

"true" width="150"/>

<mx:DataGridColumn dataField="goodsname" headerText="货品名称"

sortable="true" width="200"/>

第 9 章 网上商城后台之采购管理

·139·

<mx:DataGridColumn dataField="specvalues" headerText="规格"

sortable="false"/>

<mx:DataGridColumn dataField="puname" headerText="计量单位" sortable=

"false"/>

<mx:DataGridColumn dataField="retailprice" headerText="价格"

sortable="false"/>

<mx:DataGridColumn dataField="taxrate" headerText="税率" sortable=

"false"/>

<mx:DataGridColumn dataField="batchno" headerText="批次" sortable=

"false"/>

</IJetBlueUI:columns>

</IJetBlueUI:DataGridEx>

</mx:VBox>

除了选择供应商组件和选择商品组件,还有分页组件、查询商品类别的树状组件等。

我们都可以提取出来,当我们提取了所有的公共组件后,图 9-4 所示的 UI 主界面对应的源

代码如下。

<?xml version="1.0" encoding="UTF-8"?>

<mx:Application xmlns:mx=http://www.adobe.com/2006/mxml

horizontalAlign="center" width="100%" height="100%" fontSize="12"

creationComplete="onCreationComplete();"

xmlns:IBusinessUI="org.bizsolution.jetblue.saas.ui.*" xmlns:b2bcomp=

"b2b.*"

xmlns:common="common.*" xmlns:IJetBlueUI="org.bizsolution.jetblue.ui.*"

xmlns:ISaaSPage="org.bizsolution.jetblue.util.*">

<mx:Style source="../../style/main.css"/>

<mx:Script source="../../js/utils.as"/>

<mx:Script source="../../util/Common.as"/>

<mx:Script source="PurchaseOrderlist.as" />

<mx:HTTPService id="srv" url="querypo.do" method="POST" resultFormat=

"e4x" result="resultHandler(event)" fault="faultHander(event)">

<mx:request xmlns="">

<podate1>{DateUtil.getStringDate(podate1.selectedDate)}</podate1>

<podate2>{DateUtil.getStringDate(podate2.selectedDate)}</podate2>

<pono>{qpono.text}</pono>

第四部分 网上商城完整实现

·140·

<csname>{qcustsuppname.text}</csname>

<currentpage>{currentpage.value}</currentpage>

<pagesize>19</pagesize>

</mx:request>

</mx:HTTPService>

<mx:HTTPService id="createaction" url="createpo.do" contentType=

"application/xml" result="refresh(event)"/>

<mx:HTTPService id="updateaction" url="update.do" contentType=

"application/xml"result="refresh(event)"/>

<mx:HTTPService id="removeaction" url="remove.do" contentType=

"application/xml" result="refresh(event)"/>

<mx:HBox width="100%" height="100%">

<mx:VBox width="23%" height="100%">

<mx:TabNavigator id="tn" width="100%" height="100%"

horizontalGap="1"> <mx:VBox label="查询">

<mx:HBox width="100%"> <mx:Label text="订单日期(起)" width="80"/><mx:DateField

id="podate1" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="订单日期(止)" width="80"/><mx:DateField

id="podate2" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="单据编号" width="80"/><mx:TextInput

id="qpono" width="130"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="供应商名称" width="80"/><mx:TextInput

id="qcustsuppname" width="130"/>

</mx:HBox>

<mx:HBox width="100%">

</mx:HBox>

<mx:HBox horizontalAlign ="left" width="100%">

<mx:Button id="queryButton"

第 9 章 网上商城后台之采购管理

·141·

icon="@Embed('../../images/asterisk_orange.png')" label="查询"

labelPlacement="right" color="#993300"

click="query();"/>

</mx:HBox>

</mx:VBox>

<b2bcomp:Pifa/>

<b2bcomp:MySocial/>

</mx:TabNavigator>

</mx:VBox>

<mx:ViewStack id="purchaseorderlist" borderStyle="solid" width="77%"

height="100%">

<mx:VBox width="100%" height="100%">

<mx:VBox width="100%" height="100%" id="mainform">

<mx:HBox horizontalAlign ="right" borderStyle="solid"

height="30" width="100%">

<mx:Button id="addButton" icon="@Embed('../../images/asterisk_orange.png')" label="新增"

labelPlacement="right" color="#993300"

click="newForm();"/>

<mx:Button id="updButton" icon="@Embed('../../images/asterisk_orange.png')" label="修改"

labelPlacement="right" color="#993300"

click="showFrom();"/>

<mx:Button id="removeButton" icon="@Embed('../../images/asterisk_orange.png')" label="删除"

labelPlacement="right" color="#993300"

click="removePurchaseOrder()"/>

<mx:Button id="printButton" icon="@Embed('../../images/asterisk_orange.png')" label="打印"

labelPlacement="right" color="#993300"

click="print()"/>

<mx:Button id="printallButton" icon="@Embed('../../images/asterisk_orange.png')" label="全部打印"

labelPlacement="right" color="#993300"

click="printall()"/>

</mx:HBox> <mx:Label width="100%" text="所有采购订单" fontSize="16"

第四部分 网上商城完整实现

·142·

fontWeight="bold" color="green" textAlign="center"/>

<mx:DataGrid id="maindatagrid" width="120%" height="200"

headerStyleName="DataGridHeaderStyle" resizableColumns="true"

dataProvider="{purchaseOrders}"horizontalScrollPolicy=

"on" rowCount="10" doubleClickEnabled="true"

itemClick ="maindatagridClickEvent(event);"

itemDoubleClick="showDetail();" keyDown="maindatagridKeyDownEvent

(event);"><mx:columns>

<mx:DataGridColumn dataField="pono" headerText=" 单据编号" sortable="true" width="150"/>

<mx:DataGridColumn dataField="custsuppname "headerText="供应商名称" sortable="true" width="240"/>

<mx:DataGridColumn dataField="v_podate" headerText="订单日期" />

<mx:DataGridColumn dataField="v_deliverydate" headerText="交货日期"/>

<mx:DataGridColumn dataField="totalmoney" headerText="总金额(元)" width="120" sortable="false"/>

<mx:DataGridColumn dataField="empno" headerText=" 经手人" sortable="false"/>

<mx:DataGridColumn dataField="deliveryadd" headerText="交货地点" width="150" sortable="false"/>

<mx:DataGridColumn dataField="status_caption" headerText="状态" sortable="false"/>

</mx:columns>

</mx:DataGrid>

<mx:HBox horizontalAlign ="center" height="30"

borderStyle="solid" width="100%"> <mx:Label text="共"/><mx:Label

text="{page.recordcount}"/><mx:Label text="条记录" width="70"/>

<mx:Label text="共"/><mx:Label

text="{page.totalpage}"/><mx:Label text="页"/>

<ISaaSPage:PageBar/>

<mx:NumericStepper value="1" id="currentpage"

minimum="1" stepSize="1"

maximum="{page.totalpage}"

maxChars="{page.totalpage}" change="query()" />

第 9 章 网上商城后台之采购管理

·143·

</mx:HBox> <mx:Label width="100%" text="采购订单明细列表"

fontSize="16" fontWeight="bold" color="green" textAlign="center"/>

<mx:DataGrid id="lineitemdatagrid" width="140%" height=

"100%"

headerStyleName="DataGridHeaderStyle" resizableColumns="true"

dataProvider="{dynapurchaseOrder.lineitems}"

horizontalScrollPolicy="on" verticalScrollPolicy="on" rowCount="5">

<mx:columns>

<mx:DataGridColumn dataField="goodsno" headerText="货品编码" sortable="true" width="150"/>

<mx:DataGridColumn dataField="goodsname" headerText="货品名称" sortable="true" width="200"/>

<mx:DataGridColumn dataField="goodsbn"headerText=" 货号" sortable="true" width="200"/>

<mx:DataGridColumn dataField="specvalues" headerText="规格" width="150" sortable="false"/>

<mx:DataGridColumn dataField="gtname" headerText="货品类别" sortable="false" width="100"/>

<mx:DataGridColumn dataField="puname" headerText="计量单位" sortable="false" />

<mx:DataGridColumn dataField="quantity" headerText=" 采购数量" sortable="false" />

<mx:DataGridColumn dataField="price" headerText="价格" sortable="false" />

<mx:DataGridColumn dataField="taxrate" headerText= "税率" sortable="false"/>

<mx:DataGridColumn dataField="money" headerText="金额" sortable="false"/>

<mx:DataGridColumn dataField="arrivedquantity" headerText="到货数量" sortable="false"/>

</mx:columns>

</mx:DataGrid>

</mx:VBox>

<mx:Canvas borderStyle="solid" width="100%" id=

"lineitemform" visible="false" height="0%">

<mx:Canvas borderStyle="solid" height="100%" width="100%"

第四部分 网上商城完整实现

·144·

id="cgddform" >

<mx:VBox width="100%" height="100%">

<mx:VBox width="100%" height="100%"

id="veditdetail">

<mx:HBox width="100%"> <mx:Label text="订单编号"

width="11%"/><mx:TextInput id="pono" text="{dataObject.pono}" width="23%"

enabled="false" editable="false"

backgroundDisabledColor="0xffffff"/>

<mx:Label text="" width="8%"/> <mx:Label text="订单日期"

width="10%"/><mx:DateField id="podate" editable="true"

selectedDate="{dataObject.podate}" formatString="YYYY-MM-DD" width="20%" />

<mx:Label text="" width="8%"/> <mx:Label text="交货日期"

width="10%"/><mx:DateField id="deliverydate" editable="true"

selectedDate="{dataObject.deliverydate}" formatString="YYYY-MM-DD"

width="20%" />

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="供应商(*)" width="10%"/>

<mx:PopUpButton id="custsuppno"

data="{dataObject.custsuppno}"label="{dataObject.custsuppname}"openAlways

="true"

width="35%" textAlign="left">

<mx:popUp>

<common:Custsupp/>

</mx:popUp>

</mx:PopUpButton>

<mx:Label text="" width="3%"/> <mx:Label text="交货地点"

width="5%"/><mx:TextInput id="deliveryadd" width="47%"

text="{dataObject.deliveryadd}" maxChars="100"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="金额合计"

width="10%"/><mx:TextInput id="totalmoney" text="{dataObject.

第 9 章 网上商城后台之采购管理

·145·

totalmoney}" width="20%"

enabled="false" editable="false"

backgroundDisabledColor="0xffffff"/> <mx:Label text="元" textAlign="left"

width="7%"/> <mx:Label text="经手人"

width="9%"/><mx:TextInput id="empno" text="{dataObject.empno}" width="15%"

maxChars="10"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="备注"

width="9%"/><mx:TextArea id="remark" text="{dataObject.remark}" width=

"91%" height="100" maxChars="500" verticalScrollPolicy="on"/>

</mx:HBox>

<mx:HBox id="lineitemdtlbtngroup"

horizontalAlign="left" borderStyle="solid" height="30" width="100%">

<mx:Button id="addlineitemButton" icon="@Embed('../../images/asterisk_orange.png')" label="新增明细"

labelPlacement="right" color="#993300"

click="addlineitem()"/>

<mx:Button id="dellineitemButton"

click="removelineitem()"

icon="@Embed('../../images/asterisk _orange.png')" label="删除明细"

labelPlacement="right"

color="#993300"/>

</mx:HBox>

<IJetBlueUI:DataGridEx id="lineitemdatagrid2"

width="100%" headerStyleName="DataGridHeaderStyle"

resizableColumns="true" dataProvider="

{dataObject.lineitems}" horizontalScrollPolicy="on"

verticalScrollPolicy="on" rowCount="8"

editable="true" itemEditEnd="itemEditEndHandler(event)">

<IJetBlueUI:columns>

<mx:DataGridColumn dataField="goodsno" headerText="货品编码" sortable="true" width="120" editable="false"

textAlign="left"/>

第四部分 网上商城完整实现

·146·

<mx:DataGridColumn dataField="goodsbn" headerText="货号" sortable="true" width="120" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="goodsname" headerText="货品名称" sortable="true" width="160" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="specvalues" headerText="规格" width="160" sortable="false" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="puname" headerText="计量单位" sortable="false" editable="false"/>

<mx:DataGridColumn dataField="quantity" headerText="采购数量" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text="{data.

quantity}" restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="price" headerText="价格" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text="{data.

price}" restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="taxrate" headerText="税率%" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text="{data.

taxrate}" restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

第 9 章 网上商城后台之采购管理

·147·

</mx:DataGridColumn>

<mx:DataGridColumn dataField="money" headerText="金额" sortable="false" editable="false"/>

</IJetBlueUI:columns>

</IJetBlueUI:DataGridEx>

</mx:VBox>

<mx:HBox id="lineitembtngroup">

<mx:Button id="createSubmitBtn" click= "createSubmit();" label="提交" icon="@Embed('../../images/asterisk_orange.png')"/>

<mx:Button id="cancelBtn" label="取 消 "

click="cancelLineItem();" icon="@Embed('../../images/asterisk_orange.png')"/>

</mx:HBox>

</mx:VBox>

</mx:Canvas>

<mx:Panel height="100%" width="100%" id="cgdddetail" visible="false" title="查询条件">

<common:SelectGoods/>

</mx:Panel>

</mx:Canvas>

</mx:VBox>

</mx:ViewStack>

</mx:HBox>

</mx:Application>

对于 UI 界面事件的响应的 ActionScript 代码,比如 newForm()方法,则对应界面上的

“新增”(<mx:Button id="addButton">)按钮事件,onCreationComplete()方法对应 UI 界面加

载完成时的事件,我们往往在这个方法内做预查询操作,这样用户进入主界面的时候,就

可以看到相应的查询出来的数据,还有打印界面代码等。

[Bindable] public var dataObject: PurchaseOrder = new PurchaseOrder();

[Bindable] private var dynapurchaseOrder: PurchaseOrder = new

PurchaseOrder();

[Bindable] private var purchaseOrders:ArrayCollection = new

ArrayCollection();

[Bindable] private var exportExcelCollection : ArrayCollection = new

第四部分 网上商城完整实现

·148·

ArrayCollection();

[Bindable] private var page:Page = new Page();

private function newForm():void {

veditdetail.enabled=true;

createSubmitBtn.visible=true;

createSubmitBtn.enabled=true;

mainform.visible=false;

lineitemform.height=mainform.height;

mainform.height=0;

lineitemform.visible=true;

dataObject=new PurchaseOrder();

podate.selectedDate=new Date(new Date().getTime());

deliverydate.selectedDate=new Date(new Date().getTime());

deliverydate.setFocus();

}

private function onCreationComplete() : void {

podate1.selectedDate=new Date(new Date().getTime());

podate2.selectedDate=new Date(new Date().getTime());

srv.send();

}

private function itemEditEndHandler(event:DataGridEvent):void {

var grid:DataGrid = event.target as DataGrid;

var field:String = event.dataField;

var row:Number = Number(event.rowIndex);

var col:int = event.columnIndex;

if (grid != null) {

var newValue:Number = Number(grid.itemEditorInstance[grid.

columns[col].editorDataField]);

grid.dataProvider.getItemAt(row)[field]=newValue;

var totmoney:Number=0;

for(var i:int = 0;i<grid.dataProvider.length;i++) {

var quantity:Number = Number(grid.dataProvider.getItemAt(i)

["quantity"]);

第 9 章 网上商城后台之采购管理

·149·

var price:Number = Number(grid.dataProvider.getItemAt(i)

["price"]);

var taxrate:Number = Number(grid.dataProvider.getItemAt(i)

["taxrate"]);

var money:Number= Math.round(quantity*price*(1+taxrate/100)

*100)/100;

grid.dataProvider.getItemAt(i)["money"]=money;

totmoney=totmoney+money;

}

grid.dataProvider.refresh();

totmoney=Math.round(totmoney*100)/100;

totalmoney.text=String(totmoney);

}

}

private function resultHandler(event: ResultEvent): void {

purchaseOrders = dataObject.loadXML(event);

exportExcelCollection=purchaseOrders;

if(purchaseOrders.length!=0){

dynapurchaseOrder=purchaseOrders[0];

}else{

dynapurchaseOrder= new PurchaseOrder();

}

page = new Page();

for each (var item2: XML in event.result.page){

page=page.toPage(item2);

}

}

private function showFrom():void {

if(maindatagrid.selectedItem!=null){

mainform.visible=false;

lineitemform.height=mainform.height;

veditdetail.enabled=true;

mainform.height=0;

lineitemform.visible=true;

第四部分 网上商城完整实现

·150·

dataObject=dynapurchaseOrder;

deliverydate.setFocus();

}else{ Alert.show("请选择一条记录再进行修改");

}

}

private function removePurchaseOrder():void {

if(maindatagrid.selectedItem!=null){

dataObject=dynapurchaseOrder;

var xml:XML =dataObject.toXML(dataObject);

removeaction.send(xml);

dynapurchaseOrder= new PurchaseOrder();

}else{ Alert.show("请选择一条记录再进行删除");

}

}

private function print():void {

if(maindatagrid.selectedItem!=null){

var printJob:FlexPrintJob = new FlexPrintJob();

dataObject=dynapurchaseOrder;

if(printJob.start()) {

var formPrintView:POPrintView = new POPrintView();

addChild(formPrintView); formPrintView.pono.text ="订单编号: " + dataObject.pono +" ";

formPrintView.podate.text ="订单日期: " + dataObject.v_podate+"";

formPrintView.custsuppname.text ="供应商名称:"+dataObject.

custsuppname +" "; formPrintView.deliverydate.text ="交货日期: " + dataObject.

v_deliverydate +" "; formPrintView.deliveryadd.text ="交货地点: " + dataObject.

deliveryadd +" "; formPrintView.totalmoney.text ="金额合计: " + dataObject.

totalmoney +" 元";

formPrintView.empno.text ="经手人: " + dataObject.empno +" ";

formPrintView.printdate.text ="打印日期: " +DateUtil.getStringDate

第 9 章 网上商城后台之采购管理

·151·

(new Date(new Date().getTime())) +" ";

formPrintView.myDataGrid.dataProvider=dataObject.lineitems;

printJob.addObject(formPrintView);

printJob.send();

removeChild(formPrintView);

}

}else{ Alert.show("请选择一条记录再进行打印!");

}

}

private function getPurchaseorder():XML{

dataObject.pono=pono.text;

dataObject.podate=podate.selectedDate;

dataObject.custsuppno=String(custsuppno.data);

dataObject.deliverydate=deliverydate.selectedDate;

dataObject.deliveryadd=deliveryadd.text;

dataObject.totalmoney=Number(totalmoney.text);

dataObject.remark=remark.text;

dataObject.empno=empno.text;

var xml:XML =dataObject.toXML(dataObject);

return xml;

}

private function createSubmit():void {

if(valid()){

if(dataObject.lineitems.length==0){ Alert.show("请添加货品!");

}else{

var cansubmit :Boolean=true;

for(var i:int = 0;i<dataObject.lineitems.length;i++) {

var money:Number = Number(dataObject.lineitems.getItemAt

(i)["money"]);

if(money==0){

cansubmit=false;

break;

}

第四部分 网上商城完整实现

·152·

}

if(cansubmit){

dataObject.status="1";

var xml:XML =getPurchaseorder();

createaction.send(xml);

cancelLineItem();

}else{ Alert.show("请检查货品数量、价格,金额不能是 0!");

}

}

}

}

private function valid():Boolean{

var ret:Boolean=true;

if(custsuppno.data==null||custsuppno.data==""){ Alert.show("请选择供应商!");

ret=false;

}else if(deliveryadd.length > 100){ Alert.show("送货输入超长!");

ret=false;

}else if(empno.length > 10){ Alert.show("经手人输入超长!");

ret=false;

}

return ret;

}

而对于 Model 类,PurchaseOrder.as,其源代码如下。

package {

[Bindable]

public class PurchaseOrder extends Object {

public var pono:String = "";

public var podate:Date ;

public var v_podate:String ;

public var custsuppno:String = "";

public var custsuppname:String = "";

public var deliverydate:Date;

第 9 章 网上商城后台之采购管理

·153·

public var v_deliverydate:String;

public var deliveryadd:String="";

public var totalmoney:Number=0;

public var empno:String="";

public var remark:String = "";

public var status:String = "";

public var status_caption:String = "";

public var lineitems : ArrayCollection;

public function PurchaseOrder() {

super();

lineitems= new ArrayCollection();

}

public function loadXML(event: ResultEvent):ArrayCollection{

var purchaseOrders:ArrayCollection = new ArrayCollection();

for each (var item: XML in event.result.vo) {

var purchaseOrder: PurchaseOrder = new PurchaseOrder();

purchaseOrder.pono=item.pono;

purchaseOrder.podate=DateUtil.parse(item.podate);

purchaseOrder.v_podate=item.podate;

purchaseOrder.custsuppno=item.custsuppno;

purchaseOrder.custsuppname=item.custsuppname;

purchaseOrder.deliverydate=DateUtil.parse(item.deliverydate);

purchaseOrder.v_deliverydate=item.deliverydate;

purchaseOrder.deliveryadd=item.deliveryadd;

purchaseOrder.totalmoney=Number(item.totalmoney);

purchaseOrder.remark=item.remark;

purchaseOrder.status=item.status;

purchaseOrder.empno=item.empno;

if(purchaseOrder.status=="0"){

purchaseOrder.status_caption="存盘";

}else{

purchaseOrder.status_caption="提交";

}

purchaseOrders.addItem(purchaseOrder);

第四部分 网上商城完整实现

·154·

for each (var child: XML in event.result.vo.children){

if(child.pono==purchaseOrder.pono){

purchaseOrder.lineitems.addItem(child);

}

}

}

return purchaseOrders;

}

public function toXML(purchaseOrder:Object):XML{

var df:DateFormatter=new DateFormatter();

df.formatString="YYYY-MM-DD";

var podate:String=df.format(purchaseOrder.podate);

var deliverydate:String=df.format(purchaseOrder.deliverydate);

var xml:XML =

<purchaseOrder tablename="purchaseorder" prefix="po">

<pono key="pono" value={purchaseOrder.pono} type="String"

primaryKey="true" autogenerate="true"/>

<podate key="podate" value={podate} type="Date"/>

<custsuppno key="custsuppno" value={purchaseOrder.custsuppno}

type="String"/>

<deliverydate key="deliverydate" value={deliverydate}

type="Date"/>

<deliveryadd key="deliveryadd"

value={purchaseOrder.deliveryadd} type="String"/>

<totalmoney key="totalmoney" value={purchaseOrder.

totalmoney}

type="Number"/>

<empno key="empno" value={purchaseOrder.empno}

type="String"/>

<remark key="remark" value={purchaseOrder.remark}

type="String"/>

<status key="status" value={purchaseOrder.status}

type="String"/>

</purchaseOrder>

var doc:XMLDocument =new XMLDocument();

第 9 章 网上商城后台之采购管理

·155·

var childroot:XMLNode =doc.createElement("children");

childroot.attributes.tablename = "polineitem";

for(var i:int = 0;i<purchaseOrder.lineitems.length;i++) {

var child:XMLNode =doc.createElement("polineitem");

var quantity:Number =

Number(purchaseOrder.lineitems.getItemAt(i)["quantity"]);

var price:Number =

Number(purchaseOrder.lineitems.getItemAt(i)["price"]);

var taxrate:Number =

Number(purchaseOrder.lineitems.getItemAt(i)["taxrate"]);

var money:Number =

Number(purchaseOrder.lineitems.getItemAt(i)["money"]);

var goodsno:String = purchaseOrder.lineitems.getItemAt(i)

["goodsno"];

var goodsbn:String = purchaseOrder.lineitems.getItemAt(i)

["goodsbn"];

var specvalues:String =

purchaseOrder.lineitems.getItemAt(i)["specvalues"];

var arrivedquantity:Number =0;

var itemidNode:XMLNode =doc.createElement("itemid");

itemidNode.attributes.type="Long";

itemidNode.attributes.value=0;

itemidNode.attributes.key="itemid";

itemidNode.attributes.primaryKey="true";

itemidNode.attributes.autogenerate="true";

child.appendChild(itemidNode);

var goodsnoNode:XMLNode =doc.createElement("goodsno");

goodsnoNode.attributes.type="String";

goodsnoNode.attributes.value=goodsno;

goodsnoNode.attributes.key="goodsno";

child.appendChild(goodsnoNode);

var goodsbnNode:XMLNode =doc.createElement("goodsbn");

goodsbnNode.attributes.type="String";

goodsbnNode.attributes.value=goodsbn;

第四部分 网上商城完整实现

·156·

goodsbnNode.attributes.key="goodsbn";

child.appendChild(goodsbnNode);

var specvaluesNode:XMLNode =doc.createElement("specvalues");

specvaluesNode.attributes.type="String";

specvaluesNode.attributes.value=specvalues;

specvaluesNode.attributes.key="specvalues";

child.appendChild(specvaluesNode);

var quantityNode:XMLNode =doc.createElement("quantity");

quantityNode.attributes.type="Number";

quantityNode.attributes.value=quantity;

quantityNode.attributes.key="quantity";

child.appendChild(quantityNode);

var priceNode:XMLNode =doc.createElement("price");

priceNode.attributes.type="Number";

priceNode.attributes.value=price;

priceNode.attributes.key="price";

child.appendChild(priceNode);

var taxrateNode:XMLNode =doc.createElement("taxrate");

taxrateNode.attributes.type="Number";

taxrateNode.attributes.value=taxrate;

taxrateNode.attributes.key="taxrate";

child.appendChild(taxrateNode);

var moneyNode:XMLNode =doc.createElement("money");

moneyNode.attributes.type="Number";

moneyNode.attributes.value=money;

moneyNode.attributes.key="money";

child.appendChild(moneyNode);

var arrivedquantityNode:XMLNode

=doc.createElement("arrivedquantity");

arrivedquantityNode.attributes.type="Number";

arrivedquantityNode.attributes.value=arrivedquantity;

第 9 章 网上商城后台之采购管理

·157·

arrivedquantityNode.attributes.key="arrivedquantity";

child.appendChild(arrivedquantityNode);

childroot.appendChild(child);

}

xml.appendChild(childroot);

return xml;

}

}

}

Model 类的作用是把主界面上的所有数据,包括主表和子表的数据打包为一个 XML

数据格式文件,然后通过 Flex 本身的的 HttpService 组件传给后台。

9.1.3 采购订单序列化数据到数据库 我并没有使用任何 Flex 的第三方 MVC 框架,例如 Blaze、Cairngorm、Model-Glue 等

比较著名的 Flex 框架,主要是感觉非常的不好用。所以我就自己写了一个框架,通过接收

HttpService 传过来的数据,自动完成数据的存盘(包括增加、修改、删除)操作。

其 XML 定义如下。

<url-mapping action="createpo.do"

class="com.softbnd.jetblue.saas.webframework.action.CreateAction" description="新增采购订单"/>

我们只使用了默认的 CreateAction 就完成了采购订单的存盘操作,包括主子表的存盘,

没有做任何额外的代码编写。当然,由于采购订单没有额外的业务逻辑,所以我们就使用

了缺省的存盘实现。下面我们还会看到,对于采购入库操作,存盘后后续业务逻辑很多,

我们将继承 CreateAction,把耦合性很强的业务逻辑彻底分离出来。CreateAction 的类图如

图 9-5 所示。

第四部分 网上商城完整实现

·158·

图 9-5 CreateAction 类图

9.1.4 采购订单查询的实现 我们在 UI 主界面的源代码里面,可以看到查询的定义如下。

<mx:HTTPService id="srv" url="querypo.do" method="POST" resultFormat="e4x"

result="resultHandler(event)" fault="faultHander(event)">

<mx:request xmlns="">

<podate1>{DateUtil.getStringDate(podate1.selectedDate)}

</podate1>

<podate2>{DateUtil.getStringDate(podate2.selectedDate)}

第 9 章 网上商城后台之采购管理

·159·

</podate2>

<pono>{qpono.text}</pono>

<csname>{qcustsuppname.text}</csname>

<currentpage>{currentpage.value}</currentpage>

<pagesize>19</pagesize>

</mx:request>

</mx:HTTPService>

这里,我们的查询操作有 6 个参数。

(1)podate1:查询采购订单的开始日期。

(2)podate2:查询采购订单的结束日期。

(3)pono:按照采购订单号查询。

(4)csname:按照供应商查询。

(5)currentpage:分页时用,表示当前页。

(6)pagesize:每页显示多少条记录。这里每页缺省显示 19 条记录。

其查询操作对应的 XML 对应如下。

<url-mapping action="querypo.do" description="查询采购订单"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select purchaseorder.*,custsupp.csname as custsuppname

from

purchaseorder,custsupp where purchaseorder.custsuppno=custsupp.csno order

by

purchaseorder.pono and purchaseorder.tenantno in('#tenantno#') and

custsupp.tenantno

in('#tenantno#')">

<subquery fk="pono" SQL="select polineitem.*,gtname ,puname,

goodsname

from polineitem ,goods,packageunit,goodstype where

polineitem.goodsno=goods.goodsno

and goods.goodsunitno=packageunit.puno and goods.goodstypeno=

goodstype.gtno

and polineitem.pono='?' and polineitem.tenantno in('#tenantno#')

and goods.tenantno in('#tenantno#') and packageunit.tenantno in('#tenantno#')

第四部分 网上商城完整实现

·160·

and goodstype.tenantno

in('#tenantno#') " />

<parameter requestname="podate1" name="podate" type="Date" op=

"&gt;=" or=" purchaseorder.status='0' "/>

<parameter requestname="podate2" name="podate" type="Date" op=

"&lt;=" or=" purchaseorder.status='0' "/>

<parameter requestname="pono" name="pono" type="String"/>

<parameter requestname="csname" name="custsupp.csname" type=

"String"/>

</query>

</url-mapping>

我们可以看到,查询采购订单 querypo.do 对应 4 个参数,分别与主界面 4 个参数对应,

至于当前页 currentpage 和每页显示多少条记录 pagesize,由于在所有的页面中都有这两个

参数,所以就把他们单独处理了,而没有放在查询参数中。

对于查询,我们使用了默认的 LogonQueryAction,没有额外编写任何编码。对于多个

参数,我们也通过自己的 Framework 屏蔽掉了,只需要在 XML 文件里面配置就可以了,

Java 代码无须有任何变化。

9.1.5 采购订单的打印 对于采购订单的打印,由于我们是采用 Flex 开发的,而打印对于 Flex 来说,是非常

简单的,我们只需要编写出一个指定格式的界面就可以了,无须考虑打印机的问题,还可

以直接导出 PDF 文件。采购订单打印界面效果如图 9-6 所示。

图 9-6 采购订单打印界面

如果我们想导出 PDF 格式,只需要在选择打印机的时候选择 PDF 打印机就可以了,

如图 9-7 所示。

第 9 章 网上商城后台之采购管理

·161·

图 9-7 打印机选择界面

如果我们的打印机没有 Adobe PDF 选项,说明我们的机器没有安装 Adobe 公司的

Adobe Reader 软件,安装上这个软件就可以了。

其打印的 Flex 源代码如下。

<?xml version="1.0" encoding="UTF-8"?>

<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml"

backgroundColor="#FFFFFF" height="750" width="1000"

paddingTop="50" paddingLeft="50" paddingRight="50"> <mx:Label width="1000" text="采购订单" fontSize="16" fontWeight="bold"

textAlign="center"/>

<mx:HBox>

<mx:Label id="pono" width="200"/>

<mx:Label id="podate" width="200"/>

<mx:Label id="custsuppname" text="" width="400"/>

</mx:HBox>

<mx:HBox>

<mx:Label id="deliverydate" width="200"/>

<mx:Label id="totalmoney" text="" width="200"/>

<mx:Label id="deliveryadd" width="400"/>

</mx:HBox>

<mx:PrintDataGrid id="myDataGrid" width="100%">

<mx:columns> <mx:DataGridColumn dataField="goodsno" headerText="货品编码"

第四部分 网上商城完整实现

·162·

width="150"/> <mx:DataGridColumn dataField="goodsname" headerText="货品名称"

width="200"/> <mx:DataGridColumn dataField="specvalues" headerText="规格"

width="150" /> <mx:DataGridColumn dataField="gtname" headerText="货品类别"

width="100"/> <mx:DataGridColumn dataField="puname" headerText="计量单位"

width="70"/> <mx:DataGridColumn dataField="quantity" headerText="采购数量"

width="80"/> <mx:DataGridColumn dataField="price" headerText="价格" width=

"100"/> <mx:DataGridColumn dataField="money" headerText="金额" width=

"100"/>

</mx:columns>

</mx:PrintDataGrid>

<mx:HBox>

<mx:Label id="empno" width="200"/>

<mx:Label id="printdate" text="" width="200"/>

</mx:HBox>

</mx:VBox>

对应的 ActionScript 代码如下。

private function print():void {

if(maindatagrid.selectedItem!=null){

var printJob:FlexPrintJob = new FlexPrintJob();

dataObject=dynapurchaseOrder;

if(printJob.start()) {

var formPrintView:POPrintView = new POPrintView();

addChild(formPrintView); formPrintView.pono.text ="订单编号: " + dataObject.pono +" ";

formPrintView.podate.text ="订单日期:" + dataObject.v_podate+" ";

formPrintView.custsuppname.text ="供应商名称:"+ dataObject.

custsuppname+""; formPrintView.deliverydate.text ="交货日期: " + dataObject.

v_deliverydate +" ";

第 9 章 网上商城后台之采购管理

·163·

formPrintView.deliveryadd.text ="交货地点: " + dataObject.

deliveryadd +" "; formPrintView.totalmoney.text ="金额合计: " + dataObject.

totalmoney +" 元";

formPrintView.empno.text ="经手人: " + dataObject.empno +" ";

formPrintView.printdate.text ="打印日期:"+DateUtil.getStringDate

(new Date(new Date().getTime())) +" ";

formPrintView.myDataGrid.dataProvider=dataObject.lineitems;

printJob.addObject(formPrintView);

printJob.send();

removeChild(formPrintView);

}

}else{ Alert.show("请选择一条记录再进行打印!");

}

}

9.2 采购入库的实现

所谓采购入库单,指对于采购的货品,存放到仓库的业务活动。一个典型的采购入库

单主界面如图 9-8 所示。采购入库的表结构主要包括采购日期、供货商、采购商品,数量

和金额等。

图 9-8 采购入库单主界面

第四部分 网上商城完整实现

·164·

新增和修改界面如图 9-9 所示。

图 9-9 新增和修改界面

9.2.1 采购入库单的数据库设计 采购入库单的主表和子表的设计如表 9-3、表 9-4 所示。

表 9-3 采购入库单主表(purchaserk)

字段名称 编码 字段类型 是否主键 是否允许 NULL 采购入库编号 purchaserkno varchar(20) Y(自增) N 采购订单号 purchaseorderno varchar(20) N Y 入库日期 purchaserkdate DATE N N 供应商编码 custsuppno varchar(22) N N 仓库编码 warehouseno varchar(20) N N 金额合计 totalmoney numeric(11,2) N N 是否生成应付款 reducingflag varchar(20) N N 已付金额 paiedmoney numeric(11,2) N Y 已退金额 returnedmoney numeric(11,2) N Y 业务员 empno varchar(10) N Y 备注 remark varchar(500) N Y 状态(0 新增 1 提交) status char(1) N N 租户编码 tenantno varchar(22) N N

第 9 章 网上商城后台之采购管理

·165·

表 9-4 采购入库单子表(purchaserklineitem)

字段名称 编码 字段类型 是否主键 是否允许 NULL

采购主表 no purchaserkno varchar(20) N N

采购订单号 purchaseorderno varchar(20) N Y

子表 ID itemid Integer Y(自增) N

货品编码 goodsno varchar(20) N N

货号 goodsbn varchar(30) N N

批次 batchno varchar(30) N N

规格 specvalues varchar(500) N Y

入库数量 quantity numeric(11,2) N N

退货数量 returnedquantity numeric(11,2) N Y

价格 Price numeric(11,2) N N

金额 money numeric(11,2) N N

租户编码 tenantno varchar(22) N N

9.2.2 采购入库单界面的 Flex 实现 一个完整的采购入库单界面跟采购订单是一样的,也是由三部分组成:UI 主界面、响

应 ActionScript 事件的类与采购订单主子表对应的 Model 类。

在采购订单一节,我们分离了一些可以复用的组件,例如选取供应商组件、选择商品

的界面等,采购入库单将复用这些组件。

UI 主界面和采购订单结构差不多,我这里只列出不一样的地方,采购入库单有可能由

采购订单转化而来,也可能没有采购订单,直接入库。我把与采购订单有关联的源代码列

出来,当选择一个供应商后,如果该供应商有采购订单,则采购订单会自动出现在采购订

单的下拉列表当中,UI 界面如图 9-10 所示。

Flex 源代码如下。

<mx:HTTPService id="querycustsupp" url="querycustsupp2comp.do" method=

"POST" resultFormat="e4x" result="queryCustResultHandler(event)">

<mx:request xmlns="">

<csname>{q_csname.text}</csname>

</mx:request>

</mx:HTTPService>

第四部分 网上商城完整实现

·166·

<mx:HTTPService id="querypurchaseorder" url="querypurchaseorder.do"

method="POST" resultFormat="e4x" result="queryOrderResultHandler(event)">

<mx:request xmlns="">

<custsuppno>{custsuppno.data}</custsuppno>

</mx:request>

</mx:HTTPService>

<mx:HBox width="100%"> <mx:Label text="供应商(*)" width="10%"/>

<mx:PopUpButton id="custsuppno"

data="{purchaserk.custsuppno}" label="{purchaserk.custsuppname}"

openAlways="true"

width="35%" textAlign="left">

<mx:popUp> <mx:TitleWindow id="custsuppnoWin" title="请选择供应商:" height="300"

width="500" showCloseButton="true" verticalScrollPolicy="on"

horizontalScrollPolicy="off"borderAlpha="1" alpha=

"1"backgroundColor="0xDCDCDC" close="custsuppno.close();"> <mx:VBox label="查询" height="100%">

<mx:HBox width="100%" borderStyle="solid"> <mx:Label text="供应商名称"

width="10%"/><mx:TextInput id="q_csname" width="50%" textAlign="left" />

<mx:Button id="querySuppButton" icon="@Embed('../../images/asterisk_orange.png')" label="查询"

labelPlacement="right" color="#993300" click="querySupp()"/>

</mx:HBox>

<mx:DataGrid id="custsupp_datagrid" width="100%" height="100%"

headerStyleName="DataGridHeaderStyle"

dataProvider="{custsupps}" click="selectsupp()">

<mx:columns>

<mx:DataGridColumn

id="dgColFollowUp" width="45" textAlign="left" headerText="序号"

sortable="false" dataField="row"/>

<mx:DataGridColumn dataField="csno" headerText="供应商编码" sortable="true" width="140" textAlign=

"left" />

<mx:DataGridColumn

第 9 章 网上商城后台之采购管理

·167·

dataField="csname" headerText=" 供 应 商 名 称 " sortable="true" width="260"

textAlign="left" />

</mx:columns>

</mx:DataGrid>

</mx:VBox>

</mx:TitleWindow>

</mx:popUp>

</mx:PopUpButton>

<mx:Label text="" width="3%"/> <mx:Label text="选择订单" width="7%"/>

<mx:PopUpButton id="purchaseorderno"

width="35%" textAlign="left" open="checkcustsupp()" data="{purchaserk.

purchaseorderno}"

label="{purchaserk.purchaseorderno}">

<mx:popUp> <mx:TitleWindow id="orderWin" title="请选

择该供应商订单:" height="300" width="500" showCloseButton="true" verticalScrollPolicy

="off"

horizontalScrollPolicy="off"borderAlpha=

"1" alpha="1" backgroundColor ="0xDCDCDC"

close="purchaseorderno.close();" > <mx:VBox label="查询" height="100%"

width="100%">

<mx:DataGrid id="order_datagrid"

width="100%" height="100%" headerStyleName="DataGridHeaderStyle"

dataProvider="{purOrders}"

allowMultipleSelection="false" itemClick="selectpurorder(event)">

<mx:columns>

<mx:DataGridColumn dataField="pono" headerText=" 订 单 号 " sortable="true" width="140"

textAlign="left" />

<mx:DataGridColumn dataField="v_podate" headerText="开单日期" sortable="true" textAlign="left" />

<mx:DataGridColumn dataField="totalmoney" headerText="金额" sortable="true" textAlign="left" />

<mx:DataGridColumn dataField="v_deliverydate" headerText=" 交 货 日 期 " sortable="true"

第四部分 网上商城完整实现

·168·

textAlign="left" />

</mx:columns>

</mx:DataGrid>

</mx:VBox>

</mx:TitleWindow>

</mx:popUp>

</mx:PopUpButton>

</mx:HBox>

这里,我使用了一个技巧,就是组合了 Flex 的 PopUpButton、PopUp 组件与 TitleWindow、

DataGrid 组件,这 4 个组件合在一起,形成了一个新的组件,UI 界面显示出来,类似于

C/S 架构下开发利器 PowerBuilder 的 DataWindow 窗口。这也是我为啥选择 Flex 而没有选

择 ZK 的原因。当然,现在 ExtJS 也有类似的功能了,并且 ExtJS 还有面向移动的解决方案,

由于其采用的是 JavaScript、CSS 以及 HTML,所以可以同时应用于 iPhone 与 Android 平

台,而 Flash 则由于 iPhone 的抵制,目前只能应用于 Android 平台。

相应 ActionScript 源代码如下。

private function querySupp():void{

querycustsupp.send();

}

private function queryCustResultHandler(event: ResultEvent): void {

custsupps = new ArrayCollection();

for each (var item: XML in event.result.vo){

custsupps.addItem(item);

}

}

private function selectsupp():void{

if(custsupp_datagrid.selectedItem!=null){

var item:Object = custsupp_datagrid.selectedItem;

custsuppno.label=item.csname;

custsuppno.data=item.csno;

purchaserk.lineitems=new ArrayCollection();

purchaseorderno.label="";

purchaseorderno.data=null;

第 9 章 网上商城后台之采购管理

·169·

querypurchaseorder.send();

custsuppno.close();

}

}

private function queryOrderResultHandler(event: ResultEvent): void {

purOrders = new ArrayCollection();

for each (var item: XML in event.result.vo) {

var purchaseOrder: PurchaseOrder = new PurchaseOrder();

purchaseOrder.pono=item.pono;

purchaseOrder.v_podate=item.podate;

purchaseOrder.v_deliverydate=item.deliverydate;

purchaseOrder.totalmoney=item.totalmoney;

purOrders.addItem(purchaseOrder);

for each (var child: XML in event.result.vo.children){

if(child.pono==purchaseOrder.pono){

purchaseOrder.lineitems.addItem(child);

}

}

}

}

Action 在 XML 文件里的定义如下。

<url-mapping action="querycustsupp2comp.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select csno,csname from custsupp where status='0' and

(custtype='1' or custtype='2') and tenantno in ('#tenantno#') order by csno">

<parameter name="csname" type="String"/>

</query>

</url-mapping>

<url-mapping description="查询采购订单,为采购入库服务"

action="querypurchaseorder.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select distinct purchaseorder.pono ,

DATE_FORMAT(podate,'%Y-%m-%d') podate,DATE_FORMAT(deliverydate,'%Y-%m-%d')

deliverydate,totalmoney from

第四部分 网上商城完整实现

·170·

purchaseorder,polineitem where polineitem.quantity>arrivedquantity and

polineitem.pono=purchaseorder.pono and purchaseorder.status='1' and

purchaseorder.tenantno in ('#tenantno#') and polineitem.tenantno in

('#tenantno#')">

<parameter requestname="custsuppno" name="custsuppno" type=

"String" op="="/>

<subquery SQL="select

polineitem.*,issale,salescale,goodsname,goodstype.gtname as goodstypename,

puname from polineitem,goods,packageunit,goodstype where polineitem. goodsno=

goods.goodsno and goods.goodsunitno=packageunit.puno and goods.goodstypeno=

goodstype.gtno and polineitem.quantity>arrivedquantity and polineitem. pono='?'

and polineitem.tenantno in ('#tenantno#') and goods.tenantno in ('#tenantno#')

and

packageunit.tenantno in ('#tenantno#','0000000000') and

goodstype.tenantno in ('#tenantno#','0000000000') "/>

</query>

</url-mapping>

9.2.3 采购入库单序列化数据到数据库 在采购订单一节,我们使用了默认的 CreateAction 就序列化采购订单数据到数据库了。

对于采购入库单的存盘,则没有那么简单了。我们先看采购入库单存盘动作的业务逻辑。

<url-mapping action="createpurchaserk.do" description="新增采购入库"

class="com.softbnd.jetblue.saas.scm.purchase.action.PurchaseRkAction"> <businessprocess description="新增采购入库">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存

台账" class="com.softbnd.jetblue.saas.scm.inventory.action.

PurchaseChckinStorageLedgerAction"/>

<component name="updatePurchaseOrder" step="2" description=" 修改采购订单到货数量"

class="com.softbnd.jetblue.saas.scm.purchase.action.UpdatePurchaseOrderA

rrivedQTYAction"/>

</condiation>

<condiation key="reducingflag" type="String" value="1" op="=="> <component name="purchasepayment" step="3" description="新增

第 9 章 网上商城后台之采购管理

·171·

应付款"

class="com.softbnd.jetblue.saas.scm.finance.action.

PurchasePaymentAction"/>

</condiation>

</businessprocess>

</url-mapping>

第一步:采购入库单存盘;

第二步:修改库存台账,因为入库了,则货品对应库存肯定增加了;

第三步:回填采购订单到货数量;

第四步:新增应付账款。

对于这类耦合性很强的业务逻辑,我们以前的通常做法是弄一个 Service 接口和 Service

实现。比如 PurchaseInService,代表采购入库服务,然后把这四个步骤统统封装在一个

PurchaseInService 代码里面,一旦用户逻辑发生任何变化,比如:有的租户需要生成应付

款,有的不需要,这样做起来,就很麻烦了。而我们只需要简单的配置这个 XML 文件,

就可以动态地增加、删除自己的业务逻辑,实现真正的插件化管理。

PurchaseRkAction.java 的源代码如下。

public class PurchaseRkAction extends CreateAction{

public PurchaseRkAction(){

super();

}

@Override

public String getprefixword() {

return "PI";

}

}

这里,PurchaseRkAction 仅仅是简单地继承了一下 CreateAction,就完成了采购入库的

主子表存盘。getprefixword()方法的作用是当我们生成采购入库单的入库单号的时候,在前

面加一个前缀“PI”,比如:PI201007130001,就代表 2010 年 7 月 13 日录入的第一张采购

入库单。

第四部分 网上商城完整实现

·172·

修改库存台账,PurchaseChckinStorageLedgerAction.java 源代码如下。

public class PurchaseChckinStorageLedgerAction extends DefaultAction

implements IAction{

/** * 新增采购入库/直接入库,更改库存

* @throws Exception

*/

public TableObject execute(TableObject vo,UTransaction uta,Connection

conn ) throws Exception {

InventoryledgerService service=new InventoryledgerService();

for (TableObject child : vo.getChildren()) {

FieldObject fo=child.getFieldObject("issale");

if(fo!=null){

fo.setGenerateSQL(true);

}

fo=child.getFieldObject("salescale");

if(fo!=null){

fo.setGenerateSQL(true);

}

service.checkinStorageLedger(child,uta,conn);

}

return vo;

}

}

package com.softbnd.jetblue.saas.scm.inventory.service.impl;

import java.SQL.Connection;

import java.util.ArrayList;

import java.util.List;

import com.softbnd.jetblue.base.dao.DAOFactory;

import com.softbnd.jetblue.base.dao.IDAO;

import com.softbnd.jetblue.base.dao.doman.FieldObject;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

第 9 章 网上商城后台之采购管理

·173·

import com.softbnd.jetblue.saas.base.service.BaseService;

import com.softbnd.jetblue.saas.scm.inventory.dao.impl.InventoryDAO;

public class InventoryledgerService extends BaseService {

public InventoryledgerService() {

super();

}

/** * 采购入库/销售退库/直接入库,修改库存台账对应货品库存

*

* @param vo

* @return

* @throws Exception

*/

public TableObject checkinStorageLedger(TableObject vo, UTransaction

uta,Connection selfconn) throws Exception {

TableObject storageledger = new TableObject();

storageledger.setTableName("storageledger");

storageledger.setPrimaryKey("storageledgerid");

storageledger.setPrimaryType(TableObject.LONG);

storageledger.setAutoGenerate(true);

List<FieldObject> fields = new ArrayList<FieldObject>();

FieldObject fo = new FieldObject();

String key = "goodsno";

String value = vo.getAttribute(key);

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

fo = new FieldObject();

key = "goodspec";

value = vo.getAttribute(key);

if(value!=null){

第四部分 网上商城完整实现

·174·

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

}

fo = new FieldObject();

key = "goodsbn";

value = vo.getAttribute(key);

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

fo = new FieldObject();

key = "warehouseno";

value = vo.getAttribute(key);

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

fo = new FieldObject();

key = "batchno";

value = vo.getAttribute(key);

if(value!=null){

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

}

fo = new FieldObject();

key = "issale";

value = vo.getAttribute(key);

if(value!=null){

fo.setKey(key);

第 9 章 网上商城后台之采购管理

·175·

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

}

fo = new FieldObject();

key = "salescale";

value = vo.getAttribute(key);

if(value!=null){

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.NUMBER);

fields.add(fo);

}

key = "initialquantity";

value = vo.getAttribute(key);

if(value!=null){

fo = new FieldObject();

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.NUMBER);

fields.add(fo);

}

if(vo.getAttribute("quantity")==null){

vo.setAttribute("quantity","0");

}

if(vo.getAttribute("money")==null){

vo.setAttribute("money","0");

}

fo = new FieldObject();

key = "tenantno";

value = vo.getAttribute(key);

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.STRING);

fields.add(fo);

第四部分 网上商城完整实现

·176·

storageledger.setFields(fields);

String SQL = InventoryDAO.getGWBSQL(storageledger);

IDAO dao = DAOFactory.getInstance();

TableObject oldstorageledger = dao.find(SQL, selfconn);

if (oldstorageledger == null) {

fo = new FieldObject();

key = "totalinquantity";

value = vo.getAttribute("quantity");

if(value.equals("0")){

value=vo.getAttribute("initialquantity");

}

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.NUMBER);

fields.add(fo);

fo = new FieldObject();

key = "realquantity";

value = vo.getAttribute("initialquantity");

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.NUMBER);

fields.add(fo);

fo = new FieldObject();

key = "currentquantity";

value = vo.getAttribute("initialquantity");

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.NUMBER);

fields.add(fo);

fo = new FieldObject();

key = "totalinmoney";

value = vo.getAttribute("money");

fo.setKey(key);

fo.setValue(value);

第 9 章 网上商城后台之采购管理

·177·

fo.setType(TableObject.NUMBER);

fields.add(fo);

fo = new FieldObject();

key = "totaloutquantity";

value = "0";

fo.setKey(key);

fo.setValue(value);

fo.setType(TableObject.NUMBER);

fields.add(fo);

storageledger.setFields(fields);

insert(storageledger, uta,selfconn);

} else {

storageledger.setAttribute("storageledgerid", oldstorageledger.

getAttribute("storageledgerid"));

storageledger.setPrimaryValue(oldstorageledger.getAttribute

("storageledgerid"));

String totalinquantity = oldstorageledger.getAttribute

("totalinquantity");

double quantity=Double.parseDouble(vo.getAttribute("quantity"));

if(vo.getAttribute("initialquantity")!=null&&!vo.getAttribute

("initialquantity").equals("")){

String initialquantity=vo.getAttribute("initialquantity");

String oldinitialquantity=oldstorageledger.getAttribute

("initialquantity");

quantity=quantity+Double.parseDouble(initialquantity)-

Double.parseDouble(oldinitialquantity);

}

if (totalinquantity == null) {

totalinquantity = String.valueOf(quantity);

} else {

totalinquantity = String.valueOf(Double.parseDouble

(totalinquantity)+quantity);

}

storageledger.setAttribute("totalinquantity", totalinquantity);

storageledger.getFieldObject("totalinquantity").setType

第四部分 网上商城完整实现

·178·

(TableObject.NUMBER);

String realquantity = oldstorageledger.getAttribute

("realquantity");

if (realquantity == null) {

realquantity = String.valueOf(quantity);

} else {

realquantity = String.valueOf(Double.parseDouble

(realquantity) + quantity);

}

storageledger.setAttribute("realquantity", realquantity);

storageledger.getFieldObject("realquantity").setType

(TableObject.NUMBER);

String currentquantity = oldstorageledger.getAttribute

("currentquantity");

if (currentquantity == null) {

currentquantity = String.valueOf(quantity);

} else {

currentquantity = String.valueOf(Double.parseDouble

(currentquantity) +quantity);

}

storageledger.setAttribute("currentquantity", currentquantity);

storageledger.getFieldObject("currentquantity").setType(TableObject.NUMBER);

String totalinmoney = oldstorageledger.getAttribute

("totalinmoney");

if (totalinmoney == null) {

totalinmoney = String.valueOf(Double.parseDouble(vo.

getAttribute("money")));

} else {

totalinmoney = String.valueOf(Double.parseDouble

(totalinmoney) + Double.parseDouble(vo.getAttribute("money")));

}

storageledger.setAttribute("totalinmoney", totalinmoney);

storageledger.getFieldObject("totalinmoney").setType

第 9 章 网上商城后台之采购管理

·179·

(TableObject.NUMBER);

// String[] compoundPrimaryKeys = { "goodsno", "warehouseno",

"batchno", "goodsbn", "goodspec" };

// storageledger.setCompoundPrimaryKeys(compoundPrimaryKeys);

update(storageledger, uta,selfconn);

}

return vo;

}

/** * 销售出库/采购退库/直接出库,修改库存

*

* @param vo

* @param

* @return

* @throws Exception

*/

public TableObject checkoutStorageLedger(TableObject vo, UTransaction

uta,Connection conn) throws Exception {

TableObject storageledger = new TableObject();

storageledger.setTableName("storageledger");

storageledger.setPrimaryKey("storageledgerid");

storageledger.setPrimaryType(TableObject.LONG);

storageledger.setAutoGenerate(true);

storageledger.setAttribute("tenantno", vo.getAttribute("tenantno"));

String SQL = InventoryDAO.getGWBSQL(vo);

IDAO dao = DAOFactory.getInstance();

TableObject oldstorageledger = dao.find(SQL, conn);

if (oldstorageledger != null) {

storageledger.setAttribute("storageledgerid", oldstorageledger.

getAttribute("storageledgerid"));

storageledger.setPrimaryValue(oldstorageledger.getAttribute

("storageledgerid"));

第四部分 网上商城完整实现

·180·

String totaloutquantity = oldstorageledger.getAttribute

("totaloutquantity");

if(totaloutquantity==null){

totaloutquantity="0";

}

totaloutquantity = String.valueOf(Double.parseDouble

(totaloutquantity)+ Double.parseDouble(vo.getAttribute("quantity")));

storageledger.setAttribute("totaloutquantity", totaloutquantity,

TableObject.NUMBER);

String realquantity=oldstorageledger.getAttribute("realquantity");

realquantity = String.valueOf(Double.parseDouble(realquantity) -

Double.parseDouble(vo.getAttribute("quantity")));

storageledger.setAttribute("realquantity", realquantity,

TableObject.NUMBER);

String currentquantity = oldstorageledger.getAttribute

("currentquantity");

currentquantity = String.valueOf(Double.parseDouble

(currentquantity) - Double.parseDouble(vo.getAttribute("quantity")));

storageledger.setAttribute("currentquantity", currentquantity,

TableObject.NUMBER);

String totaloutmoney = oldstorageledger.getAttribute

("totaloutmoney");

String money=vo.getAttribute("money");

if(money==null){

money="0";

}

if (totaloutmoney != null) {

totaloutmoney = String.valueOf(Double.parseDouble

(totaloutmoney) + Double.parseDouble(money));

}else{

totaloutmoney="0";

}

storageledger.setAttribute("totalinmoney", totaloutmoney,

TableObject.NUMBER);

第 9 章 网上商城后台之采购管理

·181·

// String[] compoundPrimaryKeys = { "goodsno", "warehouseno",

"batchno", "goodsbn", "goodspec" };

// storageledger.setCompoundPrimaryKeys(compoundPrimaryKeys);

update(storageledger, uta,conn);

}

return vo;

}

public TableObject allocationentryStorageLedger(TableObject vo,

UTransaction uta,Connection conn) throws Exception {

allocationentryCheckoutStorageLedger(vo, uta,conn);

allocationentryCheckinStorageLedger(vo, uta,conn);

return vo;

}

}

回填采购订单到货数量 UpdatePurchaseOrderArrivedQTYAction.java 代码如下。

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.purchase.service.impl.PurchaseService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/** 当采购入库的时候,修改采购订单,货品的已到货数量

*/

public class UpdatePurchaseOrderArrivedQTYAction extends DefaultAction

implements IAction{

public UpdatePurchaseOrderArrivedQTYAction(){

}

public TableObject execute(TableObject vo,UTransaction uta,Connection conn)

throws Exception {

PurchaseService service=new PurchaseService();

service.updatePurchaseOrderArrivedQTY(vo, uta,conn);

return vo;

第四部分 网上商城完整实现

·182·

}

}

public class PurchaseService extends BaseService{

/** * 当采购入库的时候,修改采购订单,货品的已到货数量

*/

public TableObject updatePurchaseOrderArrivedQTY(TableObject vo,

UTransaction uta,Connection conn) throws Exception {

IDAO dao = DAOFactory.getInstance();

for (TableObject child : vo.getChildren()) {

String SQL = PurchaseDAO.getPOLineItemSQL(child);

TableObject ret = dao.find(SQL, uta.getConnection());

if (ret != null) {

ret.setTableName("polineitem");

ret.setPrimaryKey("itemid");

ret.setPrimaryType(TableObject.LONG);

ret.setPrimaryValue(ret.getAttribute("itemid"));

String arrivedquantity = ret.getAttribute("arrivedquantity");

arrivedquantity = String.valueOf(Double.parseDouble

(arrivedquantity) + Double.parseDouble(child.getAttribute("quantity")));

ret.setAttribute("arrivedquantity", arrivedquantity);

ret.getFieldObject("arrivedquantity").setType(TableObject.

NUMBER);

dao.update(ret, uta);

}

}

return vo;

}

}

对于新增应付款这一步,有的客户可能是货到付款,不会形成应付款,有的是一周之

后、一个月之后甚至几个月之后才会跟客户结账,这样就会形成自己的应付款项。所以在

界面上加了一个选项,由客户决定这张采购订单是否会形成应付款。而在我们的代码里,

无须做这样的判断,只需要配置相应的 XML 文件即可。

第 9 章 网上商城后台之采购管理

·183·

<condiation key="reducingflag" type="String" value="1" op="==">

<component name="purchasepayment" step="3" description="新增应付款"

class="com.softbnd.jetblue.saas.scm.finance.action.PurchasePaymentAction"/>

</condiation>

我们用 reducingflag 字段做标志,如果 reducingflag 等于 1,即用户单击了界面的形成

应付款,代码 PurchasePaymentAction 才会执行,否则,就不执行。

源代码 PurchasePaymentAction.java 如下。

package com.softbnd.jetblue.saas.scm.finance.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.finance.service.impl.FinanceService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

public class PurchasePaymentAction extends DefaultAction implements IAction{

public PurchasePaymentAction(){

super();

}

public TableObject execute(TableObject vo, UTransaction uta,Connection conn)

throws Exception {

FinanceService service=new FinanceService();

service.purchasePayment(vo, uta,conn);

return vo;

}

}

public class FinanceService extends BaseService {

/** * 采购入库,新增应付款

* @param vo

* @return

* @throws Exception

*/

public TableObject purchasePayment(TableObject vo,UTransaction uta,

Connection conn ) throws Exception{

第四部分 网上商城完整实现

·184·

IDAO dao = DAOFactory.getInstance();

String SQL =FinanceDAO.getCustSuppSQL(vo);

TableObject ret = dao.find(SQL, conn);

if(ret!=null){

TableObject custsupp=new TableObject();

custsupp.setTableName("custsupp");

custsupp.setPrimaryKey("csno");

custsupp.setPrimaryValue(vo.getAttribute("custsuppno"));

custsupp.setPrimaryType(TableObject.STRING);

custsupp.setAttribute("tenantno", vo.getAttribute("tenantno"));

String totalmoney = vo.getAttribute("totalmoney");

String accountpaymoney=ret.getAttribute("accountpaymoney");

if(accountpaymoney==null){

accountpaymoney="0";

}

totalmoney = String.valueOf(Double.parseDouble(totalmoney) + Double.

parseDouble(accountpaymoney));

custsupp.setAttribute("accountpaymoney", totalmoney,TableObject. NUMBER);//应付款合计

dao.update(custsupp, uta);

}

return vo;

}

}

9.2.4 采购入库单查询的实现 对于采购入库单的查询,我们有 4 个参数:采购入库起止日期、采购入库单号以及供

应商名称,Flex 对应的代码如下。

<mx:HTTPService id="srv" url="querypurchaserk.do" method="POST"

resultFormat="e4x" result="resultHandler(event)" fault="faultHander(event)" >

<mx:request xmlns="">

<date1>{DateUtil.getStringDate(date1.selectedDate)}</date1>

<date2>{DateUtil.getStringDate(date2.selectedDate)}</date2>

第 9 章 网上商城后台之采购管理

·185·

<purchaserkno>{q_purchaserkno.text}</purchaserkno>

<custsuppname>{q_custsuppname.text}</custsuppname>

<currentpage>{currentpage.value}</currentpage>

<pagesize>19</pagesize>

</mx:request>

</mx:HTTPService>

我们在 XML 文件里的查询配置文件如下。

<url-mapping action="querypurchaserk.do" description="查询采购入库单"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select purchaserk.*,DATE_FORMAT(purchaserkdate,

'%Y-%m-%d') purchaserkdate2,custsupp.csname as custsuppname,warehousename from

purchaserk,custsupp,warehouse where purchaserk.custsuppno=custsupp.csno

and purchaserk.warehouseno=warehouse.warehouseno and purchaserk.tenantno in

('#tenantno#') and custsupp.tenantno in ('#tenantno#') and

warehouse.tenantno in

('#tenantno#','0000000000') order by purchaserkno ">

<subquery SQL="select

purchaserklineitem.*,goodsname,goodstype.gtname ,packageunit.puname

from purchaserklineitem,goods,packageunit,goodstype

where purchaserklineitem.goodsno=goods.goodsno and

goods.goodsunitno=packageunit.puno and goods.goodstypeno=goodstype.gtno

and purchaserklineitem.purchaserkno='?' and purchaserklineitem.

tenantno in

('#tenantno#') and goods.tenantno in ('#tenantno#') and

packageunit.tenantno in

('#tenantno#') and goodstype.tenantno in ('#tenantno#','0000000000') "/>

<parameter requestname="date1" name="purchaserkdate" type="Date"

op="&gt;=" or=" purchaserk.status='0' "/>

<parameter requestname="date2" name="purchaserkdate" type="Date"

op="&lt;=" or=" purchaserk.status='0' "/>

<parameter requestname="purchaserkno" name="purchaserkno"

type="String" />

<parameter requestname="custsuppname" name="custsupp.csname"

type="String" />

</query>

</url-mapping>

第四部分 网上商城完整实现

·186·

对于查询,我们依然只使用了默认的类 LogonQueryAction,无须额外地编写查询代码。

9.2.5 采购入库单的打印 采购入库单的打印,跟采购订单是一样的。打印效果如图 9-10 所示。

图 9-10 采购入库单

对应的打印 Flex 源代码如下。

<?xml version="1.0"?>

<mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" backgroundColor="#FFFFFF"

height="750" width="1000"

paddingTop="50" paddingLeft="50" paddingRight="50"> <mx:Label width="1000" text="采购入库单" fontSize="16" fontWeight="bold"

textAlign="center"/>

<mx:HBox>

<mx:Label id="purchaserkno" width="200"/>

<mx:Label id="purchaserkdate" width="200"/>

<mx:Label id="custsuppname" text="" width="400"/>

</mx:HBox>

<mx:HBox>

<mx:Label id="warehousename" width="200"/>

<mx:Label id="totalmoney" text="" width="200"/>

</mx:HBox>

<mx:PrintDataGrid id="myDataGrid" width="100%">

<mx:columns> <mx:DataGridColumn dataField="purchaseorderno" headerText="订单

编码" width="180"/>

<mx:DataGridColumn dataField="goodsno" headerText="货品编码"

第 9 章 网上商城后台之采购管理

·187·

width="180"/> <mx:DataGridColumn dataField="goodsname" headerText="货品名称"

width="200"/> <mx:DataGridColumn dataField="specvalues" headerText="规格"

width="150" /> <mx:DataGridColumn dataField="gtname" headerText="货品类别"

width="100"/> <mx:DataGridColumn dataField="puname" headerText="计量单位"

width="100"/> <mx:DataGridColumn dataField="quantity" headerText="入库数量"

width="100"/> <mx:DataGridColumn dataField="price" headerText="价格"width="100"/>

<mx:DataGridColumn dataField="money" headerText="金额" width="100"/>

</mx:columns>

</mx:PrintDataGrid>

<mx:HBox>

<mx:Label id="empno" width="200"/>

<mx:Label id="printdate" text="" width="200"/>

</mx:HBox>

</mx:VBox>

对应的 ActionScript代码如下。

private function print():void {

if(maindatagrid.selectedItem!=null){

purchaserk=dypurchaserk;

var printJob:FlexPrintJob = new FlexPrintJob();

if(printJob.start()) {

var formPrintView:PCheckInPrintView = new PCheckInPrintView();

addChild(formPrintView); formPrintView.purchaserkno.text ="入库单编码: " + purchaserk.

purchaserkno +" "; formPrintView.purchaserkdate.text ="入库日期: " + purchaserk.

v_date +" "; formPrintView.custsuppname.text ="供应商名称: " + purchaserk.

custsuppname +" "; formPrintView.warehousename.text ="库房: " + purchaserk.

warehousename;

第四部分 网上商城完整实现

·188·

formPrintView.totalmoney.text ="金额合计: " + purchaserk.

totalmoney +" 元";

formPrintView.empno.text ="经手人: " + purchaserk.empno +" ";

formPrintView.printdate.text ="打印日期: " +DateUtil.

getStringDate(new Date(new Date().getTime())) +" ";

formPrintView.myDataGrid.dataProvider=purchaserk.lineitems;

printJob.addObject(formPrintView);

printJob.send();

removeChild(formPrintView);

}

}else{ Alert.show("请选择一条记录再进行打印!");

}

}

9.3 采购付款的实现

如果我们在采购入库的时候选择“生成应付款”,则在采购付款的时候,再对此采购

入库单进行付款。一张采购付款单可对多张采购入库单进行付款,前提是必须是同一家供

货商。采购付款单的主界面如图 9-11 所示。

图 9-11 采购付款单主界面

新增和修改界面如图 9-12 所示。

第 9 章 网上商城后台之采购管理

·189·

图 9-12 采购入库单的新增和修改界面

9.3.1 采购付款单的数据库设计 采购付款单的主、子表设计如表 9-5、表 9-6 所示。

表 9-5 采购付款单主表(purchasepayentry)

字段名称 编码 字段类型 是否主键 是否允许 NULL

采购付款单编号 ppentryno varchar(20) Y(自增) N

付款日期 entrydate DATE N N

供应商编码 custsuppno varchar(22) N N

金额合计 totalmoney numeric(11,2) N N

业务员 empno varchar(10) N Y

备注 remark varchar(500) N Y

状态(0 新增 1 提交) status char(1) N N

租户编码 tenantno varchar(22) N N

第四部分 网上商城完整实现

·190·

表 9-6 采购付款单子表(purchasepayentrylineitem)

字段名称 编码 字段类型 是否主键 是否允许 NULL

采购付款单编码 ppentryno varchar(20) N N

子表 ID Itemid Integer Y(自增) N

采购入库单编码 Purchaserkno varchar(20) N N

入库日期 checkindate DATE N N

金额 totalmoney numeric(11,2) N N

已付金额 paidmoney numeric(11,2) N Y

已退金额 returnedmoney numeric(11,2) N Y

未付金额 debtmoney numeric(11,2) N Y

本次付款 Paymoney numeric(11,2) N N

租户编码 tenantno varchar(22) N N

9.3.2 采购付款单界面的 Flex 实现 对于采购付款单的界面实现,跟采购订单、采购入库没有太多的区别,主要的不同是,

当你选择一个供应商的时候,系统会把该供应商所对应的所有采购应付款列出来,用户选

择该付哪些入库单的账,可以一次付清。一张入库单可以分多次付款,一张付款单也可以

有多个采购入库单。

这里把选择供应商的时候,采用 Ajax 自动把对应的采购入库单带出来的代码列出来。

<mx:Label text="供应商(*)" width="10%"/>

<mx:PopUpButton id="custsuppno"

data="{purchasePay.custsuppno}" label="{purchasePay.custsuppname}"

openAlways="true" width="36%" textAlign="left">

<mx:popUp>

<mx:TitleWindow id="custsuppnoWin" title="请选择供应商" height="300" width="530" showCloseButton="true"

verticalScrollPolicy="off"

horizontalScrollPolicy="off"

borderAlpha="1" alpha="1" backgroundColor ="0xDCDCDC" close="custsuppno.

close();"> <mx:VBox label="查询"

第 9 章 网上商城后台之采购管理

·191·

height="100%">

<mx:HBox width="100%"

borderStyle="solid"> <mx:Label text="供应商名称" width="5%"/><mx:TextInput id="q_csname" 、

width="60%" textAlign="left" />

<mx:Button

id="querySuppButton" icon="@Embed('../../images/asterisk_orange.png')" label="查询"

labelPlacement="right"

color="#993300" click="querySupp()"/>

</mx:HBox>

<mx:DataGrid id="custsupp_

datagrid" width="100%" height="100%" headerStyleName="DataGridHeaderStyle"

dataProvider="{custsupps}"

click="selectsupp()">

<mx:columns>

<mx:DataGridColumn id=

"dgColFollowUp" width="45" textAlign="left" headerText="序号"

sortable="false" dataField="row"/>

<mx:DataGridColumn dataField="csno" headerText="供应商编码" sortable="true" width="140" textAlign=

"left" />

<mx:DataGridColumn dataField="csname" headerText="供应商名称" sortable="true" width="260"

textAlign="left" />

</mx:columns>

</mx:DataGrid>

</mx:VBox>

</mx:TitleWindow>

</mx:popUp>

</mx:PopUpButton>

与采购入库单一样,这里我们依然组合了 Flex 的 PopUpButton 、PopUp、TitleWindow

与 DataGrid 组件,这 4 个组件合在一起,形成了一个新的组件,UI 界面类似于 C/S 架构下

开发利器 PowerBuilder 的 DataWindow 窗口。效果如图 9-13 所示。

第四部分 网上商城完整实现

·192·

图 9-13 采购入库单界面

当 我 们 选 择 一 个 供 应 商 的 时 候 , 会 触 发 selectsupp() 事 件 , selectsupp() 对 应 的

ActionScript 代码如下。

private function selectsupp():void{

if(custsupp_datagrid.selectedItem!=null){

var item:Object = custsupp_datagrid.selectedItem;

custsuppno.label=item.csname;

custsuppno.data=item.csno;

purchasePay.lineitems=new ArrayCollection();

querypurchasecheckin.send();

custsuppno.close();

}

}

当我们选择一个供应商后,利用 Ajax,会调用 querypurchasecheckin.send()查找所有对

应的采购入库单。querypurchasecheckin 也是一个 HttpService 对象,其定义如下。

<mx:HTTPService id="querypurchasecheckin" url="querypurchasecheckin.

do" method="POST" resultFormat="e4x" result=

"queryCheckinEntryResultHandler(event)">

<mx:request xmlns="">

第 9 章 网上商城后台之采购管理

·193·

<custsuppno>{custsuppno.data}</custsuppno>

</mx:request>

</mx:HTTPService>

这里我们还是利用通用的 LogonQueryAction 来查询,参数是供应商编码。

<url-mapping action="querypurchasecheckin.do" description="查询采购入库单,

为采购付款服务" class="com.softbnd.jetblue.saas.webframework.action.

LogonQueryAction">

<query SQL="select DATE_FORMAT(p.purchaserkdate,'%Y-%m-%d')

checkindate,purchaserkno,

purchaseorderno,purchaserkdate,custsuppno,p.warehouseno,totalmoney,paiedmone

y, returnedmoney,p.empno,p.remark,p.status,(totalmoney -paiedmoney-

returnedmoney) as paymoney,(totalmoney -paiedmoney-returnedmoney) as debtmoney

from purchaserk as p,custsupp where p.custsuppno=custsupp.csno and (totalmoney

-paiedmoney-returnedmoney) >0 and p.tenantno in ('#tenantno#') and custsupp.

tenantno in ('#tenantno#')">

<parameter requestname="custsuppno" name="custsuppno" type=

"String" op="="/>

</query>

</url-mapping>

查找到符合条件的所有采购入库单后,就可以直接把他们填充到采购入库单的

DataGrid 里面去供我们选择。我们可以删除目前不需要付款的采购入库单。

querypurchasecheckin.do 对应的 ActionScript 代码如下。

private function queryCheckinEntryResultHandler(event: ResultEvent):void{

var totmoney:Number=0;

for each (var item: XML in event.result.vo) {

totmoney=totmoney+Number(item.paymoney);

purchasePay.lineitems.addItem(item);

}

totalmoney.text=String(totmoney);

}

采购入库单的 DataGrid 的 Flex 源代码如下。

<IJetBlueUI:DataGridEx id="lineitemdatagrid2" width="100%"

第四部分 网上商城完整实现

·194·

headerStyleName="DataGridHeaderStyle"

resizableColumns="true" dataProvider="{purchasePay.lineitems}"

horizontalScrollPolicy="on" verticalScrollPolicy="on" rowCount="9"

editable="true"

itemEditEnd="itemEditEndHandler(event)">

<IJetBlueUI:columns>

<mx:DataGridColumn dataField="purchaserkno" headerText="入库单号" sortable="true" width="150" editable="false" />

<mx:DataGridColumn dataField="checkindate" headerText="入库日期" sortable="true" width="200" editable="false" />

<mx:DataGridColumn dataField="totalmoney" headerText="总金额" width="150" sortable="false" editable="false" />

<mx:DataGridColumn dataField="debtmoney" headerText="未付金额" sortable="false" editable="false" />

<mx:DataGridColumn dataField="paymoney" headerText="本次付款" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text=

"{data.paymoney}" restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="paiedmoney" headerText="已付金额" sortable="false" width="100" editable="false" />

<mx:DataGridColumn dataField="returnedmoney " headerText="已退金额" sortable="false" editable="false" />

<mx:DataGridColumn dataField="oldpaymoney" headerText="本次付款" width="0" visible="false" />

</IJetBlueUI:columns>

</IJetBlueUI:DataGridEx>

DataGrid 的 dataProvider 就是 purchasePay.lineitems。

9.3.3 采购付款单序列化数据到数据库 新增采购付款单后,我们要回填采购入库单的已付金额,还要减少应付账款,对应的

第 9 章 网上商城后台之采购管理

·195·

XML 代码如下。

<url-mapping action="createpurchasepay.do" description="新增采购付款单 "

class="com.softbnd.jetblue.saas.scm.purchase.action.PurchasePayAction"> <businessprocess description="新增采购付款单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改采购入库

单付款金额" class="com.softbnd.jetblue.saas.scm.purchase.action.

UpdatePurchasePayAction"/>

<component name="updateSupppurchasepaymoney" step="2" description="采购付款,减少对该供应商的应付金额"

class="com.softbnd.jetblue.saas.scm.finance.action.UpdateSuppPurchasepayMone

yAction"/>

</condiation>

</businessprocess>

</url-mapping>

相应的 Java 代码如下。

第一步:采购付款单存盘,对应的类是 PurchasePayAction.java。

public class PurchasePayAction extends CreateAction{

public PurchasePayAction(){

super();

}

@Override

public String getprefixword() {

return "PP";

}

}

这里,我们依然是简单的继承一下 CreateAction 就可以了,采购付款单就可以自动存

盘了。

第二步:修改采购入库单付款金额,对应的类是 UpdatePurchasePayAction.java。

public class UpdatePurchasePayAction extends DefaultAction implements IAction{

/**

第四部分 网上商城完整实现

·196·

* 新增付款单后,修改采购入库单付款金额

*/

public UpdatePurchasePayAction(){

}

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

PurchaseService service=new PurchaseService();

service.updatePurchasePayMoney(vo, uta,conn);

return vo;

}

}

public class PurchaseService extends BaseService{

/**

* 当采购入库的时候,修改采购订单、货品的已到货数量

*/

public TableObject updatePurchaseOrderArrivedQTY(TableObject vo,

UTransaction uta,Connection conn) throws Exception {

IDAO dao = DAOFactory.getInstance();

for (TableObject child : vo.getChildren()) {

String SQL = PurchaseDAO.getPOLineItemSQL(child);

TableObject ret = dao.find(SQL, uta.getConnection());

if (ret != null) {

ret.setTableName("polineitem");

ret.setPrimaryKey("itemid");

ret.setPrimaryType(TableObject.LONG);

ret.setPrimaryValue(ret.getAttribute("itemid"));

String arrivedquantity = ret.getAttribute("arrivedquantity");

第 9 章 网上商城后台之采购管理

·197·

arrivedquantity = String.valueOf(Double.parseDouble

(arrivedquantity) + Double.parseDouble(child.getAttribute("quantity")));

ret.setAttribute("arrivedquantity", arrivedquantity);

ret.getFieldObject("arrivedquantity").setType(TableObject.

NUMBER);

dao.update(ret, uta);

}

}

return vo;

}

}

第三步:减少对该供应商的应付金额,对应的类是 UpdateSuppPurchasepayMoneyAction.

java。

public class UpdateSuppPurchasepayMoneyAction extends DefaultAction

implements IAction{

public UpdateSuppPurchasepayMoneyAction(){

}

/** * 采购付款,减少对该供应商的应付金额

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection

conn ) throws Exception {

FinanceService service=new FinanceService();

service.reducePurchasePayment(vo, uta,conn);

return vo;

}

}

至此,我们完成了采购付款的存盘操作。如果有的租户对应的业务逻辑不一样,我们

只需要修改那个保存业务逻辑的 XML 文件即可,然后再加上自己的实现。

第四部分 网上商城完整实现

·198·

9.3.4 采购付款单查询的实现 采购付款单的查询是比较简单的,包括付款起止日期、付款单编号和供应商名称。Flex

代码定义如下。

首先是 HttpService 的定义。

<mx:HTTPService id="srv" url="querypurchasepay.do" method="POST"

resultFormat="e4x" result="resultHandler(event)" fault="faultHander(event)" >

<mx:request xmlns="">

<date1>{DateUtil.getStringDate(date1.selectedDate)}</date1>

<date2>{DateUtil.getStringDate(date2.selectedDate)}</date2>

<ppentryno>{qppentryno.text}</ppentryno>

<custsuppname>{qcustsuppname.text}</custsuppname>

<currentpage>{currentpage.value}</currentpage>

<pagesize>19</pagesize>

</mx:request>

</mx:HTTPService>

接下来是 Flex 界面代码。

<mx:VBox label="查询">

<mx:HBox width="100%"> <mx:Label text="单据日期(起)" width="80"/><mx:DateField id=

"date1" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="单据日期(止)" width="80"/><mx:DateField id=

"date2" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="单据编号" width="80"/><mx:TextInput id=

"qppentryno" width="130"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="供应商名称" width="80"/><mx:TextInput id=

"qcustsuppname" width="130"/>

第 9 章 网上商城后台之采购管理

·199·

</mx:HBox>

<mx:HBox width="100%">

</mx:HBox>

<mx:HBox horizontalAlign ="left" width="100%">

<mx:Button id="queryButton" icon="@Embed('../../images/ asterisk_orange.png')" label="查询"

labelPlacement="right" color="#993300" click="query();"/>

</mx:HBox>

</mx:VBox>

后是 querypurchasepay.do 查询对应的 XML 文件,代码如下所示。

<url-mapping action="querypurchasepay.do" description="查询采购付款单"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select purchasepayentry.*,DATE_FORMAT(entrydate,'%Y-%m-%d')

entrydate2,custsupp.csname as custsuppname from purchasepayentry,custsupp where

purchasepayentry.custsuppno=custsupp.csno and purchasepayentry.tenantno in

('#tenantno#') and (custsupp.tenantno in ('#tenantno#') ) ">

<subquery SQL="select ppentryno,lineitemid,purchaserkno,DATE_

FORMAT(checkindate,'%Y-%m-%d') checkindate ,totalmoney,paiedmoney,

returnedmoney,debtmoney ,paymoney, tenantno from purchasepayentrylineitem where

purchasepayentrylineitem.ppentryno='?' and purchasepayentrylineitem.tenantno

in ('#tenantno#')"/>

<parameter requestname="date1" name="entrydate" type="Date" op=

"&gt;="/>

<parameter requestname="date2" name="entrydate" type="Date" op=

"&lt;="/>

<parameter requestname="ppentryno" name="ppentryno" type="String" />

<parameter requestname="custsuppname" name="custsupp.csname" type=

"String" />

</query>

</url-mapping>

我们还是用默认的 LogonQueryAction 来完成我们的查询。

第四部分 网上商城完整实现

·200·

9.3.5 采购付款单的打印 采购付款单的打印,源代码跟以前一样,只是填充数据的不同,这里就不多说了。打

印效果如图 9-14 所示。

图 9-14 采购付款单界面

9.4 采购退货单的实现

采购退货,就是采购来的货品,由于某种原因,退回给原供应商的业务过程。它是采

购入库的逆过程。采购退货的业务过程实际是比较复杂的,我们这里就不多讨论了,我们

只关注采购退货单与采购入库单一对一的情形。即一张采购退货单,只对应一张采购入库

单,但是一张采购入库单,可分多次退货。采购退货单的主界面如图 9-15 所示。

图 9-15 采购退货单主界面

第 9 章 网上商城后台之采购管理

·201·

新增和修改界面,如图 9-16 所示。

图 9-16 采购退货单的新增和修改界面

9.4.1 采购退货单的数据库设计 采购退货单的主子表设计如表 9-7、表 9-8 所示。

表 9-7 采购退货单主表(purchasereturnentry)

字段名称 编码 字段类型 是否主键 是否允许 NULL

采购退货单编码 prentryno Varchar(20) Y(自增) N

退货日期 prentrydate DATE N N

采购入库单编码 purchasecheckinno Varchar(20) N N

供应商编码 custsuppno Varchar(22) N N

仓库编码 warehouseno Varchar(20) N N

金额合计 totalmoney Numeric(11,2) N N

业务员 empno Varchar(10) N Y

备注 remark Varchar(500) N Y

状态(0 新增 1 提交) status char(1) N N

租户编码 tenantno varchar(22) N N

第四部分 网上商城完整实现

·202·

表 9-8 采购退货单子表(purchasereturnlineitem)

字段名称 编码 字段类型 是否主键 是否允许 NULL

采购退货单主表编码 prentryno varchar(20) N N

子表 ID Itemid integer Y(自增) N

采购入库单编码 Purchaserkno varchar(20) N N

货品编码 Goodsno varchar(20) N N

货号 goodsbn varchar(30) N N

规格 specvalues varchar(500) N Y

批次 batchno varchar(20) N Y

退货数量 Quantity numeric(11,2) N N

退货价格 Price numeric(11,2) N N

金额 Quantity numeric(11,2) N N

租户编码 tenantno varchar(22) N N

9.4.2 采购退货单界面的 Flex 实现 采购退货单的 UI 跟采购付款单是差不多的,根据某个供应商,查询出此供应商所有

的采购入库单,不同的是业务逻辑完全不同。

9.4.3 采购退货单序列化数据到数据库 新增采购退货单后,我们要回填采购入库单的退货数量和退货金额,还要减少应付账

款,对应的 XML 代码如下。

<url-mapping action="createpurchaseReturnentry.do" description="新增采购退

货单"class="com.softbnd.jetblue.saas.scm.purchase.action.

PurchaseReturnentryAction"> <businessprocess description="新增采购退货单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.PurchaseReturnAction"/> <component name="updatePurchaseOrder" step="2" description="修改

采购入库单子表退货数量"

class="com.softbnd.jetblue.saas.scm.purchase.action.UpdatePurchasereturnQTYA

第 9 章 网上商城后台之采购管理

·203·

ction"/>

<condiation key="reducingflag" type="String" value="1" op="=="> <component name="purchasepayment" step="2" description="修改

采购入库单主表退货金额"

class="com.softbnd.jetblue.saas.scm.purchase.action.UpdatePurchaseReturnPayA

ction"/> <component name="updatePurchaseOrder" step="3" description="修改

对应该供应商的应付账款"

class="com.softbnd.jetblue.saas.scm.finance.action.UpdateSuppPurchasereturnM

oneyAction"/>

</condiation>

</condiation>

</businessprocess>

</url-mapping>

相应的 Java 代码如下。

第一步:采购退货单存盘,对应的类是 PurchaseReturnentryAction.java。

public class PurchaseReturnentryAction extends CreateAction{

public PurchaseReturnentryAction(){

super();

}

@Override

public String getprefixword() {

return "PR";

}

}

这里,我们依然简单地继承 CreateAction,采购退货单就可以自动存盘了。

第二步:修改库存台账,对应的类是 PurchaseReturnAction.java。

public class PurchaseReturnAction extends DefaultAction implements IAction{

/** * 采购退货,修改库存

* @throws Exception

*/

第四部分 网上商城完整实现

·204·

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

InventoryledgerService service=new InventoryledgerService();

for (TableObject child : vo.getChildren()) {

child.setAttribute("warehouseno", vo.getAttribute("warehouseno"));

service.checkoutStorageLedger(child,uta,conn);

}

return vo;

}

}

第三步:修改采购入库单子表退货数量,对应的类是 UpdatePurchasereturnQTYAction。

/** 当采购退货的时候,修改采购入库单,货品的已退货数量

*/

public class UpdatePurchasereturnQTYAction extends DefaultAction implements

IAction{

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

PurchaseService service=new PurchaseService();

service.updatePurchaseReturnQTY(vo, uta,conn);

return vo;

}

}

第四步:修改采购入库单主表退货金额,对应的类是 UpdatePurchaseReturnPayAction。

public class UpdatePurchaseReturnPayAction extends DefaultAction implements

IAction{

/** * 采购退货的时候,修改采购入库单主表退货金额

*/

public UpdatePurchaseReturnPayAction(){

}

第 9 章 网上商城后台之采购管理

·205·

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

PurchaseService service=new PurchaseService();

service.updatePurchaseReturnMoney(vo, uta,conn);

return vo;

}

}

注意这一步采用了判断条件,代码如下。

<condiation key="reducingflag" type="String" value="1" op="=="> <component name="purchasepayment" step="2" description="修改

采购入库单主表退货金额"

class="com.softbnd.jetblue.saas.scm.purchase.action.UpdatePurchaseReturnPayA

ction"/>

只有当用户在界面上选择了 “退货冲减应付款”这一步才会执行。第三步修改采购

入库单子表,第四步修改采购入库单主表,主要是退货的时候,退货金额不一定就等于入

库单价×退货数量,这里退货价格允许用户自己录入,默认等于采购价格。

第五步:修改对应该供应商的应付账款,对应的类是

UpdatePurchaseReturnPayAction.java。

public class UpdateSuppPurchasereturnMoneyAction extends DefaultAction

implements IAction{

public UpdateSuppPurchasereturnMoneyAction(){

}

/** * 采购退货,修改对应该供应商的应付账款

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

FinanceService service=new FinanceService();

service.reducePurchasePayment(vo, uta,conn);

return vo;

}

}

第四部分 网上商城完整实现

·206·

9.4.4 采购退货单查询的实现 采购退货的查询,我们依然用 LogonQueryAction 类来实现,这里就不多说了。参考以

前的代码来实现。

9.4.5 采购退货单的打印 采购退货的打印,也跟采购入库、付款的打印代码是类似的,这里就不多举例了。打

印效果如图 9-17 所示。

图 9-17 采购退货单

9.5 本章总结

本章是网上商城后台进销存系统之采购管理模块,包括采购订单、采购入库、采购付

款和采购退货等功能模块。从功能上,它是进销存模块的开端,架构上,它是构建自己 SSH

架构的理论实现。采购入库的入口,可以由采购订单转化而来,也可以独立生成采购入库

单。而采购付款和采购退货,我们要求必须跟采购入库单对应上。而有的系统,采购退货

并不与采购订单对应上,各自独立运作,这么做当然也是有好处的,业务非常清晰,适合

门店连锁超市模式的 B2B2C 商超平台。

第 10 章 网上商城后台之销售管理

·207·

第 10 章 网上商城后台之销售管理

10.1 销售订单的实现

销售订单,可以理解为销售合同,一个企业与另外一个企业签订的销售意向。一张销

售订单,可分多次实现,即可分多次销售出库,对应多个销售出库单,销售出库单的主界

面如图 10-1 所示。

图 10-1 销售出库单的主界面

新增和修改界面,如图 10-2 所示。

第四部分 网上商城完整实现

·208·

图 10-2 新增和修改界面

10.1.1 销售订单的数据库设计 销售订单的数据库设计包括两大部分:销售订单主表和销售订单子表。

(1)销售订单主表:salesorder

销售订单主表 salesorder 如表 10-1 所示。

表 10-1 销售订单主表 salesorder

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售订单编码 saleorderno varchar(20) Y(自增) N

单据日期 saleorderdate Date N N

客户编码 custsuppno varchar(20) N N

交货日期 deliverydate Date N N

交货地点 deliveryadd varchar(100) N N

金额合计 totalmoney Numeric(11,2) N N

业务员 Empno Varchar(10) N Y

备注 Remark Varchar(500) N Y

状态(0 新增 1 提交) Status char(1) N N

租户编码 Tenantno varchar(22) N N

第 10 章 网上商城后台之销售管理

·209·

(2)销售订单子表 salesorderlineitem

销售订单子表 salesorderlineitem 如表 10-2 所示。

表 10-2 销售订单子表 salesorderlineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售订单主表 saleorderno varchar(20) N N

子表 ID saleorderlid integer Y(自增) N

货品编码 goodsno varchar(20) N N

货号 goodsbn varchar(20) N N

规格 specvalues varchar(500) N N

批次 batchno varchar(500) N Y

订单数量 quantity numeric(11,2) N Y

已提数量 checkoutquantity numeric(11,2) N N

销售价格 Price numeric(11,2) N N

金额 Money numeric(11,2) N N

租户编码 tenantno varchar(22) N N

10.1.2 销售订单界面的 Flex 实现 销售订单的 UI,跟第 9 章的采购模块差不多,不同的是业务逻辑完全不同,Flex 实现

的完整源代码如下。

1.UI 主界面相关的 SaleOrderlist.mxml 文件。

(1)引入公共的 ActionScript。

<?xml version="1.0" encoding="UTF-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"

horizontalAlign="center" width="100%" height="100%"

creationComplete="onCreationComplete();" xmlns:IBusinessUI="org.

bizsolution.jetblue.saas.ui.*"

xmlns:IJetBlueUI="org.bizsolution.jetblue.ui.*" xmlns:b2bcomp="b2b.*"

xmlns:common="common.*" fontSize="12">

<mx:Style source="../../style/main.css"/>

<mx:Script source="../../js/utils.as"/>

第四部分 网上商城完整实现

·210·

<mx:Script source="SaleOrderlist.as" />

(2)设置查询条件。

<mx:HTTPService id="srv"url="querysaleorder.do" method="POST"resultFormat=

"e4x"result="resultHandler(event)" fault="faultHander(event)" >

<mx:request xmlns="">

<date1>{DateUtil.getStringDate(date1.selectedDate)}</date1>

<date2>{DateUtil.getStringDate(date2.selectedDate)}</date2>

<saleorderno>{qsaleorderno.text}</saleorderno>

<custsuppname>{qcustsuppname.text}</custsuppname>

</mx:request>

</mx:HTTPService>

<mx:HTTPService id="createaction" url="createsaleorder.do" contentType=

"application/xml" result="refresh(event)"/>

<mx:HTTPService id="removeaction" url="remove.do" contentType="application

/xml" result="refresh(event)"/>

<mx:HBox width="100%" height="100%">

<mx:VBox width="23%" height="100%">

<mx:TabNavigator id="tn" width="100%" height="100%"horizontalGap="1"> <mx:VBox label="查询">

<mx:HBox width="100%"> <mx:Label text="订单日期(起)" width="80"/><mx:DateField

id="date1" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="订单日期(止)" width="80"/><mx:DateField id=

"date2" editable="true" width="130" formatString="YYYY-MM-DD"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="单据编号" width="80"/><mx:TextInput id=

"qsaleorderno" width="130"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="客户名称" width="80"/><mx:TextInput id=

"qcustsuppname" width="130"/>

</mx:HBox>

第 10 章 网上商城后台之销售管理

·211·

<mx:HBox width="100%">

</mx:HBox>

<mx:HBox horizontalAlign ="left" width="100%">

<mx:Button id="queryButton" icon="@Embed('../../images/ asterisk_orange.png')" label="查询"

labelPlacement="right" color="#993300" click="query

(event);"/>

</mx:HBox>

</mx:VBox>

<b2bcomp:Pifa/>

<b2bcomp:MySocial/>

</mx:TabNavigator>

</mx:VBox>

(3)设置界面上所有的功能按钮,显示的 UI 就是主界面上方的一排按钮。

<mx:VBox width="77%" height="100%">

<mx:VBox width="100%" height="100%" id="mainform">

<mx:HBox horizontalAlign ="right" borderStyle="solid" height="30"

width="100%">

<mx:Button id="addButton" icon="@Embed('../../images/ asterisk_orange.png')" label="新增"

labelPlacement="right" color="#993300" click="newForm();"/>

<mx:Button id="updButton" icon="@Embed('../../images/ asterisk_orange.png')" label="修改"

labelPlacement="right"color="#993300"click="showForm();"/>

<mx:Button id="removeButton" icon="@Embed('../../images/ asterisk_orange.png')" label="删除"

labelPlacement="right" color="#993300" click=

"removeSaleOrder()"/>

<mx:Button id="printButton" icon="@Embed('../../images/ asterisk_orange.png')" label="打印"

labelPlacement="right" color="#993300" click="print()"/>

<mx:Button id="printallButton" icon="@Embed('../../ images/asterisk_orange.png')" label="全部打印"

labelPlacement="right"color="#993300" click="printall()"/>

</mx:HBox>

第四部分 网上商城完整实现

·212·

(4)设置主表的 DataGrid,显示的 UI 就是主界面上“所有销售订单”主表信息。

<mx:Label width="100%" text=" 所 有 销 售 订 单 " fontSize="16"

fontWeight="bold" color="green" textAlign="center"/>

<mx:DataGrid id="maindatagrid" width="120%" height="240"

headerStyleName="DataGridHeaderStyle" resizableColumns="true"

dataProvider="{saleOrders}" horizontalScrollPolicy="on"

rowCount="10" doubleClickEnabled="true"

itemClick ="maindatagridClickEvent(event);" itemDoubleClick=

"showDetail(event);" keyDown="maindatagridKeyDownEvent(event);">

<mx:columns>

<mx:DataGridColumn dataField="saleorderno" headerText="单据编号" sortable="true" width="150"/>

<mx:DataGridColumn dataField="custsuppname" headerText=" 客户名称" sortable="true" width="240"/>

<mx:DataGridColumn dataField="v_saleorderdate" headerText ="订单日期" />

<mx:DataGridColumn dataField="v_deliverydate" headerText= "交货日期"/>

<mx:DataGridColumn dataField="totalmoney" headerText="应

收金额(元)" sortable="false" width="120"/>

<mx:DataGridColumn dataField="empno" headerText="经手人"

sortable="false"/>

<mx:DataGridColumn dataField="deliveryadd" headerText=" 交货地点" sortable="false" width="120"/>

</mx:columns>

</mx:DataGrid>

(5)设置子表的 DataGrid,显示的 UI 就是主界面上“销售订单明细列表”子表信息

数据,当我们单击主表上每行数据的时候,子表数据会随着主表数据变化而自动变化。

<mx:Label width="100%" text="销售订单明细列表" fontSize="16" fontWeight="bold"

color="green" textAlign="center"/>

<mx:DataGrid id="lineitemdatagrid" width="140%" height="100%"

headerStyleName="DataGridHeaderStyle" resizableColumns="true"

dataProvider="{dynaSaleOrder.lineitems}"horizontalScrollPolicy=

"on"verticalScrollPolicy="on" rowCount="5">

第 10 章 网上商城后台之销售管理

·213·

<mx:columns> <mx:DataGridColumn dataField="gtname" headerText="货品类别

"sortable="false" width="100"/> <mx:DataGridColumn dataField="goodsno" headerText="货品编

码" sortable="true" width="150"/>

<mx:DataGridColumn dataField="goodsbn" headerText="货号"

sortable="true" width="150"/> <mx:DataGridColumn dataField="goodsname" headerText="货品

名称" sortable="true" width="200"/>

<mx:DataGridColumn dataField="specvalues" headerText="规

格" width="150" sortable="false"/>

<mx:DataGridColumn dataField="quantity" headerText="订单

数量" sortable="false" />

<mx:DataGridColumn dataField="puname" headerText="计量单位

" sortable="false" /> <mx:DataGridColumn dataField="price" headerText="价格"

sortable="false" /> <mx:DataGridColumn dataField="taxrate" headerText="税率"

sortable="false"/> <mx:DataGridColumn dataField="money" headerText="金额"

sortable="false"/>

<mx:DataGridColumn dataField="checkoutquantity" headerText="已提数量" sortable="false"/>

<mx:DataGridColumn dataField="batchno" headerText="批次"

sortable="false" />

</mx:columns>

</mx:DataGrid>

</mx:VBox>

(6)设置销售订单的维护界面,开始是隐藏的,当我们单击<新增>按钮的时候,则会

弹出此界面。

<mx:Canvas borderStyle="solid" width="100%" id="lineitemform"

visible="false" height="0%">

<mx:Canvas borderStyle="solid" height="100%" width="100%" id=

第四部分 网上商城完整实现

·214·

"saleorderform" >

<mx:VBox width="100%" height="100%">

<mx:VBox width="100%" height="100%" id="veditdetail">

<mx:HBox width="100%"> <mx:Label text="订单编号" width="11%"/><mx:

TextInput id="saleorderno" text="{dataObject.saleorderno}" width="23%"

enabled="false" editable="false"

backgroundDisabledColor="0xffffff"/>

<mx:Label text="" width="8%"/> <mx:Label text="订单日期" width="10%"/><mx:

DateField id="saleorderdate" editable="true" selectedDate="{dataObject.

saleorderdate}"formatString="YYYY-MM-DD" width="20%" />

<mx:Label text="" width="8%"/> <mx:Label text="交货日期" width="10%"/><mx:

DateField id="deliverydate" editable="true" selectedDate="{dataObject.

deliverydate}" formatString="YYYY-MM-DD" width="20%" />

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="客户(*)" width="10%"/>

<mx:PopUpButton id="custsuppno" data="{dataObject.

custsuppno}" label="{dataObject.custsuppname}" openAlways="true" width="35%"

textAlign="left">

<mx:popUp>

<common:Customer/>

</mx:popUp>

</mx:PopUpButton>

<mx:Label text="" width="3%"/> <mx:Label text="交货地点" width="5%"/><mx:TextInput

id="deliveryadd" text="{dataObject.deliveryadd}" width="47%" maxChars="100"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="金额合计" width="10%"/><mx:

TextInput id="totalmoney" text="{dataObject.totalmoney}" width="20%"

enabled="false" editable="false"

backgroundDisabledColor="0xffffff"/> <mx:Label text="元" textAlign="left" width="7%"/>

<mx:Label text="经手人" width="7%"/>

第 10 章 网上商城后台之销售管理

·215·

<mx:TextInput id="empno" width="20%" text="

{dataObject.empno}" maxChars="10"/>

<mx:Label text="" textAlign="left" visible=

"false"/>

</mx:HBox>

<mx:HBox width="100%"> <mx:Label text="备注" width="9%"/><mx:TextArea id=

"remark" text="{dataObject.remark}" width="91%" height="100"

verticalScrollPolicy="on" maxChars="500"/>

</mx:HBox>

<mx:HBox id="lineitemdtlbtngroup" horizontalAlign

="left" borderStyle="solid" height="30" width="100%">

(7)销售订单维护的选择商品界面,当我们单击<新增明细>按钮的时候,则会弹出此

界面。

<mx:Button id="addlineitemButton" icon="@Embed ('../../images/asterisk_orange.png')" label="新增明细"

labelPlacement="right" color="#993300" click=

"addlineitem()"/>

<mx:Button id="dellineitemButton" click=

"removelineitem()"

icon="@Embed('../../images/asterisk_orange. png')" label="删除明细"

labelPlacement="right" color="#993300"/>

</mx:HBox>

<IJetBlueUI:DataGridEx id="lineitemdatagrid2"

width="100%" headerStyleName="DataGridHeaderStyle"

resizableColumns="true" dataProvider="{dataObject.

lineitems}" horizontalScrollPolicy="on"

verticalScrollPolicy="on" rowCount="8" editable=

"true" itemEditEnd="itemEditEndHandler(event)">

<IJetBlueUI:columns>

<mx:DataGridColumn dataField="goodsno" headerText="货品编码" sortable="true" width="120" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="goodsbn"

第四部分 网上商城完整实现

·216·

headerText="货号"sortable="true" width="120" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="goodsname" headerText="货品名称"sortable="true" width="160" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="specvalues" headerText="规格" width="160" sortable="false" editable="false" textAlign=

"left"/>

<mx:DataGridColumn dataField="quantity" headerText="订单数量" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text="{data.quantity}"

restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="price" headerText="价格" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text="{data.price}"

restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="taxrate" headerText="税率%" sortable="false" editable="true" fontSize="11">

<mx:itemEditor>

<mx:Component>

<mx:TextInput text="{data.taxrate}"

restrict="0-9\."/>

</mx:Component>

</mx:itemEditor>

</mx:DataGridColumn>

<mx:DataGridColumn dataField="money" headerText= "金额" sortable="false" editable="false"/>

第 10 章 网上商城后台之销售管理

·217·

<mx:DataGridColumn dataField="puname" headerText="计量单位" sortable="false" editable="false"/>

<mx:DataGridColumn dataField="batchno" headerText="批次" sortable="false" editable="false" />

</IJetBlueUI:columns>

</IJetBlueUI:DataGridEx>

</mx:VBox>

<mx:HBox id="lineitembtngroup">

<mx:Button id="createSubmitBtn" click= "createSubmit();" label="提交" icon="@Embed('../../images/asterisk_orange.png')"/>

<mx:Button id="cancelBtn" label="取消" click=

"cancelLineItem();" icon="@Embed('../../images/asterisk_orange.png')"/>

</mx:HBox>

</mx:VBox>

</mx:Canvas>

<mx:Panel height="100%" width="100%" id="cgdddetail" visible= "false" title="查询条件">

<common:SelectGoodsforSale/>

</mx:Panel>

</mx:Canvas>

</mx:VBox>

</mx:HBox>

</mx:Application>

2.销售订单相关的 ActionScript 代码 SaleOrderlist.as

[Bindable] public var dataObject : SaleOrder = new SaleOrder();

[Bindable] private var dynaSaleOrder : SaleOrder = new SaleOrder();

[Bindable] private var saleOrders :ArrayCollection = new ArrayCollection();

[Bindable] private var goods:ArrayCollection = new ArrayCollection();

[Bindable] private var custsupps:ArrayCollection = new ArrayCollection();

[Bindable] private var exportExcelCollection : ArrayCollection = new

ArrayCollection();

(1)<新增>按钮对应的 ActionScript 代码。

private function newForm():void {

第四部分 网上商城完整实现

·218·

veditdetail.enabled=true;

createSubmitBtn.visible=true;

createSubmitBtn.enabled=true;

mainform.visible=false;

lineitemform.height=mainform.height;

mainform.height=0;

lineitemform.visible=true;

dataObject=new SaleOrder();

saleorderdate.selectedDate=new Date(new Date().getTime());

deliverydate.selectedDate=new Date(new Date().getTime());

deliverydate.setFocus();

}

(2)页面加载完成的时候对应的 ActionScript 代码。

private function onCreationComplete() : void {

date1.selectedDate=new Date(new Date().getTime());

date2.selectedDate=new Date(new Date().getTime());

srv.send();

}

(3)编辑销售订单子表表格数据(DataGrid)的时候,对应的 ActionScript 代码,自动

完成商品金额=商品数量×价格。

private function itemEditEndHandler(event:DataGridEvent):void {

var grid:DataGrid = event.target as DataGrid;

var field:String = event.dataField;

var row:Number = Number(event.rowIndex);

var col:int = event.columnIndex;

if (grid != null) {

var newValue:Number = Number(grid.itemEditorInstance[grid.columns

[col].editorDataField]);

grid.dataProvider.getItemAt(row)[field]=newValue;

var totmoney:Number=0;

for(var i:int = 0;i<grid.dataProvider.length;i++) {

var quantity:Number = Number(grid.dataProvider.getItemAt(i)

["quantity"]);

var price:Number = Number(grid.dataProvider.getItemAt(i)["price"]);

第 10 章 网上商城后台之销售管理

·219·

var taxrate:Number = Number(grid.dataProvider.getItemAt(i)

["taxrate"]);

var money:Number= Math.round(quantity*price*(1+taxrate/100)*100)

/100;

grid.dataProvider.getItemAt(i)["money"]=money;

totmoney=totmoney+money;

}

grid.dataProvider.refresh();

totmoney=Math.round(totmoney*100)/100;

totalmoney.text=String(totmoney);

}

}

(4)查询完成时对应的 ActionScript 代码,把查询结果输出到界面上。

private function resultHandler(event: ResultEvent): void {

saleOrders = new ArrayCollection();

for each (var item: XML in event.result.vo) {

var saleOrder: SaleOrder = new SaleOrder();

saleOrder.saleorderno=item.saleorderno;

saleOrder.saleorderdate=DateUtil.parse(item.saleorderdate);

saleOrder.v_saleorderdate=item.saleorderdate;

saleOrder.custsuppno=item.custsuppno;

saleOrder.custsuppname=item.custsuppname;

saleOrder.deliverydate=DateUtil.parse(item.deliverydate);

saleOrder.v_deliverydate=item.deliverydate;

saleOrder.deliveryadd=item.deliveryadd;

saleOrder.totalmoney=Number(item.totalmoney);

saleOrder.empno=item.empno;

saleOrder.remark=item.remark;

saleOrder.status=item.status;

if(saleOrder.status=="0"){ saleOrder.status_caption="存盘";

}else{ saleOrder.status_caption="提交";

}

saleOrders.addItem(saleOrder);

for each (var child: XML in event.result.vo.children){

第四部分 网上商城完整实现

·220·

if(child.saleorderno==saleOrder.saleorderno){

saleOrder.lineitems.addItem(child);

}

}

}

if(saleOrders.length!=0){

dynaSaleOrder=saleOrders[0];

}else{

dynaSaleOrder= new SaleOrder();

}

}

public function fault(event: FaultEvent) : void { Alert.show("提交失败!请检查网络是否连通,或与系统管理员联系");

}

public function refresh(event: ResultEvent) : void {

srv.send();

}

public function query(event:Event) : void {

srv.send();

}

(5)<修改>按钮对应的 ActionScript 代码。

private function showForm():void {

if(maindatagrid.selectedItem!=null){

veditdetail.enabled=true;

mainform.visible=false;

lineitemform.height=mainform.height;

mainform.height=0;

lineitemform.visible=true;

dataObject=dynaSaleOrder;

deliverydate.setFocus();

}else{ Alert.show("请选择一条记录再进行修改");

}

}

第 10 章 网上商城后台之销售管理

·221·

(6)删除一个销售订单对应的 ActionScript 代码。

private function removeSaleOrder():void {

if(maindatagrid.selectedItem!=null){

dataObject=dynaSaleOrder;

var xml:XML =dataObject.toXML(dataObject);

removeaction.send(xml);

dynaSaleOrder= new SaleOrder();

}else{ Alert.show("请选择一条记录再进行删除");

}

}

private function saveSaleOrder():XML{

dataObject.saleorderno=saleorderno.text;

dataObject.saleorderdate=saleorderdate.selectedDate;

dataObject.custsuppno=String(custsuppno.data);

dataObject.deliverydate=deliverydate.selectedDate;

dataObject.deliveryadd=deliveryadd.text;

dataObject.totalmoney=Number(totalmoney.text);

dataObject.empno=empno.text;

dataObject.remark=remark.text;

var xml:XML =dataObject.toXML(dataObject);

return xml;

}

(7)新增一个销售订单对应的 ActionScript 代码。

private function create():void {

if(custsuppno.data==null||custsuppno.data==""){ Alert.show("请选择客户!");

}else{

dataObject.status="0";

var xml:XML =saveSaleOrder();

createaction.send(xml);

cancelLineItem();

}

}

private function createSubmit():void {

第四部分 网上商城完整实现

·222·

if(custsuppno.data==null||custsuppno.data==""){ Alert.show("请选择客户!");

}else{

if(dataObject.lineitems.length==0){ Alert.show("请添加货品!");

}else{

var cansubmit :Boolean=true;

for(var i:int = 0;i<dataObject.lineitems.length;i++) {

var money:Number = Number(dataObject.lineitems.getItemAt(i)

["money"]);

if(money==0){

cansubmit=false;

break;

}

}

if(cansubmit){

dataObject.status="1";

var xml:XML =saveSaleOrder();

createaction.send(xml);

cancelLineItem();

}else{ Alert.show("请检查货品数量、价格,金额不能是 0!");

}

}

}

}

private function cancelLineItem():void {

mainform.height=lineitemform.height;

mainform.visible=true;

lineitemform.height=0;

lineitemform.visible=false;

}

(8)新增一个商品明细对应的 AcionScript 代码。

private function addlineitem():void {

cgdddetail.visible=true;

cgdddetail.y=lineitemdtlbtngroup.y;

第 10 章 网上商城后台之销售管理

·223·

lineitemdatagrid2.visible=false;

lineitembtngroup.visible=false;

lineitemdtlbtngroup.visible=false;

}

public function cancelquerylineitem():void{

cgdddetail.visible=false;

lineitemdatagrid2.visible=true;

lineitembtngroup.visible=true;

lineitemdtlbtngroup.visible=true;

}

private function itemClickEvent(event:ListEvent):void {

var rowIndex:int=event.rowIndex;

//selectItem(,true,true);

}

(9)选择一个销售订单 DataGrid 上一条记录,双击的时候对应的 ActionScript 代码。

private function maindatagridClickEvent(event:ListEvent):void {

var rowIndex:int=event.rowIndex;

dynaSaleOrder=saleOrders[rowIndex];

updButton.enabled=true;

createSubmitBtn.enabled=true;

removeButton.enabled=true;

if(dynaSaleOrder.status=="1"){

updButton.enabled=false;

removeButton.enabled=false;

createSubmitBtn.enabled=false;

}

}

(10)选择一个销售订单 DataGrid 上一条记录,上下按键时对应的 ActionScript 代

码。

private function maindatagridKeyDownEvent(event:KeyboardEvent):void {

var item:Object=maindatagrid.selectedItem;

var rowIndex:int=0;

for each (var it:Object in saleOrders){

第四部分 网上商城完整实现

·224·

if(it.saleorderno==item.saleorderno){

dynaSaleOrder=saleOrders[rowIndex];

break;

}

rowIndex++;

}

updButton.enabled=true;

removeButton.enabled=true;

createSubmitBtn.enabled=true;

if(dynaSaleOrder.status=="1"){

updButton.enabled=false;

removeButton.enabled=false;

createSubmitBtn.enabled=false;

}

}

private function removelineitem():void{

if(lineitemdatagrid2.selectedItem!=null){

var it:Object=lineitemdatagrid2.selectedItem;

if(dataObject.lineitems.contains(it)){

dataObject.lineitems.removeItemAt(dataObject.lineitems.getItemIndex(it));

}

}else { Alert.show("请选择一条记录再删除");

}

dataObject.lineitems.refresh();

var totmoney:Number=0;

for(var i:int = 0;i<dataObject.lineitems.length;i++) {

var quantity:Number = Number(dataObject.lineitems.getItemAt(i)

["quantity"]);

var price:Number = Number(dataObject.lineitems.getItemAt(i)["price"]);

var taxrate:Number = Number(dataObject.lineitems.getItemAt(i)

["taxrate"]);

var money:Number= Math.round(quantity*price*(1+taxrate/100)*100)/100;

totmoney=totmoney+money;

}

totmoney=Math.round(totmoney*100)/100;

第 10 章 网上商城后台之销售管理

·225·

totalmoney.text=String(totmoney);

}

(11)打印功能的 ActionScript 代码。

private function print():void {

if(maindatagrid.selectedItem!=null){

dataObject=dynaSaleOrder;

var printJob:FlexPrintJob = new FlexPrintJob();

if(printJob.start()) {

var formPrintView:SOPrintView = new SOPrintView();

addChild(formPrintView); formPrintView.saleorderno.text ="单据编码: " + dataObject.saleorderno

+" "; formPrintView.saleorderdate.text ="订单日期: " + dataObject.v_

saleorderdate +" "; formPrintView.custsuppname.text ="客户名称: " + dataObject.

custsuppname +" "; formPrintView.deliverydate.text ="交货日期: " + dataObject.v_

deliverydate +" "; formPrintView.deliveryadd.text ="交货地点: " + dataObject.deliveryadd

+" "; formPrintView.totalmoney.text ="金额合计:"+dataObject.totalmoney +"元";

formPrintView.empno.text ="经手人: " + dataObject.empno +" ";

formPrintView.printdate.text ="打印日期: " +DateUtil.getStringDate

(new Date(new Date().getTime())) +" ";

formPrintView.myDataGrid.dataProvider = dataObject.lineitems;

printJob.addObject(formPrintView);

printJob.send();

removeChild(formPrintView);

}

}else{ Alert.show("请选择一条记录再进行打印!");

}

}

private function printall():void {

var printJob:FlexPrintJob = new FlexPrintJob();

if(printJob.start()) {

第四部分 网上商城完整实现

·226·

for(var i:int = 0;i<saleOrders.length;i++) {

var dataObject:SaleOrder=saleOrders[i];

var formPrintView:SOPrintView = new SOPrintView();

addChild(formPrintView); formPrintView.saleorderno.text ="单据编码: " + dataObject.saleorderno

+" "; formPrintView.saleorderdate.text ="订单日期: " + dataObject.v_

saleorderdate +" "; formPrintView.custsuppname.text ="客户名称: " + dataObject.

custsuppname +" "; formPrintView.deliverydate.text ="交货日期: " + dataObject.v_

deliverydate +" "; formPrintView.deliveryadd.text="交货地点:"+dataObject.deliveryadd +" ";

formPrintView.totalmoney.text ="金额合计: "+dataObject.totalmoney +"元";

formPrintView.empno.text ="经手人: " + dataObject.empno +" ";

formPrintView.printdate.text ="打印日期: " +DateUtil.getStringDate

(new Date(new Date().getTime())) +" ";

formPrintView.myDataGrid.dataProvider = dataObject.lineitems;

printJob.addObject(formPrintView);

removeChild(formPrintView);

}

printJob.send();

}

}

private function faultHander(event:FaultEvent):void{ Alert.show("连接超时,请重新登录!!","",Alert.OK,this,alertListener);

}

private function alertListener(eventObj:CloseEvent):void {

if (eventObj.detail==Alert.OK) {

var urlRequest:URLRequest = new URLRequest("login.jsp");

navigateToURL(urlRequest, "_self");

}

}

private function showDetail(event:ListEvent):void{

showForm();

if(dynaSaleOrder.status=="1"){

setEnable(false);

第 10 章 网上商城后台之销售管理

·227·

}

}

private function setEnable(enable:Boolean):void{

veditdetail.enabled=enable;

cancelBtn.enabled=true;

}

3.销售订单对应的 ActionScript 实体对象 SaleOrder.as

package {

import mx.controls.Alert;

import mx.collections.ArrayCollection;

import mx.controls.Alert;

import flash.xml.XMLDocument;

import flash.xml.XMLNode;

import mx.formatters.DateFormatter;

[Bindable]

public class SaleOrder extends Object {

public var saleorderno:String = "";

public var saleorderdate:Date ;

public var v_saleorderdate:String ;

public var custsuppno:String = "";

public var custsuppname:String = "";

public var deliverydate:Date;

public var v_deliverydate:String;

public var deliveryadd:String="";

public var totalmoney:Number=0;

public var empno:String="";

public var remark:String = "";

public var status:String = "";

public var status_caption:String = "";

public var lineitems : ArrayCollection;

public function SaleOrder() {

super();

lineitems= new ArrayCollection();

}

第四部分 网上商城完整实现

·228·

(1)把 UI 上销售订单数据,转化为 XML 格式数据,包括主子表,后台再把这个 XML

格式数据转化为 TableObject, 后我们再把 TableObject 转为 SQL 语句,序列化到数据库。

public function toXML(saleOrder:Object):XML{

var df:DateFormatter=new DateFormatter();

df.formatString="YYYY-MM-DD";

var orderdate:String=df.format(saleOrder.saleorderdate);

var deliverydate:String=df.format(saleOrder.deliverydate);

var xml:XML =

<salesorder tablename="salesorder">

<saleorderno key="saleorderno" value={saleOrder.saleorderno}

type="String" primaryKey="true" autogenerate="true"/>

<saleorderdate key="saleorderdate" value={orderdate} type=

"Date"/>

<custsuppno key="custsuppno" value={saleOrder.custsuppno}

type="String"/>

<deliverydate key="deliverydate" value={deliverydate} type=

"Date"/>

<deliveryadd key="deliveryadd" value={saleOrder.deliveryadd}

type="String"/>

<totalmoney key="totalmoney" value={saleOrder.totalmoney}

type="Number"/>

<empno key="empno" value={saleOrder.empno} type="String"/>

<remark key="remark" value={saleOrder.remark} type="String"/>

<status key="status" value={saleOrder.status} type="String"/>

</salesorder>

var doc:XMLDocument =new XMLDocument();

var childroot:XMLNode =doc.createElement("children");

childroot.attributes.tablename = "salesorderlineitem";

for(var i:int = 0;i<saleOrder.lineitems.length;i++) {

var child:XMLNode =doc.createElement("salesorderlineitem");

var quantity:Number = Number(saleOrder.lineitems.getItemAt(i)

["quantity"]);

var price:Number = Number(saleOrder.lineitems.getItemAt(i)

第 10 章 网上商城后台之销售管理

·229·

["price"]);

var taxrate:Number = Number(saleOrder.lineitems.getItemAt(i)

["taxrate"]);

var money:Number = Number(saleOrder.lineitems.getItemAt(i)

["money"]);

var goodsno:String = saleOrder.lineitems.getItemAt(i)

["goodsno"];

var goodsbn:String = saleOrder.lineitems.getItemAt(i)

["goodsbn"];

var specvalues:String = saleOrder.lineitems.getItemAt(i)

["specvalues"];

var batchno:String = saleOrder.lineitems.getItemAt(i)

["batchno"];

var checkoutquantity:Number =0;

var goodsnoNode:XMLNode =doc.createElement("goodsno");

goodsnoNode.attributes.type="String";

goodsnoNode.attributes.value=goodsno;

goodsnoNode.attributes.key="goodsno";

child.appendChild(goodsnoNode);

var goodsbnNode:XMLNode =doc.createElement("goodsbn");

goodsbnNode.attributes.type="String";

goodsbnNode.attributes.value=goodsbn;

goodsbnNode.attributes.key="goodsbn";

child.appendChild(goodsbnNode);

var specvaluesNode:XMLNode =doc.createElement("specvalues");

specvaluesNode.attributes.type="String";

specvaluesNode.attributes.value=specvalues;

specvaluesNode.attributes.key="specvalues";

child.appendChild(specvaluesNode);

var batchnoNode:XMLNode =doc.createElement("batchno");

batchnoNode.attributes.type="String";

batchnoNode.attributes.value=batchno;

batchnoNode.attributes.key="batchno";

第四部分 网上商城完整实现

·230·

child.appendChild(batchnoNode);

var quantityNode:XMLNode =doc.createElement("quantity");

quantityNode.attributes.type="Number";

quantityNode.attributes.value=quantity;

quantityNode.attributes.key="quantity";

child.appendChild(quantityNode);

var priceNode:XMLNode =doc.createElement("price");

priceNode.attributes.type="Number";

priceNode.attributes.value=price;

priceNode.attributes.key="price";

child.appendChild(priceNode);

var taxrateNode:XMLNode =doc.createElement("taxrate");

taxrateNode.attributes.type="Number";

taxrateNode.attributes.value=taxrate;

taxrateNode.attributes.key="taxrate";

child.appendChild(taxrateNode);

var moneyNode:XMLNode =doc.createElement("money");

moneyNode.attributes.type="Number";

moneyNode.attributes.value=money;

moneyNode.attributes.key="money";

child.appendChild(moneyNode);

var checkoutquantityNode:XMLNode =doc.createElement

("checkoutquantity");

checkoutquantityNode.attributes.type="Number";

checkoutquantityNode.attributes.value=checkoutquantity;

checkoutquantityNode.attributes.key="checkoutquantity";

child.appendChild(checkoutquantityNode);

childroot.appendChild(child);

}

xml.appendChild(childroot);

return xml;

第 10 章 网上商城后台之销售管理

·231·

}

}

}

10.1.3 销售订单序列化数据到数据库 对于新增销售订单,我们使用默认的公共的 CreateAction 就可以了,没什么特别的,

因为只涉及本身销售订单主子表数据的增加。

<url-mapping action="createsaleorder.do" description=" 新 增 销 售 订 单 "

class="com.softbnd.jetblue.saas.scm.sales.action.CreateSaleOrderAction"/>

相应的 Java 代码如下。

第一步:采购退货单存盘。对应的文件为 CreateSaleOrderAction.java。

package com.softbnd.jetblue.saas.scm.sales.action;

import com.softbnd.jetblue.saas.webframework.action.CreateAction;

/**

* 2008-6-23

* @ author Xing Botao

*/

public class CreateSaleOrderAction extends CreateAction{

public CreateSaleOrderAction(){

super();

}

@Override

public String getprefixword() {

return "SO";

}

}

这里,我们依然是简单的继承一下 CreateAction 就可以了,我们只是设置了一下自动

生成销售订单流水号码的前缀为”SO”,销售订单的主子表就可以自动存盘了。

第四部分 网上商城完整实现

·232·

10.1.4 销售订单查询的实现 销售订单的查询,我们依然用 LogonQueryAction 类来实现,这里就不多讲了,参考以

前的代码来实现。

10.1.5 销售订单的打印 销售订单的打印,也跟采购模块的打印代码是类似的,这里就不多举例了。打印效果

如图 10-3 所示。

图 10-3 销售订单打印效果

10.2 销售出库的实现

销售出库单是本商城程序的核心,也是 B2B2C 实现的核心,本商城的销售出库单,

也是对应商品供应商的销售出库单,通过这个平台,我们把商城和供应商紧密联系到了一

起。采购入库的原理也是类似的,一个采购入库单,也是对应商品供应商的销售出库单。

另外,如果采购方也在这个平台上,也会自动为销售出库单的客户自动生成一张采购订单。

其业务逻辑原理如图 10-4 所示。

图 10-4 销售出库的业务逻辑原理 图 10-5 网上商城销售出库单的来源

第 10 章 网上商城后台之销售管理

·233·

对于销售出库来说,它有三个来源,第一个数据可以来自于销售订单,当我们签订销

售合同后,可以分多次出库。还有一个来源就是直接出库,直接添加销售出库单。第三个

来源则是针对网上订单来说的,从网上商城的每一笔订单或者从淘宝上来的每一笔订单,

都可以算是一个销售出库单,所以销售出库单的表结构设计得比较复杂,如图 10-5 所示。

对于前两类来源来说,销售出库单的主界面如图 10-6 所示。

图 10-6 销售出库单的主界面

新增和修改界面,如图 10-7 所示。

图 10-7 销售出库单的新增和修改界面

第四部分 网上商城完整实现

·234·

10.2.1 销售出库单的数据库设计 由于销售出库单跟网上订单交织在了一起,所以表结构相对来说,还是很复杂的。

销售出库单主表 saleoutstockentry,如表 10-3 所示。

表 10-3 销售出库单主表 saleoutstockentry

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售出库单编码 saleoutstockentryno Varchar(20) Y(自增) N

网上商城/商店下单日期 orderdate DATE N N

出库日期 saleoutstockentrydate

DATE N N

客户编码 custsuppno Varchar(22) N Y

收货人编码(本网店使用) usrno Varchar(10) N Y

销售订单编码 saleorderno varchar(20) N Y

原网店订单编码(整合其他网店使

用) eshoporderno varchar(20) N Y

交货地点 deliveryadd varchar(100) N Y

收货人电话(整合其他网店使用) shiptel Varchar(22) Y

快运公司 shipping varchar(50) N Y

已收金额 receivedmoney numeric(11,2) N Y

退货金额 returnedmoney numeric(11,2) N Y

仓库编码 warehouseno Varchar(20) N Y

网 店 销 售 订 单 状 态 ( 同 步 淘 宝

/Shopex 之类的时候用) eshopsaleorderstatus

Varchar(20) N Y

是否已经收款(0 未收款 1 已收

款) ispaied Char(1) N Y

付款类型(货到付款|自提|平邮|快递|EMS)

payway varchar(20) N Y

运费 freight numeric(11,2) N Y

订单状态(01 正常 02 关闭、03 缺

货 04 已配货(不允许修改或者取

消) 05 发货、06 完成、07 退货)

orderstatus varchar(2) N Y

第 10 章 网上商城后台之销售管理

·235·

续表

字段名称 编码 字段类型 是否主键 是否允许 NULL

金额合计 totalmoney Numeric(11,2) N N

业务员 empno Varchar(10) N Y

备注 remark Varchar(500) N Y

状态(0 新增 1 提交) status char(1) N N

租户编码 tenantno varchar(22) N N

销售出库单子表 saleoutlineitem,如表 10-4 所示。

表 10-4 销售出库单子表 saleoutlineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售出库单主表 saleoutstockentryno varchar(20) N N

子表 ID Itemid integer Y(自增) N

销售订单编码 saleorderno varchar(20) N Y

货品编码 Goodsno varchar(20) N N

货号 goodsbn varchar(30) N N

规格 specvalues varchar(500) N Y

批次 batchno varchar(20) N Y

退货数量 Quantity numeric(11,2) N N

退货价格 Price numeric(11,2) N N

金额 Quantity numeric(11,2) N N

租户编码 tenantno varchar(22) N N

10.2.2 销售出库单的 Flex 实现 销售出库单的界面要稍微复杂一点,它有两个关键点,第一就是选择客户的时候,我

们会自动把这个客户近期所有的销售订单关联出来。第二个就是多了一个是否“已付款”

的选择框,如果我们选中了已付款,则此张销售出库单就不会形成应收账款,否则就会形

成一张应收账款。销售出库单 Flex 实现的界面如图 10-8 所示。

第四部分 网上商城完整实现

·236·

图 10-8 销售出库单 Flex 实现的界面

10.2.3 销售出库单业务逻辑的实现 销售出库单本身的复杂性,也就决定了他的业务逻辑比较复杂。我们先看一下销售出

库单业务逻辑的定义,对应的 XML 如下。

<url-mapping description="新增销售出库单" action="createsaleoutstockentry.

do" class="com.softbnd.jetblue.saas.scm.sales.action.

SaleOutstockEntryAction"> <businessprocess description="新增销售出库单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.

SaleOutUpdateStorageLedgerAction"/> <component name="updatePurchaseOrder" step="2" description="修改

销售订单提货数量"

class="com.softbnd.jetblue.saas.scm.sales.action.UpdateSaleOutOrderCheckOutQ

TYAction"/>

<condiation key="ispaied" type="String" value="1" op="=="> <component name="purchasepayment" step="3" description="修改

销售收款台账"

class="com.softbnd.jetblue.saas.scm.finance.action.UpdateSaleOutBalanceMoney

Action"/>

第 10 章 网上商城后台之销售管理

·237·

</condiation> <component name="updatePurchaseOrder" step="4" description="为可

登录本系统的供货商生成销售出库单"

class="com.softbnd.jetblue.saas.scm.sales.action.SaleOutstock2SuppEntryActio

n"/>

</condiation>

</businessprocess>

</url-mapping>

第一步:销售出库单本身主子表的新增,我们的定义是 createsaleoutstockentry.do,对

应的 Java 代码如下。

package com.softbnd.jetblue.saas.scm.sales.action;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.saas.webframework.action.CreateAction;

/**

* 2008-6-27

* @ author Xing Botao

*/

public class SaleOutstockEntryAction extends CreateAction{

public SaleOutstockEntryAction(){

super();

}

@Override

protected TableObject xml2Java(HttpServletRequest request,

HttpServletResponse response) {

TableObject vo=super.xml2Java(request, response);

if("1".equals(vo.getAttribute("ispaied"))){

vo.setAttribute("receivedmoney", vo.getAttribute("totalmoney"),

TableObject.DOUBLE);

第四部分 网上商城完整实现

·238·

}

return vo;

}

@Override

public String getprefixword() {

return "SX";

}

}

这里,我们依然是简单地继承一下 CreateAction 就可以了,销售出库单就可以自动存

盘了,不过,我们这里增加了一段逻辑,如果用户选择了“已付款”选择框,则我们的表

结构已付款的字段就把这个数值记上,与应收账款的值是一致的。

第二步:修改库存台账,我们使用了 condiation 标签,当我们确认提交的时候(只

是 存 盘 操 作 , 不 提 交 , 则 不 执 行 以 下 操 作 ), 才 会 执 行 以 下 步 骤 。 对 应 的 类 是

SaleOutUpdateStorageLedgerAction.java。

public class SaleOutUpdateStorageLedgerAction extends DefaultAction implements

IAction{

/** * 销售出库/直接出库,更改库存

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

InventoryledgerService service=new InventoryledgerService();

for(TableObject child:vo.getChildren()){

child.setAttribute("warehouseno", vo.getAttribute("warehouseno"));

service.checkoutStorageLedger(child,uta,conn);

}

return vo;

}

}

第三步:在 condiation 标签下,修改销售订单提货数量,对应的类是 UpdateSale-

OutOrderCheckOutQTYAction。

第 10 章 网上商城后台之销售管理

·239·

package com.softbnd.jetblue.saas.scm.sales.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.sales.service.impl.SaleService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-3

* @ author Xing Botao

*/

public class UpdateSaleOutOrderCheckOutQTYAction extends DefaultAction

implements IAction{

/** * 销售出库,修改对应销售订单已提货数量

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

SaleService service=new SaleService();

service.updateSaleOutOrderCheckOutQTY(vo, uta,conn);

return vo;

}

}

第四步:修改销售收款台账,注意这里我们再次使用了一个 condiation 标签,只有

ispaid 为 1 的时候,即用户选择了“已付款”选择框,我们就会去修改用户的销售收款台

账,已经收取,这是 condiation 加 condiation 判断,采用了 XML 配置业务逻辑,而不是通

过程序判断,这样用户就可以随意修改他的业务逻辑,避免了 Java 代码与用户业务逻辑的

深度耦合,对应的类是 UpdateSaleOutBalanceMoneyAction。

package com.softbnd.jetblue.saas.scm.finance.action;

import java.SQL.Connection;

第四部分 网上商城完整实现

·240·

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.finance.service.impl.FinanceService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-3

* @ author Xing Botao

*/

public class UpdateSaleOutBalanceMoneyAction extends DefaultAction implements

IAction{

public UpdateSaleOutBalanceMoneyAction(){

}

/** * 销售出库,新增应收款

* @param vo

* @return

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

FinanceService service=new FinanceService();

vo.setAttribute("csno", vo.getAttribute("custsuppno"));

service.balance(vo, uta,conn);

return vo;

}

}

第五步:为可登录本系统的供货商生成销售出库单,这也是我们 B2B2C 平台实现的

核 心 , 一 个 商 城 的 销 售 出 库 单 , 也 是 对 应 供 应 商 的 销 售 出 库 单 , 对 应 的 类 是

SaleOutstock2SuppEntryAction.java。

package com.softbnd.jetblue.saas.scm.sales.action;

第 10 章 网上商城后台之销售管理

·241·

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

public class SaleOutstock2SuppEntryAction extends DefaultAction implements

IAction{

public SaleOutstock2SuppEntryAction(){

super();

}

public TableObject execute(TableObject vo, UTransaction uta,Connection conn)

throws Exception {

for(TableObject child:vo.getChildren()){

TableObject saleoutstockentry=new TableObject();

saleoutstockentry.setTableName("saleoutstockentry");

saleoutstockentry.setPrimaryKey("saleoutstockentryno");

saleoutstockentry.setPrimaryType(TableObject.STRING);

saleoutstockentry.setAutoGenerate(true);

saleoutstockentry.setAttribute("tenantno", vo.getAttribute

("tenantno"));

}

return vo;

}

}

10.2.4 销售出库单查询的实现 销售出库单的查询,我们依然用 LogonQueryAction 类来实现,这里就不多讲了。参考

以前的代码来实现。

10.2.5 销售出库单的打印 销售出库单的打印,与以前的打印代码是类似的,这里就不多举例了。打印效果如

图 10-9 所示:

第四部分 网上商城完整实现

·242·

图 10-9 销售出库单的打印效果

10.3 销售收款的实现

销售收款,就是商品根据销售出库单出库后,当时没有收款,事后收款的过程。一张

销售出款库,可分多次收款。当然,一个收款单,也可以对应多个销售收款单。如果我们

在做销售出库单的时候,选中了“已收款”选择框,则此出库单不会出现在待收款的列表

中。销售收款的主界面如图 10-10 所示。

图 10-10 销售收款的主界面

第 10 章 网上商城后台之销售管理

·243·

新增和修改界面,如图 10-11 所示。

图 10-11 销售收款的新增和修改界面

在图 10-11 的界面上,比较纠结的是当我们选择客户的时候,会自动把此客户所有的

销售出库单列出来,然后你可以删除某个销售出库单,也可以填写针对某个出库单的本次

收款,只要本地收款小于应收金额,换句话说,未收金额大于 0 的时候,本张出库单下次

还可以再次进行收款。

10.3.1 销售收款单的数据库设计 销售收款单主表 salecollectentry,如表 10-5 所示。

表 10-5 销售收款单主表 salecollectentry

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售收款单编码 scentryno Varchar(20) Y(自增) N

收款日期 scentrydate DATE N N

仓库编码 warehouseno Varchar(20) N N

金额合计 totalmoney Numeric(11,2) N N

业务员 empno Varchar(10) N Y

第四部分 网上商城完整实现

·244·

续表

字段名称 编码 字段类型 是否主键 是否允许 NULL

备注 remark Varchar(500) N Y

状态(0 新增 1 提

交) status char(1) N N

租户编码 tenantno varchar(22) N N

销售收款单子表 salecollectlineitem,如表 10-6 所示。

表 10-6 销售收款单子表 salecollectlineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售收款单编码 scentryno varchar(20) N N

子表 ID Itemid integer Y(自增) N

销售出库单编码 saleoutentryno varchar(20) N N

出库日期 checkoutdate Date N Y

总金额 totalmoney numeric(11,2) N Y

已收金额 receivedmoney numeric(11,2) N Y

退货金额 returnedmoney numeric(11,2 N Y

本次收款 collectmoney numeric(11,2) N Y

租户编码 tenantno varchar(22) N N

10.3.2 销售收款单界面的 Flex 实现 销售收款单的用户界面,跟前面单据,技术实现是差不多的,Flex 实现的源代码,这

里就不再列出了。

10.3.3 销售收款单业务逻辑的实现 新增销售收款单后,我们要回填销售出库单上付款金额,还要减少应收账款,对应的

XML 如下。

<url-mapping description="新增销售收款单 " action="createsaleCollect.do"

class="com.softbnd.jetblue.saas.scm.sales.action.SaleCollectAction">

第 10 章 网上商城后台之销售管理

·245·

<businessprocess description="新增销售收款单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatePurchaseOrder" step="1" description="修改

销售出库单已收金额"

class="com.softbnd.jetblue.saas.scm.sales.action.UpdateSaleOutOrderReceivedm

oneyAction"/> <component name="purchasepayment" step="3" description="冲减该客

户对应应收金额"

class="com.softbnd.jetblue.saas.scm.finance.action.UpdateCustSaleReceiveMone

yAction"/>

</condiation>

</businessprocess>

</url-mapping>

相应的 Java 代码如下。

第一步:销售收款单存盘,createsaleCollect.do 对应的 Java 类为:SaleCollectAction.java。

package com.softbnd.jetblue.saas.scm.sales.action;

import com.softbnd.jetblue.saas.webframework.action.CreateAction;

/**

* 2008-7-11

* @ author Xing Botao

*/

public class SaleCollectAction extends CreateAction{

public SaleCollectAction(){

super();

}

@Override

public String getprefixword() {

return "SC";

}

}

这里,我们依然是简单地继承一下 CreateAction 就可以了,销售付款单就可以自动存

第四部分 网上商城完整实现

·246·

盘了,这里我们给销售收款单的前缀是 SC。

第二步:修改销售出库单已收金额,对应的类是 UpdateSaleOutOrderReceivedmoneyAction.

java。

package com.softbnd.jetblue.saas.scm.sales.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.sales.service.impl.SaleService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-13

* @ author Xing Botao

*/

public class UpdateSaleOutOrderReceivedmoneyAction extends DefaultAction

implements IAction {

/** * 销售收款,修改销售出库单已收金额

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

SaleService service=new SaleService();

service.updateSaleOrderReceivedMoney(vo,uta,conn);

return vo;

}

}

第三步:冲减该客户对应的应收金额,对应的类是 UpdateCustSaleReceiveMoneyAction。

package com.softbnd.jetblue.saas.scm.finance.action;

第 10 章 网上商城后台之销售管理

·247·

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.finance.service.impl.FinanceService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-26

* @ author Xing Botao

*/

public class UpdateCustSaleReceiveMoneyAction extends DefaultAction implements

IAction{

public UpdateCustSaleReceiveMoneyAction(){

}

/** * 销售收款,冲减该客户对应的应收金额

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta ,Connection conn)

throws Exception {

FinanceService service=new FinanceService();

service.reducebalance(vo, uta,conn);

return vo;

}

}

10.3.4 销售收款单查询的实现 销售收款单的查询,我们依然用 LogonQueryAction 类来实现,这里就不多讲了。参考

以前的代码来实现。

第四部分 网上商城完整实现

·248·

10.3.5 销售收款单的打印 列出打印效果,如图 10-12 所示:

图 10-12 销售收款单的打印效果

10.4 销售退货的实现

销售退货,就是销售后的货品,由于某种原因,退回给原供应商的业务过程。它是销

售出库的逆过程。一张销售出库单,可分多次退货。由于我们考虑的是 B2B2C 平台,所以

每次销售退货都会相应地给商品的供货商自动生成一张销售退货单。

销售退货单的主界面如图 10-13 所示。

图 10-13 销售退货单的主界面

第 10 章 网上商城后台之销售管理

·249·

新增和修改界面,如图 10-14 所示。

图 10-14 销售退货单的新增和修改界面

10.4.1 销售退货单的数据库设计 销售退货单主表 saleoutreturnentry,如表 10-7 所示。

表 10-7 销售退货单主表 saleoutreturnentry

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售退货单编码 salereturnentryno Varchar(20) Y(自增) N

退货日期 salereturndate DATE N N

销售出库单编码 salecheckoutorderno Varchar(20) N N

客户编码 custsuppno Varchar(22) N N

仓库编码 warehouseno Varchar(20) N N

金额合计 totalmoney Numeric(11,2) N N

业务员 empno Varchar(10) N Y

备注 remark Varchar(500) N Y

状态(0 新增 1 提交) status char(1) N N

租户编码 tenantno varchar(22) N N

第四部分 网上商城完整实现

·250·

销售退货单子表 salereturnlineitem,如表 10-8 所示。

表 10-8 销售退货单子表 salereturnlineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

销售退货单主表 salereturnentryno varchar(20) N N 子表 ID Itemid integer Y(自增) N 采购入库单编码 salecheckoutordern

o varchar(20) N N

销售出库单编码 Goodsno varchar(20) N N 货号 goodsbn varchar(30) N N 规格 specvalues varchar(500) N Y 批次 batchno varchar(20) N Y 退货数量 Quantity numeric(11,2) N N 退货价格 Price numeric(11,2) N N 金额 Quantity numeric(11,2) N N 租户编码 tenantno varchar(22) N N

10.4.2 销售退货单界面的 Flex 实现 销售退货单的用户界面,跟以前界面实现类似, Flex 实现的源代码,也就不粘贴了,

不过,其界面也是蛮复杂的,第一个,如果选择了某个客户,则在出库单的下拉列表中,

会自动关联出此客户的所有的销售出库单。

10.4.3 销售退货单序列化数据到数据库 新增销售退货单后,业务逻辑也蛮复杂的,我们不仅要回填销售出库单的退货数量和

退货金额,要减少应收账款,还要为货品的供应商生成同样的一个退货单,对应的 XML

如下。

<url-mapping description="新增销售退货单" action="createsalereturnentry.do"

class="com.softbnd.jetblue.saas.scm.sales.action.SaleReturnEntryAction"> <businessprocess description="新增销售退货单">

<condiation key="status" type="String" value="1" op="=="> <component name="updatestock" step="1" description="修改库存台账"

class="com.softbnd.jetblue.saas.scm.inventory.action.SaleReturnAction"/>

第 10 章 网上商城后台之销售管理

·251·

<component name="updatePurchaseOrder" step="2" description="修改

销售出库单退货数量"

class="com.softbnd.jetblue.saas.scm.sales.action.UpdateSaleOutOrderReturnedQ

TYAction"/>

<condiation key="reducingflag" type="String" value="1" op="=="> <component name="purchasepayment" step="3" description="修改

销售出库单主表退货金额"

class="com.softbnd.jetblue.saas.scm.sales.action.UpdateSaleReturnMoneyAction

"/> <component name="purchasepayment" step="3" description="冲减

该客户对应的应收金额"

class="com.softbnd.jetblue.saas.scm.finance.action.UpdateCustSaleRuturnMoney

Action"/>

</condiation>

</condiation>

</businessprocess>

</url-mapping>

相应的 Java 代码如下。

第一步:销售退货单存盘,对应的类是 SaleReturnEntryAction.java。

package com.softbnd.jetblue.saas.scm.sales.action;

import com.softbnd.jetblue.saas.webframework.action.CreateAction;

/**

* 2008-7-16

* @ author Xing Botao

*/

public class SaleReturnEntryAction extends CreateAction{

public SaleReturnEntryAction(){

super();

}

@Override

public String getprefixword() {

return "SR";

}

}

第四部分 网上商城完整实现

·252·

这里,我们依然是简单地继承一下 CreateAction 就可以了,销售退货单就可以自动存

盘了。

第二步:销售退货,修改库存台账,增加货品入库数量,对应的类是 SaleReturnAction.

java。

package com.softbnd.jetblue.saas.scm.inventory.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import

com.softbnd.jetblue.saas.scm.inventory.service.impl.InventoryledgerService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-16

* @ author Xing Botao

*/

public class SaleReturnAction extends DefaultAction implements IAction{

public SaleReturnAction(){

super();

}

/** * 销售退货,修改库存台账,增加货品入库数量

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

InventoryledgerService service=new InventoryledgerService();

for (TableObject child : vo.getChildren()) {

child.setAttribute("warehouseno", vo.getAttribute("warehouseno"));

service.checkinStorageLedger(child, uta,conn);

}

return vo;

}

}

第 10 章 网上商城后台之销售管理

·253·

第 三 步 : 修 改 销 售 出 库 单 子 表 退 货 数 量 , 对 应 的 类 是 UpdateSaleOutOrder-

ReturnedQTYAction。

package com.softbnd.jetblue.saas.scm.sales.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.sales.service.impl.SaleService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-16

* @ author Xing Botao

*/

public class UpdateSaleOutOrderReturnedQTYAction extends DefaultAction

implements IAction{

/** * 销售退货,修改销售出库单退货数量

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

SaleService service=new SaleService();

service.updateSaleReturnQTY(vo, uta,conn);

return vo;

}

}

第四步:修改销售出库单主表退货金额,对应的类是 UpdateSaleReturnMoneyAction。

package com.softbnd.jetblue.saas.scm.sales.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

第四部分 网上商城完整实现

·254·

import com.softbnd.jetblue.saas.scm.sales.service.impl.SaleService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-16

* @ author Xing Botao

*/

public class UpdateSaleReturnMoneyAction extends DefaultAction implements

IAction {

/** * 销售退货的时候,修改销售出库单主表退货金额

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

SaleService service=new SaleService();

service.updateSaleReturnMoney(vo, uta,conn);

return vo;

}

}

注意:这一步采用了判断条件。

<condiation key="reducingflag" type="String" value="1" op="=="> <component name="purchasepayment" step="3" description="修改销售出库单主表退货金

额" class="com.softbnd.jetblue.saas.scm.sales.action.

UpdateSaleReturnMoneyAction"/>

只有当用户在界面上选择了“退货冲减应收款”,这一步才会执行。

第五步:冲减该客户对应应收金额,对应的类是 UpdateCustSaleRuturnMoneyAction

.java。

package com.softbnd.jetblue.saas.scm.finance.action;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.doman.TableObject;

第 10 章 网上商城后台之销售管理

·255·

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.scm.finance.service.impl.FinanceService;

import com.softbnd.jetblue.webframework.action.DefaultAction;

import com.softbnd.jetblue.webframework.action.IAction;

/**

* 2008-7-26

* @ author Xing Botao

*/

public class UpdateCustSaleRuturnMoneyAction extends DefaultAction implements

IAction{

public UpdateCustSaleRuturnMoneyAction(){

}

/** * 销售退货,冲减该客户对应的应收金额

* @throws Exception

*/

public TableObject execute(TableObject vo, UTransaction uta,Connection conn )

throws Exception {

FinanceService service=new FinanceService();

service.reducebalance(vo, uta,conn);

return vo;

}

}

10.4.4 销售退货单查询的实现 销售退货的查询,我们依然用 LogonQueryAction 类来实现,这里就不多说了。参考以

前的代码来实现。

10.4.5 销售退货单的打印 销售退货单的打印,跟以前的打印代码是类似的,这里就不多举例了。

第四部分 网上商城完整实现

·256·

10.5 本章小结

本章主要讲解了进销存销售模块的实现,包括销售订单,销售出库,销售收款以及销

售退货等功能模块。本章也是构建自己的 SSH 框架理论模型的继续实现,对于新增、修改、

查询、删除以及打印等技术,也是跟采购管理模块很相似的。任何一个框架,从实现到成

熟,都是需要花很长时间去重构。

第 11 章 网上商城后台之库存管理

·257·

第 11 章 网上商城后台之库存管理

11.1 直接入库的实现

直接入库与采购入库的操作是一样的,主要区别是采购入库有供应商,而直接入库则

没有供应商,主要用于日常消耗品、办公用品的录入等,或者盘点的时候出现盘盈,也可

以作为直接入库,把货品再次录入到系统中。

从业务流程上,直接入库,不会形成应付账款,因为没有跟供应商关联起来,所以其

业务相对很简单,直接入库的界面如图 11-1 所示。直接入库的新增与修改界面如图 11-2

所示。

图 11-1 直接入库界面

第四部分 网上商城完整实现

·258·

直接入库的实现细节,参考采购的实现就可以了,这里仅列出界面和数据库设计。

图 11-2 直接入库的新增和修改界面

直接入库数据库表结构设计如下。

直接入库单主表 feedin,如表 11-1 所示。

表 11-1 直接入库单主表 feedin

字段名称 编码 字段类型 是否主键 是否允许 NULL

入库编号 Feedinno varchar(20) Y(自增) N

入库日期 Feedindate DATE N N

仓库编码 warehouseno varchar(20) N N

金额合计 Totalmoney numeric(11,2) N N

业务员 Empno varchar(10) N Y

备注 Remark varchar(500) N Y

状态(0 新增 1 提交) Status char(1) N N

租户编码 Tenantno varchar(22) N N

直接入库单子表 feedinlineitem,如表 11-2 所示。

第 11 章 网上商城后台之库存管理

·259·

表 11-2 直接入库单子表 feedinlineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

主表 no Feedinno varchar(20) N N

子表 ID Itemid integer Y(自增) N

货品编码 Goodsno varchar(20) N N

货号 Goodsbn varchar(30) N N

批次 Batchno varchar(30) N N

规格 Specvalues varchar(500) N Y

入库数量 Quantity numeric(11,2) N N

价格 Price numeric(11,2) N N

金额 Money numeric(11,2) N N

租户编码 Tenantno varchar(22) N N

11.2 直接出库的实现

直接出库与销售出库的操作是一样的,主要区别是销售出库有客户,即商品卖给谁了,

而直接出库则没有客户,主要用于日常消耗品、办公用品的领用等,或者盘点的时候,出

现盘亏,也可以作为直接出库,把货品从库存中移走。

从业务流程上,直接出库,也不会形成应收账款,因为没有跟客户关联起来,所以其

业务相对很简单。

直接出库的实现细节,参考销售出库的实现就可以了,这里仅列出界面和数据库设计。

直接出库数据库表结构设计如下。

直接出库单主表 outstock,如表 11-3 所示。

表 11-3 直接出库单主表 outstock

字段名称 编码 字段类型 是否主键 是否允许 NULL

出库单编码 Outstockno varchar(20) Y(自增) N

出库日期 outstockentrydate DATE N N

第四部分 网上商城完整实现

·260·

续表

字段名称 编码 字段类型 是否主键 是否允许 NULL

仓库编码 warehouseno varchar(20) N N

金额合计 Totalmoney numeric(11,2) N N

业务员 Empno varchar(10) N Y

备注 Remark varchar(500) N Y

状态(0 新增 1 提交) Status char(1) N N

租户编码 Tenantno varchar(22) N N

图 11-3 直接出库的界面

直接出库单子表 outstocklineitem,如表 11-4 所示。

表 11-4 直接出库单子表 outstocklineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

主表 no Outstockno varchar(20) N N

子表 ID Itemid integer Y(自增) N

货品编码 Goodsno varchar(20) N N

货号 Goodsbn varchar(30) N N

批次 Batchno varchar(30) N N

规格 Specvalues varchar(500) N Y

第 11 章 网上商城后台之库存管理

·261·

续表

字段名称 编码 字段类型 是否主键 是否允许 NULL

出库数量 Quantity numeric(11,2) N N

价格 Price numeric(11,2) N N

金额 Money numeric(11,2) N N

租户编码 Tenantno varchar(22) N N

11.3 调拨单的实现

调拨单就是在一个单位内部的库房之间,进行商品的调拨,从一个库房出货到另外一

个库房。所以一张调拨单可以看做是由一张出库单和一张入库单两张单据组成的。调拨单

在一个多店管理的应用当中,尤其重要。比如,对一个拥有多个店铺的商家来说,每个店

的实时库存量和销售统计,对其来说是很关键的。一个店的滞销商品,对另外一个店来讲,

可能就是畅销的,调拨出库在这里就可能发挥出很好的作用了。

调拨出库的主界面如图 11-4 所示。

图 11-4 调拨出库的主界面

第四部分 网上商城完整实现

·262·

调拔出库新增和修改的界面如图 11-5 所示。

图 11-5 调拨出库的新增和修改界面

调拨单数据库表结构设计如下。

调拨单主表 allocationentry,如表 11-5 所示。

表 11-5 调拨单主表 allocationentry

字段名称 编码 字段类型 是否主键 是否允许 NULL

调拨单编码 allocationentryno varchar(20) Y(自增) N

调拨日期 outstockentrydate DATE N N

调出仓库编码 checkoutwarehouseno varchar(20) N N

移入仓库编码 checkinwarehouseno varchar(20) N N

金额合计 Totalmoney numeric(11,2) N N

业务员 Empno varchar(10) N Y

备注 Remark varchar(500) N Y

状态(0 新增 1 提交) Status char(1) N N

租户编码 Tenantno varchar(22) N N

第 11 章 网上商城后台之库存管理

·263·

调拨单子表 allocationentrylineitem,如表 11-6 所示。

表 11-6 调拨单子表 allocationentrylineitem

字段名称 编码 字段类型 是否主键 是否允许 NULL

主表 no Allocationentryno varchar(20) N N

子表 ID Itemid integer Y(自增) N

货品编码 Goodsno varchar(20) N N

货号 Goodsbn varchar(30) N N

批次 Batchno varchar(30) N N

规格 Specvalues varchar(500) N Y

调拨数量 Quantity numeric(11,2) N N

调拨价格 Price numeric(11,2) N N

金额 Money numeric(11,2) N N

租户编码 Tenantno varchar(22) N N

11.4 库存台账查询的实现

所谓库存台账,简单地来讲,就是查询目前所有的库房中现存商品数量。我们的查询

条件有“货号”、“商品名称”、“商品类别”、“仓库”。查询出来的 重要的属性就是“商品

编码”、“商品名称”、“规格”和“当前库存”,查询结果如图 11-6 所示。

库存台账的实现,就是我们以前所讲功能模块的一个 简单的查询,是不需要写任何

Java 代码的,我们只需要定义一个 Action 并写出相应的 SQL 语句就可以了。

<url-mapping action="querystorageledger.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction" description="查询库存台账">

<query SQL="select

storageledger.*,warehousename,goodsname,gtname ,puname from

storageledger,goods,packageunit,goodstype,warehouse where

storageledger.goodsno=goods.goodsno and goods.goodsunitno=packageunit.

第四部分 网上商城完整实现

·264·

puno and goods.goodstypeno=goodstype.gtno and

storageledger.warehouseno=warehouse.warehouseno and storageledger.

tenantno in ('#tenantno#') and goods.tenantno in ('#tenantno#') and packageunit.

tenantno in ('#tenantno#') and goodstype.tenantno in ('#tenantno#',

'0000000000') and warehouse.tenantno in ('#tenantno#','0000000000') order by

storageledger.warehouseno,storageledger.goodsno">

<parameter requestname="goodsbn" name="storageledger.goodsbn"

type="String"/>

<parameter name="goodsname" type="String"/>

<parameter name="goodstypeno" type="String"/>

<parameter requestname="warehouseno" name="storageledger.

warehouseno" type="String" op="="/>

</query>

</url-mapping>

我们依然使用默认的 LogonQueryAction 完成我们的查询。

图 11-6 库存台账查询结果

11.5 应付账款查询的实现

应付账款,就是我们采购商品后,应付给经销商的货款。由于其实现简单,只是一个

简单的查询,这里只列出界面,我们不需要写 Java 代码,还是利用默认的 LogonQueryAction

第 11 章 网上商城后台之库存管理

·265·

类就可以了。应付账款查询的界面如图 11-7 所示。

图 11-7 应付账款的查询界面

其在 XML 里面的定义为:

<url-mapping action="querycustsuppaccountpay.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select csno ,csname,accountpaiedmoney,accountpaymoney,

custtype.name as custtypename from custsupp,custtype where custtype.typeno=

custsupp.custtype and custsupp.status='0' and (custsupp.custtype='1' or

custsupp.custtype='2') and custsupp.tenantno in ('#tenantno#') and

custtype.tenantno in ('#tenantno#','0000000000')">

<parameter requestname="csname" name="custsupp.csname" type=

"String"/>

</query>

</url-mapping>

11.6 应收账款查询的实现

所谓应收账款,就是销售出库后,客户没有立即付款。跟应付账款一样,我们这里也

是一个很简单的实现,利用默认的 LogonQueryAction 类做查询,不需要另外写 Java 代码。

第四部分 网上商城完整实现

·266·

其用户界面如图 11-8 所示。

图 11-8 应收账款的查询界面

其在 XML 中的定义如下:

<url-mapping action="querycustsuppaccountreceive.do"

class="com.softbnd.jetblue.saas.webframework.action.LogonQueryAction">

<query SQL="select csno,csname,accountreceivemoney,accountreceivedmoney

,custtype.name as custtypename from custsupp,custtype where custtype.typeno=

custsupp.custtype and custsupp.status='0' and ( custsupp.custtype='0' or

custsupp.custtype='2') and custsupp.tenantno in ('#tenantno#') and custtype.

tenantno in ('#tenantno#','0000000000')">

<parameter requestname="csname" name="csname" type="String"/>

</query>

</url-mapping>

11.7 本章小结

本章主要是讲解了进销存库存管理的实现,包括直接入库、直接出库、调拨单以及库

存台账查询等。调拨单对于多店的网上商城系统尤其重要,它会牵扯到多店库存以及账单

的查询,或者销售统计排行之类的功能。我们这里并没有做得那么复杂,有兴趣的读者可

以在这个基础之上继续完善它。

第 12 章 网上商城后台之数据字典实现

·267·

第 12 章 网上商城后台之数据字典实现

12.1 客户供应商管理的实现

所谓客户供应商管理,就是录入自己的客户或者供应商的过程。与采购管理、销售管

理和库存管理进行对比的话,客户供应商管理就是一个简单的单表录入,所以实现起来非

常简单,我们在这里仅列出实现界面和实现步骤。客户供应商的列表如图 12-1 所示,其管

理界面如图 12-2 所示。

图 12-1 客户供应商列表

第四部分 网上商城完整实现

·268·

图 12-2 客户供应商管理的界面

客户供应商管理 Flex 界面的实现在这里从略,客户供应商表结构的实现也从略。其序

列化到数据库为:

<url-mapping action="createcustsupp.do"

class="com.softbnd.jetblue.saas.scm.base.action.CreateCustsuppAction"/> 客户供应商管理,界面虽然简单,

但是存盘的操作却异常复杂。为什么

呢?因为我们做的是一个 B2B2C 的网

上商城平台,客户、供应商要和实体单

据一一对应起来。如果平台运营商的

客户、供应商同时登录到这个平台使

用,则一次简单的采购入库、采购出库、

销售入库、销售出库,都有可能是自己

客户或者供应商的销售出库或者采购

入库,需要三个信息流,就如同我们以

前看到的销售出库时那个信息流一样,

如图 12-3 所示。

所以当自己的客户、供应商管理存盘后,如果用户选择了自己的客户、供应商一起使

用本系统,我们还会在租户表那里注册一下,写上服务起止日期,是否采用共享数据库等

图 12-3 客户、B2B2C 平台和供应商之间的信息流

第 12 章 网上商城后台之数据字典实现

·269·

SaaS 服务核心数据信息,这里我就不一一列出来了。

12.2 商品维护的实现

商品维护也是我们整个网上商城系统的核心功能模块。任何数据,都是以商品为基础

信息的。有的商家,需求很简单,比如淘宝卖家,不需要相对复杂的进销存功能,只需要

录入商品信息,然后跟淘宝互动,查询库存信息就可以了。所以,这里我们也是支持用户

仅仅输入商品信息,就可以玩转我们的网上商城的,不需要使用进销存。

12.2.1 商品维护的界面 商品维护的用户界面如图 12-4 所示。

图 12-4 商品维护的用户界面

而对于商品维护的详细界面,还是非常复杂的,包括基本信息、规格、扩展属性、详

细介绍、搜索优化等很多功能,详细信息如图 12-5 所示。

1.基本信息

第四部分 网上商城完整实现

·270·

图 12-5 商品维护的详细界面

基本信息还包括各类价格的设置,如图 12-6 所示。

图 12-6 商品价格设置的维护

第 12 章 网上商城后台之数据字典实现

·271·

我们只要录入期初库存,就可以把商品数量录入到库存中,便可以同步到淘宝或者我

们自己的网上商城。如果我们不想使用进销存功能,只要在这里不断修改期初库存,也可

以实时保存库存的精确数量。

2.规格。针对服装行业之类的应用,一个系统支持自定义规格,还是非常必需的。

同样一个商品,可能有不同颜色、尺寸,价格相同,我们都是支持自定义的,即支持用户

任意定义自己的规格,而每个规格,还可以定义自己的价格。规格属性的界面如图 12-7

所示。

图 12-7 规格属性界面

至于规格的定义,我们在后面章节会专门介绍自定义规格的功能。

3.扩展属性:我们不仅支持自定义规格,还支持自定义扩展属性,如图 12-8 所示。

比如本例,颜色、尺寸、材料,这些都是可以自定义的。我们将在“货品类别”功能

那里,自定义这些扩展属性。

4.详细介绍:商品详细介绍的界面如图 12-9 所示。

第四部分 网上商城完整实现

·272·

图 12-8 自定义扩展属性界面

图 12-9 商品详细介绍的界面

第 12 章 网上商城后台之数据字典实现

·273·

5.其他还有“关联货品”、“SEO 优化”、“参数设置”等页面。这里就不一一列出来

了。不过,所有这些属性的设置,我们都是支持一起存盘的。主表就是商品定义表,还有

很多子表,比如:商品规格对应表、商品扩展属性对应表等。其 ActionScript 定义如下。

package {

import mx.controls.Alert;

import mx.formatters.DateFormatter;

Import mx.collections.ArrayCollection;

import flash.xml.XMLDocument;

import flash.xml.XMLNode;

Import mx.rpc.events.ResultEvent;

import org.bizsolution.jetblue.util.DateUtil;

import org.bizsolution.jetblue.ui.BaseComboBox;

import mx.core.ClassFactory;

[Bindable] //实体商品类

public class Good extends Object { //实体商品类的所有属性,基本上与数据库商品表保持一致

public var goodsno:String="";

public var goodssname:String="";

public var goodsname:String="";

public var csno:String="";

public var csname:String="";

public var goodstypeno:String="";

public var gtname:String="";

public var goodbrandno:String="";

public var goodsunitno:String="";

public var puname:String="";

public var modelspec:String="";

public var batchno:String="";

public var goodsbn:String="";

public var barcode:String="";

public var costprice:Number = 0.00;

public var taxrate:Number=5;

public var wholesaleprice:Number =0.00;

public var retailprice:Number = 0.00;

public var marketprice:Number = 0.00;

第四部分 网上商城完整实现

·274·

public var memberprice:Number = 0.00;

public var hotprice:Number = 0.00;

public var promotionalstartdate:Date;

public var v_promotionalstartdate:String;

public var promotionalenddate:Date;

public var v_promotionalenddate:String;

public var quantity:Number=0.00;

public var consumepoints:int=-1;

public var rankingpoints:int=-1;

public var purchaselimitpoints:int=0;

public var warehouseno:String="";

public var stockupperlimitquality:Number = 0.00;

public var stocklowerlimitquality:Number = 0.00;

public var status:String="";

public var remark:String="";

public var description:String="";

public var richdescription:String="";

public var issale:String="0";

public var salescale:int = 100;

public var weight:int=0;

public var wunit:String="";

public var originarea:String="";

public var promotion:String="0";

public var best:String="0";

public var auction:String="0";

public var groupbuy:String="0";

public var extproperty:String="0000000000";

public var imagepath_small:String="";

public var imagepath:String="";

public var row:String=""

public var uloadfiles:ArrayCollection;

public var goodspecreferences:ArrayCollection;

public var goodspeccols:Array;

public var goodsextproperties:ArrayCollection;

public var goodsextparameters:ArrayCollection;

public function Good() {

第 12 章 网上商城后台之数据字典实现

·275·

goodspeccols=new Array();

goodspecreferences=new ArrayCollection();

goodsextproperties=new ArrayCollection();

goodsextparameters=new ArrayCollection();

uloadfiles=new ArrayCollection();

} //把 Flex页面上的所有数据转化为 XML格式文件,传输到后台

public function toXML(good:Object):XML{

var df:DateFormatter=new DateFormatter();

df.formatString="YYYY-MM-DD";

var promotionalstartdate:String=df.format(good.promotionalstartdate);

var promotionalenddate:String=df.format(good.promotionalenddate);

var imagepath_small:String="";

var imagepath:String="";

if(uloadfiles.length>0){

imagepath= uloadfiles[0];

var pos1:int=imagepath.indexOf(".");

imagepath_small=imagepath.substring(0,pos1)+"_small."+

imagepath.substring(pos1+1) ;

}

var xml:XML =

<goods tablename="goods">

<goodsno key="goodsno" value={good.goodsno} type="String"

primaryKey="true" autogenerate="true"/>

<goodssname key="goodssname" value={good.goodssname} type=

"String"/>

<goodsname key="goodsname" value={good.goodsname} type=

"String"/>

<csno key="csno" value={good.csno} type="String"/>

<goodstypeno key="goodstypeno" value={good.goodstypeno} type=

"String"/>

<goodbrandno key="goodbrandno" value={good.goodbrandno} type=

"String"/>

<goodsunitno key="goodsunitno" value={good.goodsunitno} type=

"String"/>

<batchno key="batchno" value={good.batchno} type="String"/>

<goodsbn key="goodsbn" value={good.goodsbn} type="String"/>

第四部分 网上商城完整实现

·276·

<barcode key="barcode" value={good.barcode} type="String"/>

<costprice key="costprice" value={good.costprice} type=

"Number"/>

<taxrate key="taxrate" value={good.taxrate} type="Number"/>

<wholesaleprice key="wholesaleprice" value={good.

wholesaleprice} type="Number"/>

<retailprice key="retailprice" value={good.retailprice} type=

"Number"/>

<marketprice key="marketprice" value={good.marketprice} type=

"Number"/>

<memberprice key="memberprice" value={good.memberprice} type=

"Number"/>

<hotprice key="hotprice" value={good.hotprice} type=

"Number"/>

<promotionalstartdate key="promotionalstartdate" value=

{promotionalstartdate} type="Date"/>

<promotionalenddate key="promotionalenddate" value=

{promotionalenddate} type="Date"/>

<consumepoints key="consumepoints" value={good.consumepoints}

type="Integer"/>

<rankingpoints key="rankingpoints" value={good.rankingpoints}

type="Integer"/>

<purchaselimitpoints key="purchaselimitpoints" value={good.

purchaselimitpoints} type="Integer"/>

<warehouseno key="warehouseno" value={good.warehouseno} type=

"String"/>

<stockupperlimitquality key="stockupperlimitquality" value=

{good.stockupperlimitquality} type="Number"/>

<stocklowerlimitquality key="stocklowerlimitquality" value=

{good.stocklowerlimitquality} type="Number"/>

<quantity key="quantity" value={good.quantity} type=

"Number"/>

<status key="status" value={good.status} type="String"/>

<remark key="remark" value={good.remark} type="String"/>

<issale key="issale" value={good.issale} type="String"/>

<salescale key="salescale" value={good.salescale} type=

"Integer"/>

第 12 章 网上商城后台之数据字典实现

·277·

<weight key="weight" value={good.weight} type="Integer"/>

<wunit key="wunit" value={good.wunit} type="String"/>

<originarea key="originarea" value={good.originarea}

type="String"/>

<promotion key="promotion" value={good.promotion} type=

"String"/>

<best key="best" value={good.best} type="String"/>

<auction key="auction" value={good.auction} type="String"/>

<groupbuy key="groupbuy" value={good.groupbuy} type=

"String"/>

<extproperty key="extproperty" value={good.extproperty} type=

"String"/>

<imagepath_small key="imagepath_small" value={imagepath_

small} type="String"/>

<imagepath key="imagepath" value={imagepath} type="String"/>

</goods>; //商品表对应的上传图片

var doc:XMLDocument =new XMLDocument();

var childroot:XMLNode =doc.createElement("children");

childroot.attributes.tablename = "goodimages";

var hasuloadfile:Boolean=false;

for(var i:int = 0;i<uloadfiles.length;i++) {

var child:XMLNode =doc.createElement("goodimages");

imagepath= uloadfiles[i];

var pos:int=imagepath.indexOf(".");

imagepath_small=imagepath.substring(0,pos)+"_small."+imagepath.

substring(pos+1);

var goodimageidNode:XMLNode =doc.createElement("goodimageid");

goodimageidNode.attributes.type="Long";

//goodimageidNode.attributes.value=0;

goodimageidNode.attributes.key="goodimageid";

goodimageidNode.attributes.primaryKey="true";

goodimageidNode.attributes.autogenerate="true";

child.appendChild(goodimageidNode);

var imagepathNode:XMLNode =doc.createElement("imagepath");

第四部分 网上商城完整实现

·278·

imagepathNode.attributes.type="String";

imagepathNode.attributes.value=imagepath;

imagepathNode.attributes.key="imagepath";

child.appendChild(imagepathNode);

childroot.appendChild(child);

var imagepath_smallNode:XMLNode =doc.createElement("imagepath_

small");

imagepath_smallNode.attributes.type="String";

imagepath_smallNode.attributes.value=imagepath_small;

imagepath_smallNode.attributes.key="imagepath_small";

child.appendChild(imagepath_smallNode);

childroot.appendChild(child);

hasuloadfile=true;

}

if(hasuloadfile){

xml.appendChild(childroot);

} //商品对应的商品规格

childroot =doc.createElement("children");

childroot.attributes.tablename = "goodspecreference";

var hasgoodspecreference:Boolean=false;

for(var j:int = 0;j<goodspecreferences.length;j++) {

var child_goodspecreference:XMLNode =doc.createElement

("goodspecreference");

var goodspecrefeidNode:XMLNode =doc.createElement

("goodspecrefeid");

goodspecrefeidNode.attributes.type="Long";

//goodspecrefeidNode.attributes.value=0;

goodspecrefeidNode.attributes.key="goodspecrefeid";

goodspecrefeidNode.attributes.primaryKey="true";

goodspecrefeidNode.attributes.autogenerate="true";

child_goodspecreference.appendChild(goodspecrefeidNode);

var goodsbn:String=goodspecreferences[j]["goodsbn"];

var goodsbnNode:XMLNode =doc.createElement("goodsbn");

第 12 章 网上商城后台之数据字典实现

·279·

goodsbnNode.attributes.type="String";

goodsbnNode.attributes.value=goodsbn;

goodsbnNode.attributes.key="goodsbn";

child_goodspecreference.appendChild(goodsbnNode);

var goodspec:String=new String();

var specval:String=new String();

var specvalues:String=new String();

var warehouseno:String=new String();

warehouseno=good.warehouseno;

for(var k:int=6;k<goodspeccols.length;k++){

var spec:String=String(goodspeccols[k].headerText);

if(spec==null){

spec="";

}

goodspec=goodspec+spec+"#";

//var specval=goodspecreferences[j]["specval"];

var val:String=String(goodspecreferences[j][goodspeccols[k].

dataField]);

if(val==null){

val="";

}

specval=specval+val+"#";

specvalues=specvalues+"["+spec+":"+val+"]"+",";

}

if(goodspec.length>2){

goodspec=goodspec.substr(0,goodspec.length-1);

specval=specval.substr(0,specval.length-1);

specvalues=specvalues.substr(0,specvalues.length-1);

}

var goodspecNode:XMLNode =doc.createElement("goodspec");

goodspecNode.attributes.type="String";

goodspecNode.attributes.value=goodspec;

goodspecNode.attributes.key="goodspec";

child_goodspecreference.appendChild(goodspecNode);

var modelspecNode:XMLNode =doc.createElement("modelspec");

第四部分 网上商城完整实现

·280·

modelspecNode.attributes.type="String";

modelspecNode.attributes.value=goodspec;

modelspecNode.attributes.key="modelspec";

var specvalNode:XMLNode =doc.createElement("specval");

specvalNode.attributes.type="String";

specvalNode.attributes.value=specval;

specvalNode.attributes.key="specval";

child_goodspecreference.appendChild(specvalNode);

var specvaluesNode:XMLNode =doc.createElement("specvalues");

specvaluesNode.attributes.type="String";

specvaluesNode.attributes.value=specvalues;

specvaluesNode.attributes.key="specvalues";

child_goodspecreference.appendChild(specvaluesNode);

var warehousenoNode:XMLNode =doc.createElement("warehouseno");

warehousenoNode.attributes.type="String";

warehousenoNode.attributes.value=warehouseno;

warehousenoNode.attributes.key="warehouseno";

child_goodspecreference.appendChild(warehousenoNode);

var quantity:String=String(goodspecreferences[j]["quantity"]);

var quantityNode:XMLNode =doc.createElement("quantity");

quantityNode.attributes.type="Number";

quantityNode.attributes.value=quantity;

quantityNode.attributes.key="quantity";

child_goodspecreference.appendChild(quantityNode);

var imarketprice:String=String(goodspecreferences[j]

["imarketprice"]);

var imarketpriceNode:XMLNode =doc.createElement

("iimarketprice");

imarketpriceNode.attributes.type="Number";

imarketpriceNode.attributes.value=imarketprice;

imarketpriceNode.attributes.key="imarketprice";

child_goodspecreference.appendChild(imarketpriceNode);

第 12 章 网上商城后台之数据字典实现

·281·

var price:String=String(goodspecreferences[j]["price"]);

var priceNode:XMLNode =doc.createElement("price");

priceNode.attributes.type="Number";

priceNode.attributes.value=price;

priceNode.attributes.key="price";

child_goodspecreference.appendChild(priceNode);

var promotionprice:String=String(goodspecreferences[j]

["promotionprice"]);

var promotionpriceNode:XMLNode =doc.createElement

("promotionprice");

promotionpriceNode.attributes.type="Number";

promotionpriceNode.attributes.value=promotionprice;

promotionpriceNode.attributes.key="promotionprice";

child_goodspecreference.appendChild(promotionpriceNode);

hasgoodspecreference=true;

childroot.appendChild(child_goodspecreference);

}

if(hasgoodspecreference){

xml.appendChild(modelspecNode);

xml.appendChild(childroot);

} //商品表对应的扩展属性表

childroot =doc.createElement("children");

childroot.attributes.tablename = "goodsextpropertyvalues";

var hasgoodsextproperties:Boolean=false;

for(var m:int = 0;m<goodsextproperties.length;m++) {

var ext:Object=goodsextproperties[m];

var child_goodsextproperties:XMLNode =doc.createElement

("goodsextpropertyvalues");

var sidNode:XMLNode =doc.createElement("sid");

sidNode.attributes.type="Long";

//sidNode.attributes.value=0;

sidNode.attributes.key="sid";

第四部分 网上商城完整实现

·282·

sidNode.attributes.primaryKey="true";

sidNode.attributes.autogenerate="true";

child_goodsextproperties.appendChild(sidNode);

var goodsextpropertyid:String=ext.id;

var goodsextpropertyidNode:XMLNode =doc.createElement

("goodsextpropertyid");

goodsextpropertyidNode.attributes.type="Integer";

goodsextpropertyidNode.attributes.value=goodsextpropertyid;

goodsextpropertyidNode.attributes.key="goodsextpropertyid";

child_goodsextproperties.appendChild(goodsextpropertyidNode);

var propertyvalue:String=ext.value;

var propertyvalueNode:XMLNode =doc.createElement

("propertyvalue");

propertyvalueNode.attributes.type="String";

propertyvalueNode.attributes.value=propertyvalue;

propertyvalueNode.attributes.key="propertyvalue";

child_goodsextproperties.appendChild(propertyvalueNode);

hasgoodsextproperties=true;

childroot.appendChild(child_goodsextproperties);

}

if(hasgoodsextproperties){

xml.appendChild(childroot);

} //商品表对应的扩展属性对应的参数表

childroot =doc.createElement("children");

childroot.attributes.tablename = "goodextparametervalues";

var hasgoodsextparameters:Boolean=false;

for(var n:int = 0;n<goodsextparameters.length;n++) {

var ext_gp:Object=goodsextparameters[n];

var child_goodsextparameters:XMLNode =doc.createElement

("goodextparametervalues");

var gpsidNode:XMLNode =doc.createElement("sid");

gpsidNode.attributes.type="Long";

//sidNode.attributes.value=0;

第 12 章 网上商城后台之数据字典实现

·283·

gpsidNode.attributes.key="sid";

gpsidNode.attributes.primaryKey="true";

gpsidNode.attributes.autogenerate="true";

child_goodsextparameters.appendChild(gpsidNode);

var goodextparameterid:String=ext_gp.goodextparameterid;

var goodextparameteridNode:XMLNode =doc.createElement

("goodextparameterid");

goodextparameteridNode.attributes.type="Integer";

goodextparameteridNode.attributes.value=goodextparameterid;

goodextparameteridNode.attributes.key="goodextparameterid";

child_goodsextparameters.appendChild(goodextparameteridNode);

var goodextparagroupid:String=ext_gp.goodextparagroupid;

var goodextparagroupidNode:XMLNode =doc.createElement

("goodextparagroupid");

goodextparagroupidNode.attributes.type="Integer";

goodextparagroupidNode.attributes.value=goodextparagroupid;

goodextparagroupidNode.attributes.key="goodextparagroupid";

child_goodsextparameters.appendChild(goodextparagroupidNode);

var parametervalue:String=ext_gp.value;

var parametervalueNode:XMLNode =doc.createElement

("parametervalue");

parametervalueNode.attributes.type="String";

parametervalueNode.attributes.value=parametervalue;

parametervalueNode.attributes.key="parametervalue";

child_goodsextparameters.appendChild(parametervalueNode);

hasgoodsextparameters=true;

childroot.appendChild(child_goodsextparameters);

}

if(hasgoodsextparameters){

xml.appendChild(childroot);

} //商品表对应的商品详细介绍表

childroot =doc.createElement("children");

childroot.attributes.tablename = "gooddescription";

第四部分 网上商城完整实现

·284·

var child_gooddescription:XMLNode =doc.createElement

("gooddescription");

var descriptionNode:XMLNode =doc.createElement("description");

descriptionNode.attributes.type="String";

descriptionNode.attributes.key="description";

descriptionNode.attributes.value=good.description;

child_gooddescription.appendChild(descriptionNode);

var richdescriptionNode:XMLNode =doc.createElement

("richdescription");

richdescriptionNode.attributes.type="String";

richdescriptionNode.attributes.key="richdescription";

richdescriptionNode.attributes.value=good.richdescription;

child_gooddescription.appendChild(richdescriptionNode);

childroot.appendChild(child_gooddescription);

xml.appendChild(childroot);

return xml;

}

}

}

一张主表 goods(商品表)自带了 goodimages(商品和上传图片对应关系表)、

goodspecreference(商品和自定义规格对应表)、goodsextpropertyvalues(商品和扩展属性对

应关系表)、goodextparametervalues(商品和扩展参数对应关系表)和 gooddescription(商

品和详细介绍对应关系表)五张子表,并形成一个 XML 数据传送到后台。

12.2.2 商品操作的存盘 商品操作的存盘如下所示。

<url-mapping action="creategood.do"

class="com.softbnd.jetblue.saas.scm.base.action.CreateGoodAction"/>

对应的 Java 类是 CreateGoodAction。

第 12 章 网上商城后台之数据字典实现

·285·

package com.softbnd.jetblue.saas.scm.base.action;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.saas.scm.base.service.GoodService;

import com.softbnd.jetblue.saas.webframework.action.CreateAction;

public class CreateGoodAction extends CreateAction {

public CreateGoodAction(){

super();

}

@Override

protected TableObject insert(HttpServletRequest request,

HttpServletResponse response) {

TableObject vo = xml2Java(request, response);

String richdescription=vo.getAttribute("richdescription");

if(richdescription!=null&&!richdescription.equals("")){

richdescription.replaceAll("\"", "“");

}

GoodService goodService=new GoodService();

return goodService.createGood(vo, loginUser.getTenant());

}

}

我们使用缺省的“TableObject vo = xml2Java(request,response)”就可以把一张主表(商品

表)带五个子表(goodimages、goodspecreference、goodsextpropertyvalues、goodextparametervalues

和 gooddescription)的数据一起转化为 TableObject 对象,并且每个子表还对应多条数据,

后 GoodService 对象对 TableObject 序列化到数据库。

12.2.3 商品的数据库表结构定义 商品表 goods 如表 12-1 所示。

第四部分 网上商城完整实现

·286·

表 12-1 商品表 goods

字段名称 编码 字段类型 是否主键 是否允许 NULL

货品编码 goodsno varchar(20) Y(自增) N

货品名称 Goodsname varchar(100) N N

经销商/厂商编码 Csno varchar(20) N Y

货品类别编码 goodstypeno varchar(20) N N

计量单位编码 goodsunitno Varchar(20) N Y

型号规格 modelspec Varchar(50) N Y

批号 batchno Varchar(20) N Y

货号 goodsbn Varchar(20) N Y

进货单价 costprice Numeric(11,2) N Y

批发单价 wholesaleprice Numeric(11,2) N Y

市场价 marketprice Numeric(11,2) N Y

零售单价 retailprice Numeric(11,2) N N

促销价 hotprice Numeric(11,2) N Y

促销日期开始时间 promotionalstartdate Date N Y

促销日期结束时间 promotionalenddate Date N Y

赠送消费积分数 consumepoints Int N Y

默认库房 warehouseno Varchar(4) N Y

期初库存 quantity Int N Y

是否精品 best Char(1) N Y

是否特价 promotion Char(1) N Y

是否拍卖 auction Char(1) N Y

是否团购 groupbuy Char(1) N Y

租户编码 tenantno Varchar(22) N N

12.3 商品类别的实现

所谓商品类别就是对商品进行归类,比如:虚拟、服装、配饰、美容、数码、家居、

母婴、食品、文体、服务等,这是淘宝的分类,每个大类又可以自定义自己的子类。

第 12 章 网上商城后台之数据字典实现

·287·

商品分类也是本商城程序核心应用之一。因为自定义扩展商品属性、自定义扩展参数

都与商品分类密切相关。

12.3.1 商品分类的界面 商品分类界面如图 12-10 所示。

图 12-10 商品分类界面

1.商品分类本身的用户界面是一个树状结构。

2.选中一个商品分类,我们可以为这个分类设置商品扩展属性。

3.选中一个商品分类,我们可以为这个分类设置扩展参数,如图 12-11 所示。

4.为每个参数设置参数值,如图 12-12 所示。

设置好这些参数,我们就可以在商品的参数设置中动态地看到效果了,如图 12-13 所

示。

5.关联品牌:我们还可以为某个分类关联自己的品牌,如图 12-14 所示。

至此,我们就设置好了某个分类。当然,也不是每个分类都必须设置扩展属性、扩展

参数等。默认只定义一个商品分类就可以了。

第四部分 网上商城完整实现

·288·

图 12-11 为一个商品的分类设置扩展参数

图 12-12 为某个商品分类的扩展参数设置参数值

第 12 章 网上商城后台之数据字典实现

·289·

图 12-13 商品参数设置的动态效果

图 12-14 为某个分类关联自己的品牌

第四部分 网上商城完整实现

·290·

12.4 本章小结

本章主要讲解了网上商城系统的基础数据设置。商品管理和商品类别是我们的核心功

能,因为库房管理、计量单位、品牌管理、规格管理、商城信息发布等功能模块都是单表

设计、实现起来非常简单,就不一一列举了。

另外,对于进销存系统本身,我们还有系统管理功能,比如角色管理、用户管理、为

角色分类菜单、为用户分配角色、租户管理等功能,这些都与其他管理软件设置类似,也

就不一一列举了。

第 13 章 同步淘宝数据

·291·

第 13 章 同步淘宝数据

写本书的时候,不仅淘宝早已经开放了自己的 API,而且盛大、百度、新浪微博、腾

讯微博、甚至腾讯 QQ 也将要开放自己的 API。可以这么说,也许是受到苹果 APP Store 创

意的影响,今年是中国的开放平台年,各厂商在跑马圈地,自己处于垄断地位之后,都想

把开发者引入到自己的地盘上来。对于各家推出的开发平台计划,一致的评价是:开放比

垄断好,但也套用《中国计算机报》某记者的一句话:API——以开放为名的封锁运动。个

中玩味,值得读者思考。

由于淘宝事实上已经垄断了整个国内 C2C 市场,访问量已经超过亚马逊和 ebay,现在

全球排名第一。淘宝每天的独立访问量已经超过 4000 万,每天的 500 多万订单里,大约

400 万是实物交易(其余是 100 万左右的虚拟物品交易),这些订单每天都通过淘宝的几百

万卖家发送到全国各地(数据截止日期是 2010 年 6 月)。对于买家,淘宝的注册人数就已

经过亿。淘宝每天的交易额,可看淘宝网战略副总裁路鹏在微博上公开的数据:现在淘宝

网每天交易额 10 个亿左右,2010 年 10 月 30 号打破纪录,一天交易额突破 12 个亿人民币,

现在整个香港一天零售总额是 6 个多亿港币。也就是说,淘宝日交易量相当于 2 个香港的

日交易量。淘宝网交易额的发展速度是惊人的,去年的日交易额才几个亿,2010 年春节期

间,从大年三十到初四,淘宝网五天交易额超过 10 亿。而短短几个月之后,日交易额超

10 亿已经成为常态了。

由于淘宝的巨大流量,所以基于淘宝开放平台做开发,能给软件公司和程序员带来巨

大收益,因此吸引大量的程序员注意,也是情理之中的事情。事实上,到目前为止,已经

有很多家公司或者个人在淘宝 API 的基础上,做出了各类服务于淘宝大卖家的工具软件和

管理软件。

淘宝 API 本身并不复杂,很简单,稍微难的是淘宝的认证机制。淘宝 API 的 Session

第四部分 网上商城完整实现

·292·

认证机制,也几经修改,即使是程序员当中的老手,不折腾一阵子,也是很难调通淘宝的

Session 认证机制。

对于 Session 的认证机制,淘宝也有自己的例子和视频,不过,即使例子调试通过了,

由于淘宝还分测试环境和正式环境,想彻底搞明白,还是需要下一番工夫的。

13.1 了解淘宝开放平台

如果想使用淘宝 API,则必须在淘宝或者支付宝上注册一个合法的用户,估计现在每

个人都已经有了自己的淘宝或者支付宝账号,就不在这一步介绍了。

淘宝 API 的网址是:http://open.taobao.com/,在其右上方,有个申请成为开发者的按

钮,单击那个按钮,就可以按照步骤一步一步来就可以了。主界面如图 13-1 所示。

图 13-1 淘宝开放平台的主界面

当我们用自己的淘宝或者支付宝账户登录后,就可以看到作为淘宝 API 开发者的主界

面,如图 13-2 所示。

第 13 章 同步淘宝数据

·293·

图 13-2 淘宝 API 开发者主界面

其右上部分有一个“创建新应用”的按钮,单击那个按钮,就可以发布自己的应用程

序了。应用程序发布类型为:淘宝客、淘宝商家、淘宝箱、淘拍档四种类型,如图 13-3 所

示。

(1)淘宝客应用是指使用 TOP 所提供的接口创建的(仅供自己使用)淘客网站或淘客

程序,用于赚取淘客佣金,如果您想销售淘客程序请选择淘宝箱应用。

(2)淘宝商家应用是指淘宝商城商家和集市卖家使用 TOP 所提供的接口,创建自己的

商品、订单、发货等店铺管理工具。

(3)淘宝箱应用是指开发者使用 TOP 所提供的接口,开发用于在淘宝箱上销售的应

用。

(4)淘拍档应用是指淘宝合作伙伴为淘宝会员提供的 IT 类服务。

使用者类型还有一个自用或者他用。自用是指开发者基于 TOP 所开发的应用仅仅为自

己使用,而不销售或免费送给第三方使用,如淘宝商家自身技术团队或聘请外包技术团队

所开发的仅用于自己店铺进销存管理的应用、淘宝客所开发的用于推广的淘客网站等。他

用是指开发者所开发的有偿或无偿提供给第三方使用的应用,如店铺统计软件、店铺装修

软件、快递查询软件等。

第四部分 网上商城完整实现

·294·

图 13-3 淘宝网的应用程序发布界面

其中,对于“淘宝商家应用”,是有条件限制的,要么开发者自己是 B 类商家,即商

城商家;要么是 C 类三钻以上商家,即普通卖家。

淘宝的 API 也是分三级权限限制的,分为初级、中级和高级。三个级别,能够拥有的

API 权限是完全不一样的。大家可以从 http://dl.open.taobao.com/sdk/AP 权限文档.rar 这个

地址下载开发者权限文档,查看自己是否需要申请高级权限。

对于 API 的开发文档和 Demo 例子,我们也是需要看的,在我们从 http://open.taobao.com/

登录到开发者工作台后,还有个菜单“开发文档”,URL 链接地址如下:http://open.taobao.

com/dev/,单击后,就可以看到淘宝 API 的各类开发文档和视频资料等。

在这个 URL 链接上,分别对应有 Java、PHP 和.NET 以及 C/S 架构下的例子。淘宝对

每一个 App 应用程序的上线是有严格限制的,它并不接受 http://127.0.0.1 之类的本地地址

作为正式系统的上线回调地址,所以给我们的本地调试会带来很大的麻烦。例如:当我们

填入 http://127.0.0.1:8080/eshop/synctaobaoorder.do 之类的回调地址的时候,淘宝平台会返回

“您填写的 URL 无法正常访问,请重新输入!”这类的错误提示,如图 13-4 所示。

第 13 章 同步淘宝数据

·295·

图 13-4 淘宝网的错误提示

所以我们还只能使用淘宝自带的沙箱测试地址作为我们的 Demo 地址。当我们调试好

自己的程序后,必须申请一个国际域名,才能作为自己的程序正式上线使用,也才能使用

淘宝平台的正式工业数据。当然,也不是一定不能调试淘宝的正式数据。这里有个小技巧,

就是我们先申请一个国际域名,然后把这个国际域名的 IP 地址直接指向自己的拨号网络地

址,而拨号网络的地址,再通过 DMZ 的方式,指向本机地址就可以了。如果本机直接可

以拨号上网,那么把国际域名的 IP 地址直接指向本机,就更简单了。下一节我们将讲述如

何接入淘宝平台。

13.2 如何接入淘宝开放平台

在上一节我们对淘宝开发环境做了一个简单讲解。本节开始,将讲述如何让我们的代

码接入淘宝开放平台。由于本书所有的例子都是基于 Java 和 J2EE 的,所以对于接入淘宝

的代码,我们也都是基于 Java 和 J2EE 的,对于 PHP 和.NET 以及其他环境的开发者,请

参考淘宝自己的例子。

连接到淘宝平台的基础是必须先搞懂淘宝的 Session 认证机制。对于应用的认证和授

权,正式上线后,TOP 分为两个步骤。第一个步骤是淘宝对应用程序本身的授权,也就是

第四部分 网上商城完整实现

·296·

前文我们讲到的 Appkey,连接到淘宝,调用任何 TOP API,都需要那个 Appkey。Appkey

验证通过后,第二个步骤,还需要用户输入自己淘宝的用户名和密码,对这个应用授权,

如图 13-5 所示。

图 13-5 用户输入自己淘宝的用户名和密码

软件的使用者正确输入自己淘宝的用户名和密码后,还须对这个软件本身授权,授权

后,应用程序本身获得 TOP 认证后的 Session,然后通过这个 Session,应用才可以通过 TOP

API 去查询用户自己的淘宝数据,例如:淘宝上订单信息、库存信息等。如图 13-6 所示:

图 13-6 查询用户自己的淘宝数据

第 13 章 同步淘宝数据

·297·

淘宝的二次认证,其实类似于标准的 OpenID 认证,当我们开发类似于基于 MSN、Yahoo

或者 Gmail 认证机制的应用时,可以看到熟悉的类似的界面。

而对于测试环境的调试,TOP 认证步骤稍微不同,就是在调用 TOP API 去查询淘宝测

试数据之前,需要先获得一个授权码,如图 13-7 所示(以淘宝自带例子为例)。

图 13-7 单击获得授权码按钮

单击获得授权码按钮,测试环境会弹出让你选择测试的沙箱数据,以及回调地址,如

图 13-8 所示。

图 13-8 选择测试的沙箱数据

第四部分 网上商城完整实现

·298·

当我们随意选择一个测试环境账号(正式环境对应如图 13-8 所示的会员登录账号)和

应用回调 URL 后,就会获得一个授权码,如图 13-9 所示。

接着把这个授权码复制到刚才的页面,如图 13-10 所示。

图 13-9 获取淘宝 TOP 授权码

图 13-10 复制授权码并提交

然后再单击提交按钮,就可以出现显示淘宝测试数据的页面了。不过,有时候由于我

们选择的测试环境账号不同,而出现 404 错误的页面,如图 13-11 所示。

第 13 章 同步淘宝数据

·299·

图 13-11 404 错误页面

到底是由于申请时 Appkey 权限不同,还是其他原因造成 404 错误,这个真的不清楚。

我也问过淘宝 API 的 TOP 技术支持,他们也不能讲清楚这个问题。而淘宝的开放平台错误

自查手册上写道:如果用户收到的响应码是 404,表示用户的网络有问题或者 TOP 被屏蔽

了……。其实,可以肯定的是用户网络没有任何问题,TOP 也没有被屏蔽,为啥会返回 404

错误,的确不清楚。如果返回了 404 错误,我们换一个测试环境也许就好了。好在正式环

境,则没有遇到过此类错误。

下面,我们结合淘宝自带的例子,详细讲解一下测试环境下如何接入淘宝。

(1)编写 index.jsp,输入授权码,源代码如下。

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<%

String path = request.getContextPath();

String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.

getServerPort()+path+"/";

%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

<html>

<head>

<script type="text/javascript">

function doSubmit(){

if(getDataForm.authKey.value == ""){ alert("请先获取授权码");

return

}

window.document.location.href("http://container.api.tbsandbox.com/container?

authcode="+getDataForm.authKey.value);

}

第四部分 网上商城完整实现

·300·

function checkApp(){

window.open("http://open.taobao.com/isv/authorize.php?appkey="+getDataForm.

appkey.value+"&url=http://127.0.0.1:8080/eshop/synctaobaoorder.action")

}

</script>

<base href="<%=basePath%>">

<title>My JSP 'index.jsp' starting page</title>

<meta http-equiv="pragma" content="no-cache">

<meta http-equiv="cache-control" content="no-cache">

<meta http-equiv="expires" content="0">

<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">

<meta http-equiv="description" content="This is my page">

<!--

<link rel="stylesheet" type="text/css" href="styles.css">

-->

</head>

<body>

<form id="getDataForm" name="getDataForm" action="" method="get">

<table border="1" width="581" height="159">

<tr>

<td colspan="2">

<input type="button" name="getAuthKey" id="getAuthKey" value="获得授权码" onclick="checkApp();"/>

&nbsp;&nbsp;appkey<input type="text" size="8" id="appkey" name=

"appkey" value="12027243">

</td>

</tr>

<tr>

<td>

<textarea rows="5" cols="50" id="authKey" name="authKey">

</textarea>

</td>

<td> <input type="button" name="getData" id="getData" value="提交"

onclick="doSubmit();"/>

</td>

</tr>

第 13 章 同步淘宝数据

·301·

</table>

</form>

</body>

</html>

程序运行后,结果如图 13-12 所示。

图 13-12 输入授权码的程序运行结果

(2)单击“获取授权码”按钮,则转向获得 TOP 测试数据的 URL 地址,如图 13-13

所示。

图 13-13 获取 TOP 测试数据的 URL 地址

第四部分 网上商城完整实现

·302·

(3)选择 sandbox_c_25,回调 URL 写入 http://127.0.0.1:8080/eshop/synctaobaoorder.action,

单击“获取授权码”按钮。则淘宝会返回相应的授权码,如图 13-14 所示。

图 13-14 淘宝将返回的授权码

在正式环境中获得授权码的步骤,其实就相当于用户输入自己淘宝用户名和密码的步

骤。单击“复制授权码”按钮,复制授权码。然后返回到 index.jsp 原页面,把授权码复制

到 TextArea 框,如图 13-15 所示。

图 13-15 将授权码复制到 TextArea 框

第 13 章 同步淘宝数据

·303·

单击“提交”按钮,则 index.jsp 会跳转到

http://127.0.0.1:8080/eshop/synctaobaoorder.action。

(4)synctaobaoorder.action 定义如下。

<url-mapping action="synctaobaoorder.action" description="同步淘宝商品订单"

class="com.softbnd.jetblue.saas.eshop.taobao.action.SynchronTaobaoOrderActio

n">

</url-mapping>

(5)类 SynchronTaobaoOrderAction.java 源代码如下。

package com.softbnd.jetblue.saas.eshop.taobao.action;

import java.io.PrintWriter;

import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.softbnd.jetblue.saas.eshop.action.EShopAction;

import com.taobao.api.TaobaoJsonRestClient;

import com.taobao.api.TaobaoRestClient;

import com.taobao.api.json.JSONObject;

import com.taobao.api.model.Item;

import com.taobao.api.model.ItemGetRequest;

import com.taobao.api.model.ItemGetResponse;

import com.taobao.api.model.Trade;

import com.taobao.api.model.TradesGetResponse;

import com.taobao.api.model.TradesSoldGetRequest;

public class SynchronTaobaoOrderAction extends EShopAction {

public SynchronTaobaoOrderAction() {

super();

}

@Override

public void perform(HttpServletRequest req, HttpServletResponse resp) {

try {

String top_appkey = req.getParameter("top_appkey");

String top_parameters = req.getParameter("top_parameters");

第四部分 网上商城完整实现

·304·

String top_session = req.getParameter("top_session");

String top_sign = req.getParameter("top_signs");

String sign = req.getParameter("top_sign");

if (top_session == null) {

resp.sendRedirect("http://container.open.taobao.com/container?

appkey=" + appkey);

}else{

TaobaoRestClient client = new TaobaoJsonRestClient(testSandboxURL,

appkey, secret);

TradesSoldGetRequest tbReq = new TradesSoldGetRequest();

tbReq.setFields("buyer_nick,buyer_alipay_no,seller_nick,tid,iid,

created,status,title,payment,pay_time,orders.title");

tbReq.setPageSize(100);

TradesGetResponse tbRsp = client.tradesSoldGet(tbReq,

top_session);

if (!tbRsp.isSuccess()) {

if (tbRsp.isRedirect()) {

} else {

req.getRequestDispatcher("/index.jsp").forward(req,resp);

}

} else {

if (tbRsp.getTrades() != null) {

for (int i = 0; i < tbRsp.getTrades().size(); i++) {

Trade trade = tbRsp.getTrades().get(i);

String goodsid = trade.getIid();

ItemGetRequest itemGetRequest = new ItemGetRequest().

withIid(goodsid).withNick(trade.getSellerNick()).withFields("title,num,props");

ItemGetResponse itemGetResponse = client.itemGet

(itemGetRequest, top_session);

Item good = itemGetResponse.getItem();

EShopOrder vo = getSaleoutstockentry(trade,

good);

service.insertOrder(vo,loginUser.getTenant());

req.getRequestDispatcher("index.jsp").forward(req,

resp);

第 13 章 同步淘宝数据

·305·

}

} else {

req.getRequestDispatcher("index.jsp").forward(req, resp);

}

}

}

} catch (Exception e) {

e.printStackTrace();

}

}

}

当 TOP API 正确认证我们的应用后,它会返回一个 top_session 参数,这个 top_session

就是用户的 Session Key,我们只有拿到用户的 Session Key,才能继续调用淘宝 API 的参数,

取得库存信息、订单信息等数据。本例中,我们用这个 Session Key,调用 TOP API 的

TradesSoldGetRequest 类,获得用户的订单信息,然后把这个订单信息存到我们的进销存数

据库中的销售出库单表中,作为我们的一个销售出库单。当然,你也可以存到销售订单表

中,看你怎么看待这条数据的意义了,你如果是代销或者没有现货,还需要采购后才能发

货,就可以存到销售订单中,如果你有现货,可以立即发货,就可以存到销售出库单当中,

相应库存便会减少。

不熟悉 TOP API 的读者,建议去 http://my.open.taobao.com/ apidoc/index.htm 详细查阅

TOP 的 API 文档,搞清楚每个类是做什么用的。

我们在第 13.3 节,将讲述一些与进销存数据密切相关的几个 API。

13.3 同步淘宝数据到本地

在上一节,我们讲解了如何连接到淘宝查询淘宝的数据,在本节,我们将讲述与进销

存数据有关的 TOP API,把查询出来的数据,同步到本地数据库。

1.TaobaoRestClient 接口。

从上一节的例子看到,我们先引入了 TaobaoRestClient 接口,它是淘宝对外部提供的

第四部分 网上商城完整实现

·306·

REST API 客户端,连接任何 TOP 数据,第一步都是初始化 TaobaoRestClient。我们实例化

TaobaoRestClient 接口的实例是 TaobaoJsonRestClient,顾名思义,TaobaoJsonRestClient 返

回的数据格式是 JSON 格式。

如果我们不想返回 JSON 格式的数据,还可以选择 TaobaoXmlRestClient 类,它返回的

是 XML 格式数据。两者之间的类图关系如图 13-16 所示。

图 13-16 淘宝网各数据格式之间的关系

从图 13-16 中我们可以看到,TaobaoJsonRestClient 和 TaobaoXmlRestClient 均继承

TaobaoBaseRestClient 类,TaobaoBaseRestClient 实现了 TaobaoRestClient 接口。

2.TradesSoldGetRequest 类。

搜索当前会话用户作为卖家已卖出的交易数据。TradesSoldGetRequest 实现了抽象类

TaobaoRequest。所有与用户数据相关的 TOP API,都是继承 TaobaoRequest 的, 相应的类

图如图 13-17 所示。

我这里仅仅列出了少数几个 API 类,我粗略查了查,截止到现在,淘宝大约开放的

134 个相关的 API 都是继承自 TaobaoRequest 类。因为 TaobaoRequest 负责所有 API 调用

的监控,包括流量控制、终端用户 ip,isv 传入,用户监控和统计等,其流量规则如表 13-1

所示。

第 13 章 同步淘宝数据

·307·

图 13-17 淘宝网各相应类图的关系

表 13-1 淘宝网流量规则

状态 总量(单个接口) 频次(单个 APP) 限制对象

测试环境开发 高值与频率限制相关 ≤400 次/分钟 所有

正式环境测试 ≤5000 次/天 ≤400 次/分钟 所有

高值与频率限制相关 ≤500 次/分钟 淘宝客类应用 上线后

高值与频率限制相关 ≤20000 次/分钟 卖家工具类应用

你无论干了什么,淘宝都是有监控的。

3.TradesGetResponse 类。

把 TradesSoldGetRequest 和 top_session 作为参数,传递给 TradesGetResponse,则

TradesGetResponse 就把淘宝上的数据返回到了客户端。其实大家从名字也可以猜测出,

TradesSoldGetRequest、TradesGetResponse 与 Java Servlet 的 HttpRequest 和 HttpResponse 是

何其的相似,HttpRequest 从客户端取数据/查询参数到服务器,HttpResponse 把服务器的数

据返回到客户端。

TradesGetResponse 有一个方法:getTrades(),此函数返回此用户所有的订单信息,其

类图如图 13-18 所示。

第四部分 网上商城完整实现

·308·

图 13-18 TradesGetResponse 类的类图

每个订单信息就是一个 Trade 类,Trade 类主要是商品订单的主表信息,比如买家留言、

付款时间、买家获得积分、买家使用积分、淘宝物流订单号、买家付款金额、确认收货时

间、某笔交易中卖家实际收到支付宝的打款、货到付款服务费、发票抬头等信息,这里也

仅仅列出了一部分。对于一个订单上包含的商品信息,则 Trade 类还有个属性 orders,我们

可以通过 getOrders()得到此订单上的所有的商品。下面简单地列出部分属性的含义:

(1)iid:商品字符串编号;

(2)numIid:商品数字编号;

(3)skuId:商品 小属性单元;

(4)skuPropertiesName:SKU 的值。如机身颜色——黑色;

(5)itemMealName:套餐的值。如 M8 原装电池、便携支架、M8 专用座充、莫凡保

护袋;

(6)num:购买数量;

(7)title:商品标题;

第 13 章 同步淘宝数据

·309·

(8)price:商品价格;

(9)picPath:商品图片路径;

(10)tid:淘宝交易号,即副订单在淘宝业务订单中的 bizOrderId;

(11)buyerNick:买家昵称;

(12)created:创建时间;

(13)totalFee:应付金额;

(14)payment:实付金额;

(15)discountFee:系统优惠金额;

(16)status:订单状态。

4.ItemGetRequest 和 ItemGetResponse 类,这两个类也是一对。

通过 TradesGetResponse 我们可以得到所有的订单,每个订单上又存储了商品编码,如

果我们想查询到商品名称、规格、秒杀商品类型、是否在外部网店显示等基于商品的详细

信息,还必须使用 ItemGetRequest 和 ItemGetResponse 这两个类,ItemGetRequest 是调用

taobao.items.get()搜索商品时需要传入的参数,上个步骤所获得商品编码作为实例化

ItemGetRequest()的参数,ItemGetResponse 是调用 taobao.items.get()函数搜索商品时返回的

Response。

5.得到订单的详细信息后,我们就可以把数据同步到本地数据库了。这里,我们调

用一个 TaoBaoService 类,把上步所取得订单数据同步到我们的进销存数据库当中,其序列

图如图 13-19 所示。

图 13-19 将订单数据同步到进销存数据库中

第四部分 网上商城完整实现

·310·

TaoBaoService.java 源代码如下。

package com.softbnd.jetblue.saas.eshop.taobao.service;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.base.dao.transaction.TransactionFactory;

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.base.dao.business.domain.Tenant;

import com.softbnd.jetblue.saas.eshop.model.EShopOrder;

import com.softbnd.jetblue.saas.eshop.service.EShopService;

import com.softbnd.jetblue.saas.eshop.taobao.dao.TaoBaoDAO;

public class TaoBaoService extends EShopService{ public static String WAIT_BUYER_PAY="WAIT_BUYER_PAY";//等待买家付款

public static String WAIT_BUYER_CONFIRM_GOODS="WAIT_BUYER_CONFIRM_GOODS"; //卖家已发货

public static String WAIT_SELLER_SEND_GOODS="WAIT_SELLER_SEND_GOODS";//买

家已付款

public static String TRADE_FINISHED="TRADE_FINISHED";//交易成功

public static String TRADE_CLOSED="TRADE_CLOSED";//交易关闭

public static String CREATE_CLOSED_OF_TAOBAO="CREATE_CLOSED_OF_TAOBAO"; //创建关闭

public static String OTHER="OTHER";//其他状态

public TaoBaoService(){

super();

}

public TableObject insertOrder(EShopOrder vo,Tenant tenant){

TableObject ret=null;

UTransaction transaction =TransactionFactory.getInstance(tenant);

transaction.begin();

try{ // 判断商品是否存在,不存在自动新增商品基本信息

TaoBaoDAO dao=new TaoBaoDAO();

String goodsno=dao.insertGood(vo.getGood(),transaction);

String saleoutstockentryno=vo.getAttribute("saleoutstockentryno");

for(TableObject child:vo.getChildren()){

child.setAttribute("goodsno", goodsno);

第 13 章 同步淘宝数据

·311·

}

// 网店销售单同步,判断销售单是否存在,不存在从网店新增销售出库单到本系统

ret=dao.insertOrder(vo,transaction);

//减库存

String deCompileTradeStatus=getDecompileTradeStatus(vo.

getAttribute("eshopsaleorderstatus"));

if((saleoutstockentryno==null||saleoutstockentryno.equals(""))

&&deCompileTradeStatus.equals(TRADE_FINISHED)){

}

transaction.commit();

}catch(Exception e){

e.printStackTrace();

transaction.rollback();

}

return ret;

}

public String getTradeStatus(String status){

String ret="";

if(status==null){ ret="其他状态";

}else if(status.equals(WAIT_BUYER_PAY)){ ret="等待买家付款";

}else if(status.equals(WAIT_BUYER_CONFIRM_GOODS)){ ret="卖家已发货";

}else if(status.equals(WAIT_SELLER_SEND_GOODS)){ ret="买家已付款";

}else if(status.equals(TRADE_FINISHED)){ ret="交易成功";

}else if(status.equals(TRADE_CLOSED)){ ret="交易关闭";

}else if(status.equals(CREATE_CLOSED_OF_TAOBAO)){ ret="创建关闭";

}else if(status.equals(OTHER)){

第四部分 网上商城完整实现

·312·

ret="其他状态";

}else { ret="其他状态";

}

return ret;

}

public String getDecompileTradeStatus(String status){

String ret="";

if(status==null){

ret=OTHER; }else if(status.equals("等待买家付款")){

ret=WAIT_BUYER_PAY; }else if(status.equals("卖家已发货")){

ret=WAIT_BUYER_CONFIRM_GOODS; }else if(status.equals("买家已付款")){

ret=WAIT_SELLER_SEND_GOODS; }else if(status.equals("交易成功")){

ret=TRADE_FINISHED; }else if(status.equals("交易关闭")){

ret=TRADE_CLOSED; }else if(status.equals("创建关闭")){

ret=CREATE_CLOSED_OF_TAOBAO; }else if(status.equals("其他状态")){

ret=OTHER;

}else {

ret=OTHER;

}

return ret;

}

}

对应的 TaoBaoDAO 类源代码如下。

package com.softbnd.jetblue.saas.eshop.taobao.dao;

import java.SQL.Connection;

import com.softbnd.jetblue.base.dao.DAOFactory;

import com.softbnd.jetblue.base.dao.IDAO;

import com.softbnd.jetblue.base.dao.doman.TableObject;

第 13 章 同步淘宝数据

·313·

import com.softbnd.jetblue.base.dao.transaction.UTransaction;

import com.softbnd.jetblue.saas.eshop.model.Good;

public class TaoBaoDAO {

public TaoBaoDAO() {

}

/** * 网店销售单同步,判断销售单是否存在,不存在则从网店新增销售出库单到本系统

* @param vo

* @param transaction

* @return

* @throws Exception

*/

public TableObject insertOrder(TableObject vo, UTransaction transaction)

throws Exception{

TableObject ret = null;

IDAO dao = DAOFactory.getInstance();

try {

Connection selfconn=transaction.getConnection();

String tradestatus=vo.getAttribute("eshopsaleorderstatus");

ret = (TableObject)dao.iifinsert(vo, transaction,"saleorderno",

vo.getAttribute("saleorderno"),selfconn);

if(!ret.getAttribute("eshopsaleorderstatus").equals

(tradestatus)){

vo.setAttribute("eshopsaleorderstatus", tradestatus);

dao.update(vo, transaction);

}

selfconn.close();

} catch (Exception e) {

e.printStackTrace();

throw new Exception(e);

}

return ret;

}

/**

第四部分 网上商城完整实现

·314·

* 网店销售单同步,判断客户是否存在,不存在则自动新增客户基本信息

* @param vo

* @param transaction

* @return

* @throws Exception

*/

public TableObject insertCustomer(TableObject vo, UTransaction

transaction) throws Exception{

TableObject ret = null;

IDAO dao = DAOFactory.getInstance();

try {

ret = dao.insert(vo, transaction);

} catch (Exception e) {

e.printStackTrace();

throw new Exception(e);

}

return ret;

}

/** * 网店销售单同步,判断商品是否存在,不存在则自动新增商品基本信息

* @param vo

* @param transaction

* @return

* @throws Exception

*/

public String insertGood(Good good, UTransaction transaction) throws

Exception{

String ret = null;

IDAO dao = DAOFactory.getInstance();

try {

Connection selfconn=transaction.getConnection();

ret = ((TableObject)dao.iifinsert(good, transaction, "goodsname",

good.getAttribute("goodsname"),selfconn)).getAttribute("goodsno");

selfconn.close();

} catch (Exception e) {

e.printStackTrace();

第 13 章 同步淘宝数据

·315·

throw new Exception(e);

}

return ret;

}

}

13.4 同步本地数据到淘宝

在第 13.3 节,我们讲述了同步淘宝数据到本地,淘宝同样给我们提供了同步本地数据

到淘宝的 API。本节将重点讲述如何把本地数据同步到淘宝上。

13.4.1 发布新产品 发布新产品的步骤如下。

(1)获取类目 ID;

(2)根据类目 ID 调用 taobao.itemprops.get()获取类目属性 pid,用 taobao.itempropvalues.

get()取得 vid,或通过属性工具获得相应的属性串;

(3)必须上传产品图片。

相关源代码如下。

ProductAddRequest request = new ProductAddRequest(); request.setCid("50008165"); // 类目 ID

request.setOuterid("200612"); //商家编码

request.setProps("20000:30812;1632501:31578;21861:3683581"); //关键属性

request.setBinds("1637400:4606395;21862:31578"); //非关键属性

request.setCustomerProps("21861:自定义属性值"); //自定义属性

request.setSaleProps("1627207:28324"); //销售属性

request.setName("山寨 E71"); //产品名称

request.setImage(new File("D://test.jpg")); //产品图片

request.setPrice("100"); //产品价钱

request.setNum("10"); //产品数量

request.setDesc("测试添加产品"); //产品描述

ProductAddResponse response = client.productAdd(request, sessionId); System.out.println(response.getBody()); //输出响应的信息

第四部分 网上商城完整实现

·316·

而且,我们还可以利用 taobao.product.img.upload()上传产品图片。

13.4.2 发布新商品 对于发布新商品,我们则利用不同的 API。

ItemAddRequest req = new ItemAddRequest(); req.setNum(20); //商品数量

req.setPrice("5000"); //商品价格

req.setType("fixed"); //发布类型

req.setStuffStatus("new"); //新旧程度

req.setTitle("男式真皮鞋(黑色)"); //宝贝标题

req.setDesc("top add item test description"); //宝贝描述

Location location = new Location(); location.setState("浙江"); //所在地省份,如浙江,具体可以用 taobao.areas.get()取到

location.setCity("杭州"); //所在地城市,如杭州,具体可以用 taobao.areas.get()取到

req.setLocation(location); req.setApproveStatus("onsale"); //商品上传后的状态

req.setCid("1101"); //类目 ID

req.setProps("20879:21456;20000:21660;1627207:3232483;1627207:3232484;21

315:40042;31185:99198;31356:100692;"+

"20100:21373;20143:45566;31359:134701;22572:37142;20121:21507;22623:4728

6;20122:685;20137:10024;20145:21391;"+

"20183:21968;31357:100697;31696:107066;1626817:3227607;1626975:3229214"); //商品属性列表

req.setValidThru(14);//有效期,7天或者 14天

req.setHasInvoice(true); //是否有发票,可选值 true/false (商城卖家此字段必须为

true) req.setHasWarranty(true);//是否有保修,可选值 true/false

req.setAutoRepost(true); //自动上架,可选值 true/false

req.setHasShowcase(true); //商家发布的所有商品都是橱窗推荐

req.setSellerCids(",105714340,106033750,"); //店铺类目列表

req.setHasDiscount(true); //支持会员打折,可选值 true/false

req.setFreightPayer("buyer"); //运费承担方式

req.setPostageid("1906682"); //运费模板 ID

req.setPostFee("5"); //平邮费用,如 5.00元

第 13 章 同步淘宝数据

·317·

req.setExpressFee("10"); //快递费用,如 10.00元

req.setEmsFee("20"); //ems费用,如 20.00元

req.setListTime(DateUtil.strToDate("2009-01-11 23:00:00")); //定时上架时间,如 2008-05-26 09:12:00

request.setImage(new File("f:\\a1.jpg")); //商品主图片

//商品的积分返点比例,如:5 表示返点比例 5%。注意返点比例必须是大于 0的整数,而且最大是 90

req.setAuctionPoint("5"); req.setOuterid("123456"); //商家编码

req.setProductid("13820935"); //产品 ID

req.setSkuProperties("1627207:3232483,1627207:3232484,"); //SKU的属性串(销

售属性串)

req.setSkuQuantities("10,10,"); //SKU的数量串(SKU的总和,等于商品数量)

req.setSkuPrices("5000,5000,"); //SKU的价格串(宝贝价格,不能低于销售属性最低价,

也不能高于最高价)

req.setSkuOuterIds("654321,654321,"); //SKU的外部 id串,结构如:1234,1342,……

ItemAddResponse rsp = client.itemAdd(req, sessionId);

淘宝的产品与商品的区别:商家只能通过产品发布宝贝,发布商品前必须先发布产品,

也就是说,商品要归属到某个产品目录下。

13.4.3 更新已有商品数据 在第 13.3 节我们利用了一对 TradeGetRequest 和 TradeGetResponse 实现了同步淘宝

数据,而对于已有订单商品数据的修改,同样有 TradeOrderSkuUpdateRequest 更改订单

的 SKU 属 性 请 求 和 TradeOrderSkuUpdateResponse 更 改 订 单 的 SKU 属 性 响 应 ;

TradePriceUpdateRequest 和 TradePriceUpdateResponse 修改订单商品价格等。

对于已有商品,我们还可以利用 ItemUpdateRequest 和 ItemUpdateResponse 修改已有

上架商品信息。

public void testItemUpdate() throws TaobaoApiException{

ItemUpdateRequest request = new ItemUpdateRequest();

request.setIid("4d39d7588e43df96156da437b4716cd3");

request.setNum(1);

ItemUpdateResponse response=tip_jsonclient.itemUpdate(request, tip_

sessionID);

System.out.println(response.getBody());

第四部分 网上商城完整实现

·318·

}

13.5 查询自己感兴趣的其他淘宝数据

在本章第 13.3 节和第 13.4 节,我们分别实现了把淘宝上的数据同步到本地数据库,

以及把本地数据同步到淘宝,本节将继续讲述如何利用淘宝 API 查询自己感兴趣的数据,

供开发类似淘宝客程序的程序员参考。

1.taobao.shop.get(获取卖家店铺的基本信息)

在淘宝网站上所带的 API 例子上,我们还可以看到获取卖家店铺的基本信息的例子,

其源代码如下。

(1)以 JAVA SDK 为例。

TaobaoRestClient client = new

TaobaoJsonRestClient("http://gw.api.tbsandbox.com/router/rest", "test",

"test");

ShopGetRequest req = new ShopGetRequest();

req.setFields("sid,cid,title,nick,desc,bulletin,pic_path,created,modified"); req.setNick("卖家昵称");

ShopGetResponse rsp= client.shopGet(req);

System.out.println(rsp.getBody());

(2)返回数据类型为 JSON 的示例。

{

"rsp":{

"shops":[

{ "bulletin":"1111是",

"cid":"1041",

"desc":"",

"modified":"2009-11-18 15:33:38",

"nick":"hz0799",

"pic_path":"/a9/30/T1aXdXXbtbXXartXjX.gif",

"sid":"33759380",

第 13 章 同步淘宝数据

·319·

"title":"淘宝测试店铺,千万不要拍"

}

]

}

}

(3)返回类型为 XML 的示例。

<rsp>

<shop>

<sid>33759380</sid>

<cid>1041</cid> <title>淘宝测试店铺,千万不要拍</title>

<nick>hz0799</nick> <bulletin><p>1111是</p></bulletin>

<pic_path>/a9/30/T1aXdXXbtbXXartXjX.gif</pic_path>

<modified>2009-11-18 15:33:38</modified>

</shop>

</rsp>

2.与淘宝客店铺推广相关的 API

taobao.taobaoke.items.get: 淘宝客商品查询;

taobao.taobaoke.items.convert: 淘宝客商品转换;

taobao.taobaoke.shops.convert: 淘宝客店铺转换;

taobao.taobaoke.listurl.get: 淘宝客关键字搜索 URL;

taobao.taobaoke.report.get: 淘宝客报表查询。

第一步,获得店铺卖家昵称。通过接口 taobao.taobaoke.items.get (查询淘宝客推广商

品)获得店铺卖家昵称。如想要获得“裙子”的相关淘宝客推广店铺链接;

第二步,获得店铺 ID。根据店铺卖家昵称获得店铺 id,调用接口 taobao.shop.get(获

取卖家店铺基本信息接口);

第三步,获得推广店铺链接。根据店铺 ID,获得该店铺的所有商品的淘宝可推广链接。

调用接口 taobao.taobaoke.shops.convert(淘宝客店铺转换接口)。

其他例如与淘宝客单品推广相关 API、与淘宝客多品推广相关 API、社区推广相关 API

步骤与此也是类似的。

第四部分 网上商城完整实现

·320·

3.卖家工具相关 API。在本章第 13.3 节“同步淘宝数据到本地”,利用了一些与卖家

工具相关的 API,即订单管理,其他还有查询库存管理相关数据、批量上传商品 API 等其

他功能。

关于查询相关数据的详细 API,请参阅淘宝开放平台的 WiKi(http://wiki.open.

taobao.com/)。

13.6 本章小结

本章主要总结了淘宝相关的一些实现,包括同步淘宝数据到本地,上传数据到淘宝等。

淘宝开放的 API,截止到目前,已经有上百个了,而且淘宝也在逐步开放 SNS 淘江湖以及

买家数据 API,2011 年,淘宝估计会开放自己的数百个 API,光是讲解这些 API,就会是

厚达数百页的一本书,本章只是管中窥豹,讲解了基于淘宝 API 开发的一些 基本的步骤,

我也是把淘宝的数据,作为自己进销存销售订单数据来看待的,淘宝相当于自己的一个

具有人气的销售窗口或者柜台。读者如果想深入研究淘宝 API 的开发, 好的学习资料就

是淘宝自己的 WiKi 网站:http://wiki.open.taobao.com/。

第 14 章 网上商城前台实现技术

·321·

第 14 章 网上商城前台实现技术

从第 9 章到第 12 章,我们一直讲了网上商城后台管理功能的实现,用的是 Flex 加自

定义的 SSH 框架。从本章开始,我们将讨论前台模板技术的实现,并详细讲解如何与我们

先前定义的 SSH 框架相结合。

对于 PHP 开发者来说,采用模板技术是一个再简单不过的事情,PHP 模板引擎主要

分为两种:一种是替换特定字串型的。美工做出来的页面,中间会嵌入一些什么{store.title}

这样的字符串,然后程序读入这个模板文件,将中间的{store.title}的字样替换成实际从

数据库中读取的内容。还有一种复杂一些,是一种编译型的。以 smarty 为代表,模板

文件中实际上包含了一些简化的 PHP 代码,比如 常用的是“<{if $counts>0 }>”这样

的语句。

但对于 Java 程序员来说,就没有那么幸运了。常见的如 Apache 的 Velocity,不过,模

板技术其实还是非常复杂的,先前选择的轻量级的解决方案 Jdynamite 有两个致命的缺陷,

一是不支持 include 指令;二是不支持<for><if> 等包含简单逻辑判断的语句。对于不支持

include 指令的缺陷,笔者写了一段代码,使得 Jdynamite 支持 include 指令了,而对于第二

个致命缺陷,笔者还没有找到非常完美的解决办法。因此现在的网上商城模板技术,还只

是一个半成品。现在就把这个半成品模板技术写出来,与大家共享一下。所以说,架构师

的选型至关重要,当初就是嫌弃 Velocity 太厚重,加上以前没用过模板技术,就想当然了。

对于第二个缺陷,解决的办法是很原始的,就是在 Java 代码里做判断,只把结果输出到前

台。不过,这样做的弊端就是 Java 代码里面,不可避免地夹杂着 HTML 网页代码,并不利

于 CSS 样式表的修改。下一个版本,我打算把模板技术,重新改回到 Velocity 上。

第四部分 网上商城完整实现

·322·

14.1 模板技术具体实现

如图 14-1 所示,是所有相关类的类图。

图 14-1 模板技术中的类图

14.1.1 IParseTemplateAction 接口讲解 接口 IParseTemplateAction 有 4 种方法。

(1)getHtml(LoginUser loginUser,BaseService service,Connection conn,HttpServletRequest

request,HttpServletResponse response)throws Exception; 这种方法,主要是解析 HTML 方

法,返回解析后的 HTML 代码;

(2)getIncludeVariables()方法,主要是把解析后的公共变量放到缓存里面,这样再被

别的模块调用时就不需要再解析;

(3)getParent(),主要是应用于 include 的时候得到父类,然后再利用父类,调用

getIncludeVariables()方法,得到公共的变量;

接口 IParseTemplateAction 有两个实现类,分别是类 DefaultParseTemplateAction 和类

ParseTemplateAction。为什么会有两个完全不同的实现呢?DefaultParseTemplateAction 主要

用于单个静态 HTML 页面的解析。比如 ParsePageHeaderAction 类,主要用于解析每个网页

第 14 章 网上商城前台实现技术

·323·

都有的 head.html,比如显示购物车商品数量的网页,如图 14-2 所示。

图 14-2 显示购物车商品数量的网页

对应的 HTML 代码如下。

<div class="nav">

<div class="nav1"></div>

<div class="nav2"></div> <a href="showcart.action" class="buy">购物车 <strong

id="cart_goods_kinds">{amount}</strong> 种商品</a>

<a href="myfavorite.action" class="buyline">收藏夹</a>

<a href="myorder.action" class="buyline">我的订单</a>

</div>

代码中的“{amount}”就是动态解析出来的。

而类 ParseTemplateAction,主要用于解析类似于 index.html 这类包含“include 其他页

面”的模板。例如我们商城的首页,index.html 源代码如下。

<!-- include file="header.html" -->

<div class="keyword">

<div class="keyword1"></div>

<div class="keyword2"></div> 热门搜索:

{hotsearch}

</div>

<div class="content">

<div class="left" area="top_left" widget_type="area">

<!-- include file="notice.html" -->

<!-- include file="register_and_apply.html" -->

第四部分 网上商城完整实现

·324·

<!-- include file="brands.html" -->

</div>

......

<div class="clear"></div>

<div class="content" area="bottom_down" widget_type="area">

<!-- include file="link.html" -->

</div>

<!-- include file="footer.html" -->

index.html 包含了很多其他 html 页面,前面我们讲到,Jdynamite 并不支持 include 指

令,下面的章节将讲解是如何解决这个问题的。

14.1.2 DefaultParseTemplateAction 类讲解 上面讲了,DefaultParseTemplateAction 主要用于单个静态 HTML 页面的解析,其源代

码如下。

public abstract class DefaultParseTemplateAction implements

IParseTemplateAction {

private JDynamiTe dynamiTe;

private ITemplateAction parent;

private static HashMap<String, JDynamiTe> includes = new HashMap<String,

JDynamiTe>();

private HashMap<String,String> variables;

protected String filename;

public DefaultParseTemplateAction(String view) {

this.filename=view;

variables=new HashMap<String,String>();

}

public JDynamiTe getJDynamiTe(String filename, HttpServletRequest request,

HttpServletResponse response) throws Exception {

dynamiTe = new JDynamiTe();

URL url = new URL(filename);

InputStream is = null;

第 14 章 网上商城前台实现技术

·325·

BufferedReader in = null;

request.setCharacterEncoding("UTF-8");

response.setCharacterEncoding("UTF-8");

is = url.openStream();

in = new BufferedReader(new InputStreamReader(is, "UTF-8"));

dynamiTe.setInput(in);

in.close();

is.close();

includes.put(filename, dynamiTe);

return dynamiTe;

}

@Override

public String getIncludeVariables(String key) {

if(variables==null){

variables=new HashMap<String,String>();

}

return variables.get(key);

}

@Override

public ITemplateAction getParent() {

return parent;

}

@Override

public void setParent(ITemplateAction parentaction) {

parent=parentaction;

}

public HashMap<String, String> getVariables() {

if(variables==null){

variables=new HashMap<String,String>();

}

return variables;

}

@Override

public String getHtml(LoginUser loginUser, BaseService service, Connection

conn, HttpServletRequest request, HttpServletResponse response) throws

Exception {

JDynamiTe dynamiTe=getJDynamiTe(filename,request,response);

第四部分 网上商城完整实现

·326·

dynamiTe.parse();

return dynamiTe.toString();

}

}

这个类的核心实现就是 getHtml()方法。getJDynamiTe()方法读入指定的 URL 静态文件

到内存,getHtml()方法则负责解析各个变量。由于 DefaultParseTemplateAction 是基类,所

以它没有任何具体的变量替换。

14.1.3 ParseTemplateAction 类讲解 类 ParseTemplateAction 主要用于解析类似 index.html,这类包含“include 其他页面”

的模板。Jdynamite 并不支持 include 指令,所以我们的 ParseTemplateAction 就必须实现

include,其源代码如下。

public abstract class ParseTemplateAction extends AbstractAction implements

IParseTemplateAction {

private JDynamiTe dynamiTe;

protected String filename;

protected JetMappings urlMapping = null;

private String html = "";

public ParseTemplateAction() {

super();

}

protected abstract IParseTemplateAction getParseTemplateAction(String

filename);

@Override

public void perform(HttpServletRequest request, HttpServletResponse

response) {

urlMapping = webUtil.getJetMappings(request, urlMappings);

String view = urlMapping.getView();

loginUser = (LoginUser) request.getSession().getAttribute("loginUser");

UTransaction transaction = TransactionFactory.getInstance(); // transaction.begin(); //开启事务,做增加、修改、删除的时候用

第 14 章 网上商城前台实现技术

·327·

Connection conn = transaction.getConnection();// 查 询 的 时 候 用 ,共 享

Connection

try {

String filename = "";

if (urlMapping.getTemplatepath() == null || urlMapping.

getTemplatepath().equals("")) {

filename = context.getResource("/themes/").toString() + view;

} else {

filename = context.getResource(urlMapping.getTemplatepath()).

toString() + view;

}

IParseTemplateAction parsetemplateAction = getParseTemplateAction

(filename);

html = parsetemplateAction.gethtml(loginUser, service, conn, request,

response);

html = replaceinclue(html, request, response, conn);

response.setCharacterEncoding("UTF-8");

PrintWriter pw = response.getWriter();

pw.print(html);

pw.close();

} catch (Exception e) {

e.printStackTrace();

} finally {

try {

conn.close();

} catch (SQLException e) {

e.printStackTrace();

}

}

}

protected String replaceinclue(String html1, HttpServletRequest request,

HttpServletResponse response, Connection conn) {

int start = 0;

int end = 0;

String html = html1;

try {

第四部分 网上商城完整实现

·328·

String path = "";

if (urlMapping.getTemplatepath() == null || urlMapping.

getTemplatepath().equals("")) {

path = context.getResource("/themes/").toString();

} else {

path = context.getResource(urlMapping.getTemplatepath()).

toString();

}

while (start != -1) {

start = html.indexOf("<!-- include file=");

if (start >= 0) {

end = html.indexOf("-->", start);

String dynamichtml = "";

String fullname = path + html.substring(start + 19, end - 2);

IParseTemplateAction parsetemplateAction=getParseTemplateAction

(fullname);

if (parsetemplateAction != null) {

dynamichtml = parsetemplateAction.gethtml(loginUser,

service, conn, request, response);

html1 = html1.replace(html.substring(start, end + 3),

dynamichtml);

}

html = html.substring(end);

}

}

} catch (Exception e) {

e.printStackTrace();

}

return html1;

}

public String getIncludeVariables(String key) {

return null;

}

public ITemplateAction getParent() {

return null;

第 14 章 网上商城前台实现技术

·329·

}

public void setParent(ITemplateAction parent) {

}

protected JDynamiTe getJDynamiTe(String filename, HttpServletRequest

request, HttpServletResponse response) throws Exception {

dynamiTe = new JDynamiTe();

URL url = new URL(filename);

InputStream is = null;

BufferedReader in = null;

request.setCharacterEncoding("UTF-8");

response.setCharacterEncoding("UTF-8");

is = url.openStream();

in = new BufferedReader(new InputStreamReader(is, "UTF-8"));

dynamiTe.setInput(in);

in.close();

is.close();

return dynamiTe;

}

public String getHtml() {

return html;

}

public void setHtml(String html) {

this.html = html;

}

}

本书中是如何实现解析 include 指令的呢?原理很简单,就是通过循环,对那个

index.html 文件全文解析,找到“include”标签后,然后再找到 include 后的 html 文件,接

着再通过这个 html 文件名,再次找到对应实际解析这个 html 文件的 Java Action 类,利用

getHtml()方法再次动态解析替换。利用下面的这句循环,反复解析替换。

while (start != -1) {

start = html.indexOf("<!-- include file=");

第四部分 网上商城完整实现

·330·

至于 ParseFooterAction、PurchaseIndexTemplateAction、ShowStoreAction 等众多实现解析

各类模板的 Action 类,我就不一一举例子了。它们就是在分别继承类 DefaultParseTemplateAction

和类 ParseTemplateAction 的基础之上,分别满足解析头文件(head.html)、左边导航栏

(left.html)、导航菜单(topmenu.html)等一系列 html 模板文件的解析。

在 Jdynamite 的基础上,通过 DefaultParseTemplateAction 和 ParseTemplateAction 两个

类,我们就完成了静态 HTML 模板文件的解析。

14.2 实现用户注册

任何一个网上商城系统,都必须有一个用户注册

功能。网上商城买家注册用户和使用后台进销存功能

的商家客户,注册信息还是很不一样的。注册本身功

能比较简单。比较有意思的是,用户在注册时候,如

何给用户一些更友好的提示,比如,注册用户名已经

存在,密码太简单之类的。这种提示,不能经过页面

刷新,把用户辛辛苦苦填的注册信息全部清除。现在

我们一般采用 Ajax 技术来实现了。我们的注册界面如

图 14-3 所示。

其注册的源代码如下。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0

Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>51taohuo.net 我要淘货网-->注册窗口</title>

<link rel="shortcut icon" href="favicon.ico" />

<link rel="icon" href="animated_favicon.gif" type="image/gif" />

<link href="css/style.css" rel="stylesheet" type="text/css" />

<script language="JavaScript" src="js/utils.js"> </script>

<script language="JavaScript" src="js/user.js"> </script>

图 14-3 注册界面

第 14 章 网上商城前台实现技术

·331·

<script src="js/jquery-1.4.2.min.js" language="javascript"></script>

<body>

<div class="usBox">

<div class="usBox_2 clearfix">

<div class="regtitle"></div>

<form action="zhuce.action" method="post" name="formUser" onsubmit=

"return validuser2();">

<table width="100%" border="0" align="left" cellpadding="5"

cellspacing="3">

<tr> <td width="13%" align="right">用户名</td>

<td width="87%">

<input name="usrno" type="text" size="25" id="usrno" onblur=

"is_registered(this.value,'');" class="inputBg"/>

<span id="usrno_notice" style="color:#FF0000"> *</span>

<input name="partnerno" type="hidden" value=" "/>

</td>

</tr>

<tr>

<td align="right">Email</td>

<td>

<input name="email" type="text" size="25" id="email"

onblur="checkEmail(this.value);" class="inputBg"/>

<span id="email_notice" style="color:#FF0000"> *</span>

</td>

</tr>

<tr> <td align="right">密码</td>

<td>

<input name="usrpwd" type="password" id="password1" onblur="check_

password(this.value);" onkeyup="checkIntensity(this.value)" class="inputBg"

style="width:179px;" />

<span style="color:#FF0000" id="passwd_notice"> *</span>

</td>

</tr>

<tr> <td align="right">密码强度</td>

第四部分 网上商城完整实现

·332·

<td>

<table width="145" border="0" cellspacing="0" cellpadding="1">

<tr align="center"> <td width="33%" id="pwd_lower">弱</td>

<td width="33%" id="pwd_middle">中</td>

<td width="33%" id="pwd_high">强</td>

</tr>

</table>

</td>

</tr>

<tr> <td align="right">确认密码</td>

<td>

<input name="confirm_password" type="password" id="conform_

password" onblur="check_conform_password(this.value);" class="inputBg" style=

"width:179px;"/>

<span style="color:#FF0000" id="conform_password_notice">

*</span>

</td>

</tr>

<tr> <td align="right">验证码</td>

<td>

<input name="validcode" type="text" id="validcode"class="

inputBg" style="width:179px;" />

<span style="color:#FF0000"> *<img border=0 src="randomImage">

</span>

</td>

</tr>

<tr>

<td>&nbsp;</td>

<td><label>

<input name="agreement" type="checkbox" value="1" checked=

"checked" /> 我已看过并接受<a href="">《用户协议》</a></label></td>

</tr>

<tr>

第 14 章 网上商城前台实现技术

·333·

<td>&nbsp;</td>

<td align="left">

<input name="Submit" type="submit" value="" class="us_Submit_reg">

</td>

</tr>

<tr>

<td colspan="2">&nbsp;</td>

</tr>

<tr>

<td>&nbsp;</td>

<td class="actionSub"> <a href="http://www.51taohuo.net/login.action">我已有账号,我要登录

</a><br /> <a href="findpassword.htm">您忘记密码了吗?</a>

</td>

</tr>

</table>

<input name="partnerno" type="hidden" value=" "/>

</form>

</div>

</div>

</body>

</html>

对于用户名和邮箱是否已存在的判断,我们是在控件 input 框的 onblur 事件里来调用

is_registered() 的 JavaScript 的 实 现 。 onblur 是 控 件 在 失 去 焦 点 的 时 候 触 发 的 事 件 。

is_registered()方法就是一段 JavaScript 应用了。其源代码如下。

function is_registered( usrno,partnerno )

{

var submit_disabled = false;

var unlen = usrno.replace(/[^\x00-\xff]/g, "**").length;

if ( usrno == '' )

{ document.getElementById('usrno_notice').innerHTML = "用户名不允许为空";

submit_disabled = true;

第四部分 网上商城完整实现

·334·

}

if ( !chkstr( usrno ) )

{ document.getElementById('usrno_notice').innerHTML="用户名输入不合法";

submit_disabled = true;

}

if ( unlen < 4 )

{ document.getElementById('usrno_notice').innerHTML = "用户名至少为 4

个字符";

submit_disabled = true;

}

if ( unlen > 10 )

{ document.getElementById('usrno_notice').innerHTML = "用户名不能超过

10位";

submit_disabled = true;

}

if ( submit_disabled )

{

document.forms['formUser'].elements['Submit'].disabled = 'disabled';

return false;

}

$.ajax({type:"post",

url: "checkusr.action?usrno="+ usrno+"&partnerno="+partnerno,

success:registed_callback

});

这段 JavaScript 代码来源于 Ecmall 所附带的 JavaScript,我做了部分更改,Ecmall 采用

的是 PHP 技术,调用的 Ajax 方式也稍微不用,我采用的是 JQuery 框架,所以 Ajax 调用格

式是

$.ajax({type:"post",

url: "checkusr.action?usrno="+ usrno+"&partnerno="+partnerno,

success:registed_callback

}

第 14 章 网上商城前台实现技术

·335·

这里,我们用 checkusr.action 去服务器端做用户名的验证,而 checkusr.action 则又用

到了咱们在后台进销存章节所讲到的自定义的 SSH 框架。checkusr.action 定义如下。

<url-mapping action="checkusr.action" description="验证用户名是否存在"

class="com.softbnd.jetblue.common.action.CheckUniqueUserAction">

</url-mapping>

而 CheckUniqueUserAction.java 源代码如下。

package com.softbnd.jetblue.common.action;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.saas.webframework.action.JSONQueryAction;

public class CheckUniqueUserAction extends JSONQueryAction{

public CheckUniqueUserAction(){

super();

}

public void perform(HttpServletRequest request, HttpServletResponse

response) {

String usrno=request.getParameter("usrno");

String partnerno=request.getParameter("partnerno");

if(partnerno!=null){

partnerno=partnerno+"_";

}else{

partnerno="";

}

usrno=partnerno+usrno;

String SQL="select * from tuser where usrno='"+usrno+"'";

TableObject vo=service.find(SQL);

if(vo==null){

SQL="select * from tenant where usrno='"+usrno+"'";

TableObject vo2=service.find(SQL);

if(vo2==null){

super.print(response, "ok");

}else{

super.print(response, "no");

第四部分 网上商城完整实现

·336·

}

}else{

super.print(response, "no");

}

}

}

14.3 实现商品展示

我们在后台通过进销存录入了商品信息,必然要在前台展示出来,图 14-4 为网上商城

首页页面。

图 14-4 网上商城首页页面

首页上有精品推荐、特价商品、 新商品以及商品分类等。我们要通过模板技术,把

第 14 章 网上商城前台实现技术

·337·

这些商品都显示出来。

Index.html 完整的源代码如下。

<!-- include file="header.html" -->

<div class="keyword">

<div class="keyword1"></div>

<div class="keyword2"></div> 热门搜索:

{hotsearch}

</div>

<div class="content">

<div class="left" area="top_left" widget_type="area">

<!-- include file="notice.html" -->

<!-- include file="register_and_apply.html" -->

<!-- include file="brands.html" -->

</div>

<div class="right">

<div class="main">

<div id="module_middle" area="cycle_image" widget_type="area">

<!-- include file="ad_cycle.html" -->

</div>

<div class="sidebar" area="sales" widget_type="area">

<!-- include file="sale_price.html" -->

</div>

</div>

<div area="top_right" widget_type="area">

<!-- include file="best_item.html" -->

</div>

</div>

</div>

<div class="clear"></div>

<div class="ad_banner" area="banner" widget_type="area">

第四部分 网上商城完整实现

·338·

<!--<div id="_widget_505" name="image_ad" widget_type="widget" class=

"widget">

<div class="ad_image">

<a href="" target="_blank"><img src="mall/data/files/mall/template/

200908070205218467.gif" /></a>

</div>

</div> -->

</div>

<div class="content">

<div class="left" area="bottom_left" widget_type="area">

<!-- include file="recommended_store.html"

<div id="_widget_930" name="image_ad" widget_type="widget" class=

"widget">

<div class="ad_image">

<a href="" target="_blank"><img src="mall/data/files/mall/

template/200908070208384344.gif" /></a>

</div>

</div>-->

<!-- include file="latest_sold.html" -->

<!--<div id="_widget_598" name="image_ad" widget_type="widget"

class="widget">

<div class="ad_image">

<a href="" target="_blank"><img src="mall/data/files/mall/

template/200908070208252680.gif" /></a>

</div>

</div> -->

<!-- include file="saletop10.html" -->

</div>

<div class="right" widget_type="area" area="bottom_right">

<!-- include file="module_recommend.html" -->

<!-- include file="module_recommend2.html" -->

<!-- include file="category_list.html" -->

</div>

</div>

第 14 章 网上商城前台实现技术

·339·

<div class="clear"></div>

<div class="content" area="bottom_down" widget_type="area">

<!-- include file="link.html" -->

</div>

<!-- include file="footer.html" -->

其中,<!-- include file="header.html" -->对应首页头部的模板文件,一般放 Logo 等信

息。<!-- include file="notice.html" -->是网上商城公告的位置。<!-- include file="ad_cycle.html"

-->则是对应首页轮转图片。 <!-- include file="sale_price.html" -->,列出来的是特价商品。

<!-- include file="best_item.html" -->列出来的是精品推荐。<!-- include file="latest_sold.html"

-->列出来的则是 新上架商品。而<!-- include file="footer.html" -->对应首页尾部的模板文

件。其 Java 源代码如下。

package com.softbnd.jetblue.saas.eshop.ecmall.action;

import java.SQL.Connection;

import java.util.HashMap;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import cb.jdynamite.JDynamiTe;

import com.softbnd.jetblue.saas.base.dao.business.domain.LoginUser;

import com.softbnd.jetblue.saas.base.service.BaseService;

import com.softbnd.jetblue.util.html.template.action.IParseTemplateAction;

import com.softbnd.jetblue.util.html.template.action.ITemplateAction;

import com.softbnd.jetblue.util.html.template.action.ParseTemplateAction;

public class ParseIndexTemplateAction extends ParseTemplateAction implements

IParseTemplateAction,ITemplateAction {

private HashMap<String,IParseTemplateAction> includefiles=new HashMap

<String,IParseTemplateAction>();

public ParseIndexTemplateAction() {

super();

}

第四部分 网上商城完整实现

·340·

@Override

protected IParseTemplateAction getParseTemplateAction(String view) {

IParseTemplateAction action=null;

this.filename=view;

view=view.substring(view.lastIndexOf("/")+1);

if(view.equals("index.html")){

action= this;

}else if(view.equals("header.html")){

action= new ParsePageHeaderAction(filename);

}else if(view.equals("footer.html")){

action= new ParseFooterAction(filename);

}else if(view.equals("notice.html")){

action= new ParseNoticeAction(filename);

}else if(view.equals("register_and_apply.html")){

action= new ParseRegisterApplyAction(filename);

}else if(view.equals("brands.html")){

action= new ParseBrandsAction(filename);

}else if(view.equals("ad_cycle.html")){

action= new ParseADCycleAction(filename);

}else if(view.equals("link.html")){

action= new ParseADCycleAction(filename);

}else if(view.equals("recommended_store.html")){

action= new ParseRecommendedStoreAction(filename);

}else if(view.equals("saletop10.html")){

action= new ParseTop10Action(filename);

}else if(view.equals("latest_sold.html")){

action= new ParseLatestSoldAction(filename);

}else if(view.equals("module_recommend.html")){

action= new ParseModuleRecommendAction(filename);

}else if(view.equals("module_recommend2.html")){

action= new ParseModuleRecommend2Action(filename);

}else if(view.equals("category_list.html")){

action= new ParseCategoryListAction(filename);

}else if(view.equals("best_item.html")){

action= new ParseBestItemAction(filename);

}else if(view.equals("sale_price.html")){

第 14 章 网上商城前台实现技术

·341·

action= new ParseSalePriceAction(filename);

}

if(action!=null){

action.setParent(this);

includefiles.put(view, action);

}

return action;

}

@Override

public String gethtml(LoginUser loginUser, BaseService service, Connection

conn, HttpServletRequest request, HttpServletResponse response) throws

Exception {

JDynamiTe dynamiTe=getJDynamiTe(filename,request,response); String hotsearch="<a href='search.action?keyword=外贸女装 '>外贸女装

</a>&nbsp;&nbsp;<a href='search.action?keyword=外贸童装'>外贸童装</a>&nbsp;

&nbsp;"; hotsearch=hotsearch+"<a href='search.action?keyword=阿迪达斯'>阿迪达斯

</a>&nbsp;&nbsp;<a href='search.action?keyword=外贸男装'>外贸男装</a>";

dynamiTe.setVariable("hotsearch",hotsearch);

dynamiTe.parse();

return dynamiTe.toString();

}

@Override

public IParseTemplateAction getAction(String filename) {

IParseTemplateAction action=includefiles.get(filename);

return action;

}

@Override

public ITemplateAction getParent() {

return this;

}

}

Index.html 自身还有个标签“热门搜索:{hotsearch}”,我们就在 getHtml()方法里设置如

下。

第四部分 网上商城完整实现

·342·

String hotsearch="<a href='search.action?keyword=外贸女装'>外贸女装</a>

&nbsp;&nbsp;<a href='search.action?keyword=外贸童装'>外贸童装</a>&nbsp;&nbsp;";

hotsearch=hotsearch+"<a href='search.action?keyword=阿迪达斯'>阿迪达斯</a>

&nbsp;&nbsp;<a href='search.action?keyword=外贸男装'>外贸男装</a>";

dynamiTe.setVariable("hotsearch",hotsearch);

其中,dynamiTe.setVariable("hotsearch",hotsearch)就是用来设置{hotsearch}标签的,

当 Jdynamite 遇到{hotsearch}时,就把它解析为外贸女装、阿迪达斯等热门词汇。

我们再举个<!-- include file="sale_price.html" -->的例子,完成列出特价商品的功能,其

sale_price.html 源代码如下。

<div id="_widget_279" name="sale_price" widget_type="widget" class=

"widget">

<div class="module_common"> <h2><b class="sales" title="SALES超特价"></b></h2>

<div class="wrap">

<div class="wrap_child">

<ul class="list_pic">{saleprice1}</ul>

<ul class="list_text">

{saleprice2}

</ul>

</div>

</div>

</div>

</div>

这里有两个标签{saleprice1}和{saleprice2},我们对应的 Java 源代码如下。

package com.softbnd.jetblue.saas.eshop.ecmall.action;

import java.SQL.Connection;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import cb.jdynamite.JDynamiTe;

import com.softbnd.jetblue.base.dao.doman.TableObject;

import com.softbnd.jetblue.saas.base.dao.business.domain.LoginUser;

import com.softbnd.jetblue.saas.base.service.BaseService;

import

第 14 章 网上商城前台实现技术

·343·

com.softbnd.jetblue.util.html.template.action.DefaultParseTemplateAction;

public class ParseSalePriceAction extends DefaultParseTemplateAction{

public ParseSalePriceAction(String view) {

super(view);

}

@Override

public String gethtml(LoginUser loginUser, BaseService service, Connection

conn, HttpServletRequest request, HttpServletResponse response) throws

Exception {

JDynamiTe dynamiTe = getJDynamiTe(filename, request, response);

String saleprice1 = "";

String SQL="select goods.retailprice, goods.goodsno, goods.imagepath,

goods.goodsname,goods.tenantno "

+"from goods,storageledger where promotion='1' and status=0 "

+ "and storageledger.realquantity>0 and goods.goodsbn=storageledger.

goodsbn and goods.goodsno=storageledger.goodsno "

+" order by lstupddate desc limit 0,7";

TableObject[] salesVos = service.query(SQL,conn);

int num=salesVos.length;

if(num>3){

num=3;

}

for(int i=0; i<num; i++) {

TableObject salesVo = salesVos[i];

saleprice1 +="<li><p><a href='showgood.action?goodsno=" +salesVo.

getAttribute("goodsno")

+"&tenantno="+salesVo.getAttribute("tenantno")

+ "' target='_blank'>"

+"<img src='jxc/"+salesVo.getAttribute("imagepath")+"'

width='70' "

+"height='70' /></a></p><h3><a href='showgood.action?

goodsno=" +salesVo.getAttribute("goodsno")

+"&tenantno="+salesVo.getAttribute("tenantno")

+ "' target='_blank' "

+"title='"+salesVo.getAttribute("goodsname")+"'>"+salesVo.

getAttribute("goodsname")+"</a><span>&yen;"+salesVo.getAttribute("retailpric

e")+"</span></h3></li>";

第四部分 网上商城完整实现

·344·

}

dynamiTe.setVariable("saleprice1", saleprice1);

String saleprice2 = "";

for(int i=num; i<salesVos.length; i++) {

TableObject salesVo = salesVos[i];

saleprice2 +="<li><a href='showgood.action?goodsno=" +salesVo.g

etAttribute("goodsno")

+"&tenantno="+salesVo.getAttribute("tenantno")

+ "' target='_blank' "

+"title='"+salesVo.getAttribute("goodsname")+"'>"+salesVo.

getAttribute("goodsname")+"</a>"

+"<span>&yen;"+salesVo.getAttribute("retailprice")+"</span></li>";

}

dynamiTe.setVariable("saleprice2", saleprice2);

dynamiTe.parse();

return dynamiTe.toString();

}

}

14.4 实现购物车

购物车在网上商城系统中是一个很核心的功能。一般购物车上的临时数据,要么放在

Session 里面,要么放在 Cookie 里面,也有直接存放到数据库里面的。这里我们就先只放

在 Session 里面,先不考虑浏览器不允许 Cookie 的问题。

购物车一般包括以下几个功能。

(1)加入购物车,把浏览过的商品,添加到购物车当中,如图 14-5 所示。

这里,对于商品的展示,我们有个技巧:当我们鼠标靠近商品的图片时,就会在右面

自动显示出一幅大图,如图 14-6 所示。

第 14 章 网上商城前台实现技术

·345·

图 14-5 加入购物车

图 14-6 鼠标靠近某商品时,将自动显示该商品的大图

第四部分 网上商城完整实现

·346·

当然,这幅图片不是足够大,我们其实可以做一幅非常大的图片,然后利用 JavaScript

技巧,动态放大一个商品的局部细节,使得用户体验更加友好。

这里我们利用的是 JQuery 的局部图片放大技巧。

(2)把商品分享到其他网站

对于网上商城来说,能够把自己的商品分享到其他网站,无疑是推广自己的有力武器。

好在现在很多网站,都开放了自己的 API,允许网友与自己互动。比如开心网、人人网,

以及新浪微博等,如图 14-7 所示。

图 14-7 分享商品到其他网站

实现这部分功能的 JavaScript 代码如下。

<li id="btn_list2" title="分享该商品">

<ul class="drop_down">

<li>

<img src="mall/data/system/kaixin001.gif" />

<a target="_blank" href="http://www.kaixin001.com/

repaste/share.php?rtitle={}&rurl=http%3A%2F%2Fwww.sqmaoyi%2Fmall%2Findex.php%3Fapp%3Dgoods%26id%3D28">开心网</a>

</li>

<li>

<img src="mall/data/system/renren.gif" />

<a target="_blank" href="http://share.renren.com/

share/buttonshare.do?link=http%3A%2F%2Fwww.sqmaoyi%2Fmall%2Findex.php%3Fapp%

第 14 章 网上商城前台实现技术

·347·

3Dgoods%26id%3D28&title={goods.goods_name}">人人网</a>

</li>

<li>

<img src="http://t.sina.com.cn/favicon.ico"/>

<a href="javascript:void((function(s,d,e,r,l,p,t,z,c)

{var%20f='http://v.t.sina.com.cn/share/share.php?appkey=’ ',u=z||d.location,

p=['&url=',e(u),'&title=',e(t||d.title),'&appkey=2924220432','&source=',e(r)

,'&sourceUrl=',e(l),'&content=',c||'UTF-8','&pic=',e(p||'')].join('');functi

on%20a(){if(!window.open([f,p].join(''),'mb',['toolbar=0,status=0,resizable=

1,width=440,height=430,left=',(s.width-440)/2,',top=',(s.height-430)/2].join

('')))u.href=[f,p].join('');};if(/Firefox/.test(navigator.userAgent))setTimeout(a,0);else%20a();})(screen,document,encodeURIComponent,'','','图片链接|默认

为空','{goods.goods_name}','','页面编码 UTF-8'));">新浪微博</a>

</li>

</ul>

</li>

例如:我们选择分享到新浪微博,则新浪会自动弹出如图 14-8 所示的窗口。

图 14-8 新浪自动弹出的窗口

第四部分 网上商城完整实现

·348·

我们单击转发,则会把这段商品推广代码,自动转发到自己的微博上。

(3)修改、删除、继续添加购物车商品信息功能,如图 14-9 所示。

图 14-9 修改购物车商品的信息

(4)第三步,结算、选择配送地址和付款等,如图 14-10 所示。

图 14-10 网上结算

这里,我们选择 简单的 Session 来实现购物车。我们先看看“加入购物车”的按钮

第 14 章 网上商城前台实现技术

·349·

功能的实现。

<ul class="ware_btn"> <li class="btn_c2" title="添加到购物车"><a href="javascript:buy();"></a>

</li> <li class="btn_c3" title="添加到收藏夹">

<a href="javascript:collect_goods({$goods.goods_id});"></a></li>

</ul>

添加到购物车后面对应着 JavaScript buy()方法如下。

function buy()

{

if (goodsspec.getSpec() == null)

{ alert("请选择规格");

return;

}

var storageledgerid = goodsspec.getSpec().id;

var quantity = $("#quantity").val();

var goodstenantno= $("#goodstenantno").val();

if (quantity == '')

{ alert("请输入购买数量");

return;

}

if(!isNumber(quantity)){ alert("购买数量输入不正确");

return;

}else {

if (parseInt(quantity) < 1){ alert("购买数量输入不正确");

return;

}

}

add_to_cart(storageledgerid,quantity);

第四部分 网上商城完整实现

·350·

}

/* add cart */

function add_to_cart(storageledgerid,quantity)

{

formbuy.storageledgerid.value=storageledgerid;

formbuy.goodquantity.value=quantity;

formbuy.action="showcart.action";

formbuy.submit();

}

Buy()方法又调用了 add_to_cart()方法,而 add_to_cart(0 方法则通过 Form 提交的方式

提交到服务器端,调用相应的 Java 代码。

showcart.action 的定义如下。

<url-mapping action="showcart.action" description=" 购 物 车 "

view="cart.index.html" templatepath="/mall/themes/mall/default/"

class="com.softbnd.jetblue.saas.eshop.ecmall.cart.action.

ShowCartAction">

</url-mapping>

ShowCartAction.java 源代码对应如下。

public class ShowCartAction extends ParseTemplateAction implements

IparseTemplateAction,ITemplateAction{

private HashMap<String,IParseTemplateAction> includefiles=new HashMap

<String,IParseTemplateAction>();

@Override

protected IParseTemplateAction getParseTemplateAction(String view) {

IParseTemplateAction action=null;

this.filename=view;

view=view.substring(view.lastIndexOf("/")+1);

if(view.equals("cart.index.html")){

action= this;

}else if(view.equals("header.html")){

action= new ParsePageHeaderAction(filename);

}else if(view.equals("footer.html")){

第 14 章 网上商城前台实现技术

·351·

action= new ParseFooterAction(filename);

}

if(action!=null){

action.setParent(this);

includefiles.put(view, action);

}

return action;

}

@Override

public String gethtml(LoginUser loginUser, BaseService service,

Connection conn, HttpServletRequest request, HttpServletResponse response)

throws Exception {

String goodsno=request.getParameter("goodsno");

String tenantno=request.getParameter("goodstenantno");

TableObject tshoppingcart=(TableObject) request.getSession().

getAttribute("goodcar");

JDynamiTe dynamiTe=getJDynamiTe(filename,request,response);

String storageledgerid=request.getParameter("storageledgerid");

String tenantname="";

if(goodsno!=null&&tenantno!=null){

String goodsname=request.getParameter("goodsname");

String quantity=request.getParameter("goodquantity");

String price=request.getParameter("price");

String goods_image=request.getParameter("goods_image");

String goodsunitname=request.getParameter("goodsunitname");

if(goodsunitname==null){

goodsunitname="";

}

String SQL="select tenantname from tenant where tenantno='"+

tenantno+"'";

TableObject tenant=service.find(SQL,conn);

if(tenant!=null){

tenantname=tenant.getAttribute("tenantname");

}

SQL="select goodsbn,goodspec from storageledger where

storageledgerid="+storageledgerid+" and tenantno='"+tenantno+"'";

第四部分 网上商城完整实现

·352·

TableObject vo=service.find(SQL,conn);

String goodsbn=vo.getAttribute("goodsbn");

String specvalues=vo.getAttribute("goodspec");

if(specvalues==null){

specvalues="";

}

String totalmoney=String.valueOf(Double.parseDouble(price)

*Double.parseDouble(quantity));

if(tshoppingcart==null){

tshoppingcart=new TableObject();

TableObject ashoppingcart=getnewShoppingcart(tenantno,

tenantname, quantity, goodsno, goodsbn,

goodsname, price, goods_image, totalmoney,

storageledgerid,goodsunitname,specvalues);

tshoppingcart.addTchildren(tenantno,ashoppingcart);

request.getSession().setAttribute("goodcar",tshoppingcart);

}else{

TableObject ashoppingcart=tshoppingcart.getTchildren

(tenantno);

if(ashoppingcart!=null){

boolean find=false;

for(int i=0;i<ashoppingcart.getChildren().length;i++){

TableObject cart=ashoppingcart.getChildren()[i];

if(cart.getAttribute("goodsbn").equals(goodsbn)

&&cart.getAttribute("tenantno").equals(tenantno)){

find=true;

break;

}

}

if(!find){

TableObject childcart=new TableObject();

childcart.setTableName("saleoutlineitem");

childcart.setPrimaryKey("lineitemid");

childcart.setPrimaryType(TableObject.LONG);

childcart.setAutoGenerate(true);

childcart.setAttribute("storageledgerid",

第 14 章 网上商城前台实现技术

·353·

storageledgerid,TableObject.LONG);

childcart.setAttribute("quantity", quantity,TableObject.

DOUBLE);

childcart.setAttribute("goodsno", goodsno);

childcart.setAttribute("goodsbn", goodsbn);

childcart.setAttribute("goodsname", goodsname);

childcart.setAttribute("price", price,TableObject.

DOUBLE);

childcart.setAttribute("goods_image", goods_image);

childcart.setAttribute("money", totalmoney,TableObject.

DOUBLE);

childcart.setAttribute("tenantno", tenantno);

childcart.setAttribute("specvalues", specvalues);

childcart.setAttribute("goodsunitname", goodsunitname);

ashoppingcart.addChildren(childcart);

}

}else{

ashoppingcart=getnewShoppingcart(tenantno, tenantname,

quantity, goodsno, goodsbn,

goodsname, price, goods_image, totalmoney,

storageledgerid,goodsunitname,specvalues);

tshoppingcart.addTchildren(tenantno,ashoppingcart);

}

}

}

if(tshoppingcart!=null){

double amount=0;

for(TableObject shoppingcart:tshoppingcart.getTchildrens()){

dynamiTe.setDynElemValue("allcarts", ""); // reset for each

row

for (TableObject children:shoppingcart.getChildren()) {

if(storageledgerid==null){

storageledgerid=children.getAttribute

("storageledgerid");

}

第四部分 网上商城完整实现

·354·

dynamiTe.setVariable("goods.storageledgerid",storageledgerid);

dynamiTe.setVariable("goods.tenantno",shoppingcart.

getAttribute("tenantno"));

dynamiTe.setVariable("goods.goodsno",children.

getAttribute("goodsno"));

dynamiTe.setVariable("goods.goodsbn", children.getAttribute

("goodsbn"));

dynamiTe.setVariable("goods.goods_name", children.

getAttribute("goodsname"));

dynamiTe.setVariable("goods.price", children.

getAttribute("price"));

dynamiTe.setVariable("goods.quantity", children.

getAttribute("quantity"));

dynamiTe.setVariable("goods.goods_image",children.

getAttribute("goods_image"));

double subtotal=Double.parseDouble(children.getAttribute

("quantity"))*Double.parseDouble(children.getAttribute("price"));

amount=amount+subtotal;

dynamiTe.setVariable("goods.subtotal",String.valueOf

(subtotal));

dynamiTe.parseDynElem("allcarts"); // add a row

}

dynamiTe.setVariable("cart.store_name",shoppingcart.

getAttribute("tenantname"));

dynamiTe.setVariable("cart.amount",String.valueOf(amount));

dynamiTe.parseDynElem("allstores"); // add a row

}

}

dynamiTe.parse();

return dynamiTe.toString();

}

@Override

public IParseTemplateAction getAction(String filename) {

IParseTemplateAction action=includefiles.get(filename);

return action;

}

第 14 章 网上商城前台实现技术

·355·

@Override

public ITemplateAction getParent() {

return this;

}

private TableObject getnewShoppingcart(String tenantno, String tenanntname,

String quantity,String goodsno,String goodsbn,

String goodsname,String price,String goods_image,String

totalmoney,String storageledgerid,String goodsunitname,String specvalues){

TableObject ashoppingcart=new TableObject();

ashoppingcart.setTableName("saleoutstockentry");

ashoppingcart.setPrimaryKey("saleoutstockentryno");

ashoppingcart.setPrimaryType(TableObject.STRING);

ashoppingcart.setAutoGenerate(true);

ashoppingcart.setAttribute("tenantno", tenantno);

ashoppingcart.setAttribute("tenantname", tenanntname);

TableObject childcart=new TableObject();

childcart.setTableName("saleoutlineitem");

childcart.setPrimaryKey("lineitemid");

childcart.setPrimaryType(TableObject.LONG);

childcart.setAutoGenerate(true);

childcart.setAttribute("quantity", quantity,TableObject.DOUBLE);

childcart.setAttribute("goodsno", goodsno);

childcart.setAttribute("goodsbn", goodsbn);

childcart.setAttribute("goodsname", goodsname);

childcart.setAttribute("price", price,TableObject.DOUBLE);

childcart.setAttribute("goods_image", goods_image);

childcart.setAttribute("money", totalmoney,TableObject.DOUBLE);

childcart.setAttribute("storageledgerid", storageledgerid,

TableObject.LONG);

childcart.setAttribute("tenantno", tenantno);

childcart.setAttribute("goodsunitname", goodsunitname);

childcart.setAttribute("specvalues", specvalues);

ashoppingcart.setAttribute("totalmoney", totalmoney,TableObject.

DOUBLE);

ashoppingcart.addChildren(childcart);

return ashoppingcart;

第四部分 网上商城完整实现

·356·

}

}

这段源代码是实现购物车的核心源代码。当我们单击“加入购物车”按钮的时候,程

序先去 Session 中寻找这个商品是否存在,如果不存在,则把这个商品加入到购物车中,如

果存在,则就不做处理了。

如果商城消费者购买了多个商品,则用 for 循环全部显示出来。

for(TableObject shoppingcart:tshoppingcart.getTchildrens()){

dynamiTe.setDynElemValue("allcarts", ""); // reset for each row

for (TableObject children:shoppingcart.getChildren()) { 。。。。。。

dynamiTe.setVariable("goods.goodsno",children.getAttribute("goodsno"));

dynamiTe.setVariable("goods.goodsbn", children.getAttribute

("goodsbn"));

}

}

dynamiTe.setVariable("cart.store_name",shoppingcart.

getAttribute("tenantname"));

dynamiTe.setVariable("cart.amount",String.valueOf(amount));

dynamiTe.parseDynElem("allstores"); // add a row

dynamiTe 通过 dynamiTe.parseDynElem("allstores")可以动态增加一行。

对应的 HTML 源代码如下。

<!-- BEGIN DYNAMIC : allstores -->

<div class="con"> <h4>店铺: <a href="">{cart.store_name}</a></h4>

<div class="buytable">

<table>

<tr> <th width="420">店铺商品</th>

<th>价格</th>

<th>数量</th>

<th>小计</th>

<th>操作</th>

第 14 章 网上商城前台实现技术

·357·

</tr>

<!-- BEGIN DYNAMIC : allcarts -->

<tr id="cart_item_{goods.storageledgerid}">

<td class="padding1">

<p class="ware_pic"><a href="showgood.action?

goodsno={goods.goodsno}&tenantno={goods.tenantno}" target="_blank"><img src="

{goods.goods_image}" alt="{goods.goods_name}" width="65" height="65"

/></a></p>

<h3>

<a href="showgood.action?goodsno={goods.

goodsno}&tenantno={goods.tenantno}" target="_blank">{goods.goods_name}</a>

<span class="attr">{goods.specification}

</span>

</h3>

</td>

<td class="align1">

<span class="price1">{goods.price}</span>

<input id="price_{goods.storageledgerid}"

value="{goods.price}" type="hidden">

</td>

<td class="align2">

<img src="mall/themes/mall/default/styles/

default/images/subtract.gif" onclick="decrease_quantity({goods. storageledgerid});"alt="减少" width="12" height="12" />

<input id="input_item_{goods.storageledgerid}"

name="input_item_{goods.storageledgerid}" value="{goods.quantity}" orig=

"{goods.quantity}" changed="{goods.quantity}" onkeyup="change_quantity

({goods.tenantno}, {goods.storageledgerid},{goods.price});" class="text1 width3"

type="text" />

<img src="mall/themes/mall/default/styles

/default/images/adding.gif"onclick="add_quantity({goods.storageledgerid},'{goods.tenantno}');" alt="增加" width="12" height="12" />

</td>

<td class="align1"><span class="price2" id="item

{goods.storageledgerid}_subtotal">{goods.subtotal}</span></td>

<td class="align2">

<a class="del" href="removecart.action?

第四部分 网上商城完整实现

·358·

tenantno={goods.tenantno}&storageledgerid={goods.storageledgerid}">删除</a>

</td>

</tr>

<!-- END DYNAMIC : allcarts -->

</table>

</div>

</div>

<!-- END DYNAMIC : allstores -->

因为我们做的是一个网上商城系统,一张订单可能对应着多个商铺店主,所以有两个

标签<!-- BEGIN DYNAMIC : allstores -->和<!-- BEGIN DYNAMIC : allcarts -->分别对应多

个店主和多个商品。

相应的界面如图 14-11 所示。

图 14-11 网上商城一张订单对应的多个商铺店主

如果是一张订单,有多个店铺的多个商品,那么用户在确认订单后,后台处理逻辑是

很复杂的,我们要把一张购物单拆成多张购物单。我们要先从订单中找出所有的店铺,每

个店铺再形成每一张商品的订单。事实上,淘宝也是这么做的。对于每一个订单,存盘后

就是存到我们在后台管理能看到的销售出库单,如图 14-12 所示。

第 14 章 网上商城前台实现技术

·359·

图 14-12 从后台看到的销售出库单

这样,前台网上商城系统,与后台进销存,就完美地结合在一起了。购物车的例子也

就讲完了。

14.5 实现用户中心管理

对于网上商城系统来讲,用户中心也是必不可少要实现的,用户中心如图 14-13 所示。

用户中心一般要编辑个人资料,如邮寄地址、联系方式等信息。还有一项重要的功能

是查看订单。订单的信息如图 14-14 所示。

限于篇幅,我就不把这些源代码都列出来了。所用到的技术,都是咱们以前讲到的技

术。

第四部分 网上商城完整实现

·360·

图 14-13 用户中心管理界面

图 14-14 查看订单信息

14.6 本章小结

本章是后台进销存的继续,在这里我们把后台进销存的录入数据在前台展示出来,此

外还讨论了网页模板技术的实现。对于用户下的订单,如果是多个店铺的,我们也给出了

自己的解决方案。在后台,我们通过 SaaS 技术和数据隔离策略把前台网上商城传过来的订

单自动分拆为每个店铺自己的销售订单。

第 14 章 网上商城前台实现技术

·361·

这里网上商城平台本身并没有参与到订单本身的审批,就跟淘宝的做法一样。事实上,

如果自己真地开发一套基于 SaaS 技术架构的 B2B2C 的平台,作为平台本身,是否要参与

到订单的审批与转发过程当中,也是存在争议的。如果像京东商城那样,所有的订单,都

是京东商城自己处理的,物流和消费者的售后,也是京东商城负责的,那么,作为一个经

销商,能做的就是通过后台进销存,把商品发布到京东商城平台而已,自己只是京东商城

库房的一部分。京东商城销售出去商品后,对应库房(经销商)库存减少,然后京东商城

再跟经销商发生结算,仅此而已。如果是当当网,则经销商很可能是跟当当网共享一个物

流渠道和支付渠道。而商品的上架和下架以及售后服务,都是经销商自己去维护的,当当

网相当于出租出去了自己的柜台。那么在这样的 B2B2C 架构下,消费者下的订单,当当网

和经销商,都是可以直接查得到的。

第四部分 网上商城完整实现

·362·

附录 A 电子商务发展简史

A.1 电子商务发展历史

电子商务已经成为我们这个时代 重要和 成功的互联网应用之一。下面我们聊聊世

界电子商务发展简史。

1979 年,英国开始研究 Videotex(这是一个双向信息传送服务的视传系统:通过电话

线路或者电视电缆将信息从计算机网络输向用户终端,并在用户电视屏幕或者计算机终端

上显示的视传系统,用于各种设备、比如电子银行、电子邮递和家庭采购),当时很多公司

对此表现出兴趣。在这个背景上,一个名叫 Michael Aldrich(生于 1941 年)的英格兰企业

家、改革家和发明家,为 Videotex 提出了远程购物的概念,这就是现代电子商务的 初

设想。

1982 年,Minitel 继 ideotext 推出在线购物、查询股票、搜索电话目录等服务,甚至可

以在线聊天。这是 WWW 之前 成功的应用, 早在法国成功推出,在英国也有推出但

没有那么成功。

1987 年,CompuServe 的一个分支 Swreg 推出软件开发社区,这是开发者们使用的

一个在线市场,可以销售自己的软件产品,电子商务 初应用在软件行业中。

1990 年,Tim Berners-Lee 编写出 WorldWideWeb,这是第一个 Web 浏览器,并引发

一场革命,真正意义上的电子商务从此真正蓬勃发展起来。

1992 年,St. Martin’s 出版社出版了 J.H. Snider 和 Terra Ziporyn 等人的著作,未来的

商店,革命性地讲述新技术将怎样改变未来的购物。

1994 年,Netscape 发布 Netscape 浏览器(图 A-1),后来推出 Secure Sockets Layer

附录 A 电子商务发展简史

·363·

(SSL)安全技术。Pizza Hut 开始通过他们的网站卖批萨饼。汽车、自行车、成人用品等商

品开始在线销售。

图 A-1 Netscape 浏览器

1995 年,Amazon.com 开始在线零售业务(图 A-2),随着 Jeff Bezos 创办了第一个

24 小时播音的非商业在线电台,香港电台及 NetRadio 开播。Dell 与 Cisco 等公司开始全

面使用互联网进行销售。

1998 年,美国开始在线销售电子邮票,可以在线订购并下载打印。

1999 年,eCompanies 花费 750 万美元买下 Business.com 域名。Napster 发布 P2P 文

件分享系统。一些家装产品开始在 ATG Stores 销售。

2000 年,互联网泡沫并非一夜到来,1995~2000 年,凡是沾 e 字或 .com 字的公司

都能在股票市场大红大紫,这期间看到大量公司崛起或者倒下。那时的风险投资商都很慷

慨,他们认为,只要生意扩张得足够大,总有一天,他们的盈利会补上所有的投入和亏空,

纳斯达克在 2000 年 3 月 10 日到达 5132.52 点,如图 A-3 所示。

第四部分 网上商城完整实现

·364·

图 A-2 Amazon.com 开始在线零售业务

图 A-3 纳斯达克的顶峰达到 5132.52

附录 A 电子商务发展简史

·365·

2002 年,PayPal 推出在线支付,后来被 eBay 以 15 亿美元收购。CSN Stores 及 NetShops

等在线商店创立,注重专一商品的销售,如图 A-4 所示。

图 A-4 PayPal 的在线支付

2003 年,电子商务趋于成熟,Amazon.com 发布首年盈利报告。

2007 年,R.H. Donnelley 以 3 亿 4500 万美金购买 Business.com 域名。

2008 年,美国的在线销售额高达 2040 亿美元,比 2007 年增加 17%。

2009 年,在线零售商每天带动的产值据估算有 2 万 5000 亿美元,年增长 14%,eBay 的

营业额达到 18.9 亿美元。

图 A-5 为电子商务的发展简史图。

第四部分 网上商城完整实现

·366·

图 A-5 电子商务的发展简史图

附录 A 电子商务发展简史

·367·

以上资料中文编译来源:锐商企业 CMS 网站内容管理系统,有修改。

A.2 中国电子商务发展简史

中国真正意义上电子商务是从 1997 年开始到 2010 年,已经有十三个发展年头了。在

这十三年期间,代表性事件如下:

1997 年,中国化工信息网正式在互联网上提供服务,开拓了网络化工的先河,是全国

第一个介入行业网站服务的国有机构。

1997 年,“易贸通”推出 Tradeeasy.comB2B 贸易入门网站。

1997 年 12 月,中国化工网(英文版)上线,成为国内第一家垂直 B2B 电子商务商业

网站。

1998 年 10 月,美商网(又名“相逢中国”)获多家美国知名 VC 千万美金投资,是

早进入中国 B2B 电子商务市场的海外网站,首开全球 B2B 电子商务先河。

1998 年 2 月,由焦点科技运营的中国制造网(英文版)在南京上线。

1998 年 12 月,阿里巴巴正式在开曼群岛注册成立,1999 年 3 月其子公司阿里巴巴中

国在我国杭州创建,同年 6 月在开曼群岛注册阿里巴巴集团。

1999 年 8 月,邵亦波创办国内首家 C2C 电子商务平台“易趣网”。

1999 年 5 月,“中国电子商务第一人”王峻涛创办“8848”涉水电子商务,并在当年

融资 260 万美元,标志着国内第一家 B2C 电子商务网站诞生。

1999 年 6 月,《数字化经济》一书在 8848 首发,成为中国网上首发图书第一例。

1999 年 9 月,招商银行率先在国内全面启动“一网通”网上银行服务,建立了由网上

企业银行、网上个人银行、网上支付、网上证券及网上商城为核心的网络银行服务体系,

并经央行批准成为国内首家开展网上个人银行业务的商业银行。

1999 年,中国网库推出“中国网络黄页”,并在全国各地开通了地方 114 网,并以各

地 114 网为基础为企业提供网络信息化应用等全套服务。

2000 年 4 月,于 1992 年成立的慧聪国际推出了慧聪商务网,即现在的慧聪网。

2000 年 5 月,卓越网成立,为我国早期 B2C 网站之一。

2000 年 6 月 21 日,中国电子商务协会正式成立。

第四部分 网上商城完整实现

·368·

2000 年 12 月,阿里巴巴在前一年 10 月获高盛等 500 万天使投资的基础上,获日本软

银等境外财团联合投资 2500 万美元,由此开始奠定阿里巴巴电子商务王国的基础。

2001 年 7 月 9 日,中国人民银行颁布《网上银行业务管理暂行办法》。

2001 年 10 月,中国化工网成功打赢“中国入世跨国知识产权第一案”,捍卫了对全球

化工顶级域名 chemnet.com 的所有权,成为我国互联网领域知识产权官司的标本。

2002 年 3 月,全球 大网络交易平台 eBay 以 3000 万美元的价格,购入易趣网 33%股

份。

2002 年 9 月,王峻涛创办 6688 电子商务网站,二度进军 B2C 网上商城。

2003 年 5 月,“非典”给电子商务带来了意外的发展机遇,各 B2B、B2C 电子商务网

站会员数量迅速增加,并且部分实现赢利,C2C 也由此酝酿变局。

2003 年 5 月,阿里巴巴集团投资 1 亿人民币成立淘宝网,进军 C2C;随后几年内,渐

改变国内 C2C 市场格局,而网购理念与网民网购消费习惯也进一步得到普及。

2003 年 6 月,eBay 以 1.5 亿美元收购易趣剩余 67%股份,国内 大 C2C 企业由此被

外资全盘并购。

2003 年 10 月,阿里巴巴推出“支付宝”,致力于为网络交易用户提供基于第三方担保

的在线支付服务,正式进军电子支付领域。

2003 年 12 月,慧聪网(08292-HK)香港创业板上市,为国内 B2B 电子商务首家上市

公司。

2004 年 1 月,阿里巴巴集团董事局主席马云正式提出“网商”概念。

2004 年 1 月 8 日,中国电子商务“先驱”8848 在京“复出”回到电子商务领域,转

型专注做“中国电子商务引擎”。

2004 年 6 月,“第一届网商大会”在杭州举办。

2004 年 8 月,亚马逊以 7500 万美元协议收购卓越网,并更名为卓越亚马逊。

2005 年 2 月,支付宝推出保障用户利益的“全额赔付”制度,开国内电子支付的先河;

当年 7 月又推出“你敢用,我敢赔”的支盟计划。

2005 年 4 月 1 日,《电子签名法》正式施行,奠定了电子商务市场良好发展态势的基

础,也是中国信息化领域的第一部法律。

2005 年 4 月 18 日,中国电子商务协会政策法律委员会组织有关企业起草的《网上交

易平台服务自律规范》正式对外发布。

附录 A 电子商务发展简史

·369·

2005 年 8 月,阿里巴巴并购雅虎中国全部资产,同时得到雅虎 10 亿美元投资,雅虎

则拥有 40%股份,由此成为阿里巴巴 大控股股东。

2005 年 9 月 12 日,腾讯依托 QQ 逾 5.9 亿的庞大用户推出“拍拍网”,C2C 三足鼎立

格局渐形成。

2006 年 3 月,“第一届中小企业电子商务应用发展大会”在北京举行。

2006 年 6 月,商务部公布了《中华人民共和国商务部关于网上交易的指导意见》(征

求意见稿)。

2006 年 10 月,慧聪网与分众无线联手推出国内首个无线 B2B 平台。

2006 年 11 月,创立于 1999 年的 B2B 电子商务商之一亚商在线,被世界 500 强公司

之一的 Office Depot 收购,亚商在线是中国当时 大的办公用品与办公服务 B2B 电子商务

公司,Office Depot 公司是世界 大的电子商务零售商之一,网上年销售额达 38 亿美元。

2006 年 12 月 15 日,电子商务领军企业网盛科技(002095,SZ)登录深圳中小企业板,

标志着A股“中国互联网第一股”诞生,由此改变了十年来我国互联网产业与资本市场的

无一境内上市公司的尴尬历史。

2006 年 12 月,eBay 和 TOM 在线建合资公司 TOM 易趣,分别持股 49%和 51%。

2007 年 3 月 6 日,商务部发布了《关于网上交易的指导意见(暂行)》。

2007 年 4 月,PPG 共获得 5000 万美元的国际风险投资,这种无店铺、无渠道的 B2C

新型电子商务直销模式,表明传统产业与电子商务的进一步融合。

2007 年 6 月 1 日,国家发改委、国务院信息化工作办公室联合发布我国首部电子商务

发展规划——《电子商务发展“十一五”规划》,首次在国家政策层面确立了发展电子商务

的战略和任务,这是我国第一个国家级的电子商务发展规划。

2007 年 6 月,我国行业网站首例并购案宣告完成,网盛科技斥资 1000 万 51%控股并

购中国服装网(efu.com.cn),由此揭开了我国行业网站整合大幕。

2007 年 6 月,“中国行业网站投资与发展高峰论坛”在杭州举行,这是我国行业网站

与资本的首次大规模对接。

2007 年 8 月,今日资本向京东商城投资 1000 万美元,开启国内家电 3C 网购新时代。

2007 年 10 月,“MadeInChina 慧聪网”上线,标志着慧聪网开始涉足外贸领域。

2007 年 11 月 6 日,开曼群岛注册成立的阿里巴巴网络有限公司(1688-HK)成功在

香港主板上市,融资 16.9 亿美元,创全球互联网企业融资额第二大纪录。

第四部分 网上商城完整实现

·370·

2008 年 4 月 24 日,商务部起草了《电子商务模式规范》和《网络购物服务规范》。

2008 年 5 月,易趣网宣布用户网上开店将获终身免费,免费项目涵盖包括店铺费、商

品登录费、店铺使用费等传统项目费用,C2C 市场竞争加剧。

2008 年 5 月,中东 大的 B2B 在线电子商务交易平台“特佳易”进入中国市场,同

年,进入中国市场的还有“欧罗帕-欧洲买家中心”等海外知名 B2B 服务商,国外 B2B 服

务商对中国市场的重视和期望达到空前的高度。

2008 年 5 月 29 日,中国电子商务协会正式批复了杭州市政府有关申请,决定授予杭

州市“中国电子商务之都”称号。

2008 年起,为应对国际金融危机对经济的影响,我国各地方政府纷纷出台政策,通过

切实的财政扶持等手段,普及中小企业电子商务的应用,其中杭州、浙江、南京、江苏、

广州、广东、上海、成都、四川等省市走在了全国前列。

2008 年 7 月,北京市工商局公布了“关于贯彻落实《北京市信息化促进条例》加强电

子商务监督管理的意见”,规定从 8 月 1 日起北京地区的网店经营者从事买卖前必须先注册

营业执照,否则将被工商部门查处。这是全国首部地方性针对网店的地方性法规。

2008 年 7 月 23 日,首次以电子商务生态为主题的学术研讨会在古城西安举办。

2008 年 8 月,凡客诚品已累计向启明投资、软银等财团三轮融资 4000 万美元。

2008 年 9 月,百度“有啊”宣布上线,淘宝随即屏蔽百度搜索,引发“屏蔽门”事件。

2008 年,服装 B2C 直销热兴起投资热,以 VANCL、BONO、衣服网、李宁为行业代

表的各类服装网购平台兴起,其在线直销模式渐引发了统服装销售渠道的变革。

2008 年 12 月 3 日,商务部国际电子商务中心成立移动商务应用实验室。

2008 年末至 2009 年初,中国服装 B2C 模式的创新者和领导者 PPG,遭遇资金困境与

诚信危机。

2008 年,中国电子商务 B2B 市场交易额达到 3 万亿元;网购交易额也首次突破千亿,

达到 1500 亿。

2008 年底,受国际金融危机产业链的深度蔓延,部分严重依赖外贸中小企业生存的电

子商务企业倒闭,其中就包括:老牌电子商务企业万国商业网、上市公司九城关贸下属的

沱沱网、慧聪网下属宁波慧聪网等知名外贸 B2B 电子商务服务企业。

2009 年 1 月,网易“有道”搜索推出国内首个面向普通大众提供购物搜索服务的购物

搜索,随后谷歌(中国)也采取市场跟进策略,推出类似搜索产品,这标志着“购物搜索

附录 A 电子商务发展简史

·371·

时代”的启幕。

2009 年 1 月,今日资本、雄牛资本等向京东商城联合注资 2100 万美元,引发国内家

电 B2C 领域投资热。

2009 年 2 月,慧聪网行业公司获 ISO9001 质量管理体系的证书,成国内首家获得 ISO

质量管理体系认证的互联网企业。

2009 年 5 月 1 日起,由中国国际经济贸易仲裁委员会颁布的《中国国际经济贸易仲裁

委员会网上仲裁规则》正式施行,该规则特别适用于解决电子商务争议。

2009 年 5 月 3 日,当当网宣布率先实现赢利,平均毛利率达 20%,成为目前国内首家

实现全面盈利的网上购物企业。

2009 年 6 月,视频网站土豆网、优酷网先后启动将视频技术与淘宝的网购平台相结合,

共同提升用户网络购物的真实体验,推出“视频电子商务”应用技术。

2009 年 6 月,“国家队”银联支付与 B2C 企业当当网签订合作协议,这是银联支付成

立七年来,首度进入电子商务支付领域,与在线第三方支付市场领导支付宝形成了正面竞

争。

2009 年 7 月 24 日,淘宝网“诚信自查系统”上线,为 C2C 历史上规模 大的一次反

涉嫌炒作卖家的自查举措。

2009 年 8 月,百度宣布以“X2C”为核心的电子商务战略,并公布“凤鸣计划”。

2009 年 8 月,中国电子商务协会授予金华为“中国电子商务应用示范城市”。

2009 年 9 月,卓越亚马逊再次推出全场免运费与当当网相持,这是两大行业竞争者十

年来首次同时免运费,标志着“免运费”将开始成为 B2C 行业标准规则。

2009 年 9 月,“首届电子商务与快递物流大会”在杭州休博园召开,其宏观背景是,

物流快递行业作为电子商务的支撑产业之一,近几年在第三方电子商务平台的带动下得到

了快速发展。

以上数据摘自中国电子商务研究中心《1997-2009:中国电子商务十二年调查报告》,

有删节。

参考文档 (1)使用 IBM 中间件实现 SaaS 解决方案第 2 部分:启用多租户的方法

http://www.ibm.com/developerworks/cn/webservices/ws-multitenantpart2/

第四部分 网上商城完整实现

·372·

编 辑 后 记

100 多年前,美国加利福尼亚因发现金矿掀起了一股淘金热,许多先行者一夜暴富,

吸引了更多的后继者涌来,李维·施特劳斯也是众多淘金者中的一员。随着淘金者日益增

多,竞争日趋激烈,后来者能靠金子致富的机会越来越少了,原打算淘金的李维·施特劳

斯逐渐把眼光转移到为淘金者服务这一行业并发明了“牛仔裤”,现如今当年那些靠淘金暴

富的人早已不知所终,而“李维斯”牛仔裤的神话却一直延续至今。

今天,电子商务在中国正在掀起新一轮的淘金热,无数人通过电子商务成为百万富翁

甚至千万富翁,这一淘金效应也正吸引着越来越多的后来者投入其中,目前只淘宝网一家

就集中了几百万的卖家,作为后来者,通过简单的 C2C 一夜暴富的机会是越来越小了,而

与此同时,淘宝 API 开放平台的出现,无疑给众多的技术人员开启了一条新的创富之路。

《B2B2C 网上商城开发指南——基于 SaaS 和淘宝 API 开放平台》一书正是基于此种形

式应运而生的,书中不仅讲解了技术,更讲解了开放这一趋势,作为拥有十年电子商务开

发经验的老兵,作者在书中毫无保留的讲述了自己在网上商城架构、设计、编码方面的心

得体会,相信对每一位有志于在此领域创建自己“牛仔裤”品牌的人士都会有所帮助。

本书即将付印之际,正赶上 2011 年“大淘宝开放年战略发布会”的召开,可以说此书

的出版是生逢其时。作为本书的编辑,有幸在第一时间阅读了全书,确实是一件幸事。关

于本书以及“开放平台”这一话题,您有任何意见或建议,请发至邮箱:[email protected],

或联系 QQ:909234868,期待您的高见。

第四部分 网上商城完整实现

·374·

反侵权盗版声明

电子工业出版社依法对本作品享有专有出版权。任何未经权利人书面许可,复制、

销售或通过信息网络传播本作品的行为;歪曲、篡改、剽窃本作品的行为,均违反《中

华人民共和国著作权法》,其行为人应承担相应的民事责任和行政责任,构成犯罪的,将

被依法追究刑事责任。

为了维护市场秩序,保护权利人的合法权益,我社将依法查处和打击侵权盗版的单位

和个人。欢迎社会各界人士积极举报侵权盗版行为,本社将奖励举报有功人员,并保证举

报人的信息不被泄露。

举报电话:(010)88254396;(010)88258888 传 真:(010)88254397

E-mail:[email protected]

通信地址:北京市万寿路 173信箱 电子工业出版社总编办公室

邮 编:100036

www.phei.com.cn www.broadview.com.cn

B2B2C网上商城开发指南——基于SaaS和淘宝开放平台

尊敬的读者: 感谢您选择我们出版的图书,您的支持与信任是我们持续上升的动力。为了使您能通过本书更透

彻地了解相关领域,更深入的学习相关技术,我们将特别为您提供一系列后续的服务,包括: 1.提供本书的修订和升级内容、相关配套资料; 2.本书作者的见面会信息或网络视频的沟通活动; 3.相关领域的培训优惠等。 您可以任意选择以下四种方式之一与我们联系,我们都将记录和保存您的信息,并给您提供不定

期的信息反馈。

1.在线提交

登陆www.broadview.com.cn/12983,填写本书的读者调查表。

2.电子邮件

您可以发邮件至[email protected][email protected]

3.读者电话

您可以直接拨打我们的读者服务电话:010-88254369。

4.信件 您可以写信至如下地址:北京万寿路173信箱博文视点,邮编:100036。 您还可以告诉我们更多有关您个人的情况,及您对本书的意见、评论等,内容可以包括: (1)您的姓名、职业、您关注的领域、您的电话、E-mail地址或通信地址; (2)您了解新书信息的途径、影响您购买图书的因素; (3)您对本书的意见、您读过的同领域的图书、您还希望增加的图书、您希望参加的培训等。

如果您在后期想停止接收后续资讯,只需编写邮件“退订+需退订的邮箱地址”发送至邮箱:

[email protected] 即可取消服务。

同时,我们非常欢迎您为本书撰写书评,将您的切身感受变成文字与广大书友共享。我们将挑选特别优秀的作品转载在我们的网站(www.broadview.com.cn)上,或推荐至CSDN.NET等专业网站上发表,被发表的书评的作者将获得价值50元的博文视点图书奖励。

更多信息,请关注博文视点官方微博:http://t.sina.com.cn/broadviewbj。 我们期待您的消息!

博文视点愿与所有爱书的人一起,共同学习,共同进步!

通信地址:北京万寿路173信箱 博文视点(100036) 电话:010-51260888 E-mail:[email protected][email protected]