├── .gitignore ├── README.md ├── index.js ├── lib ├── interfacemanager.js ├── modelproxy-client.js ├── modelproxy.js ├── modelproxy.js.bak ├── proxyfactory.js └── proxyfactory.js_redis ├── package.json └── tests ├── README.md ├── interfaceRules ├── Cart.getMyCart.rule.json ├── Search.getNav.rule.json ├── Search.list.rule.json └── Search.suggest.rule.json ├── interface_test.json ├── interfacemanager.test.js ├── mockserver.js ├── modelproxy.test.js └── proxyfactory.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | tests/coverage.html 2 | 3 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | > 如果觉得不错的话,请star一下吧 😊 4 | 5 | ##### 使用技术: 前端架构express + node + xtemplate + mockjs + modelproxy-copy,服务端架构nodejs + express 6 | 7 | ##### 项目说明: 此项目是本人空余时间搭建的。希望大家提供宝贵的意见和建议,谢谢。 8 | 9 | ##### JS/React/Vue/Angular前端群: 599399742 10 | 11 | ##### 邮   箱: sosout@139.com 12 | 13 | ##### 个人网站: http://www.sosout.com/ 14 | 15 | ##### 个人博客: http://blog.sosout.com/ 16 | 17 | ##### 个人简书: http://www.jianshu.com/users/23b9a23b8849/latest_articles 18 | 19 | 该模块基于modelproxy改写,大部分代码保持不变,加上modelproxy在网上很难下载,所以我特此分享下,供大家使用,今后有什么新功能我也会在此基础迭代。 20 | --- 21 | 淘系的技术大背景下,必须依赖Java提供稳定的后端接口服务。在这样环境下,Node Server在实际应用中的一个主要作用即是代理(Proxy)功能。由于淘宝业务复杂,后端接口方式多种多样(MTop, Modulet, HSF...)。然而在使用Node开发web应用时,我们希望有一种统一方式访问这些代理资源的基础框架,为开发者屏蔽接口访问差异,同时提供友好简洁的数据接口使用方式。于是就有了 midway-modelproxy 这个构件。使用midway-modelproxy,可以提供如下好处: 22 | 23 | 1. 不同的开发者对于接口访问代码编写方式统一,含义清晰,降低维护难度。 24 | 2. 框架内部采用工厂+单例模式,实现接口一次配置多次复用。并且开发者可以随意定制组装自己的业务Model(依赖注入)。 25 | 3. 可以非常方便地实现线上,日常,预发环境的切换。 26 | 4. 内置[river-mock](http://gitlab.alibaba-inc.com/river/mock/tree/master)和[mockjs](http://mockjs.com)等mock引擎,提供mock数据非常方便。 27 | 5. 使用接口配置文件,对接口的依赖描述做统一的管理,避免散落在各个代码之中。 28 | 6. 支持浏览器端共享Model,浏览器端可以使用它做前端数据渲染。整个代理过程对浏览器透明。 29 | 7. 接口配置文件本身是结构化的描述文档,可以使用[river](http://gitlab.alibaba-inc.com/river/spec/tree/master)工具集合,自动生成文档。也可使用它做相关自动化接口测试,使整个开发过程形成一个闭环。 30 | 31 | ### ModelProxy工作原理图及相关开发过程图览 32 | --- 33 | ![](http://gtms03.alicdn.com/tps/i3/T1kp4XFNNXXXXaE5nO-688-514.png) 34 | 35 | # 使用前必读 36 | --- 37 | 使用ModelProxy之前,您需要在工程根目录下创建名为interface.json的配置文件。该文件定义了工程项目中所有需要使用到的接口集合(详细配置说明见后文)。定义之后,您可以在代码中按照需要引入不同的接口,创建与业务相关的Model对象。接口的定义和model其实是多对多的关系。也即一个接口可以被多个model使用,一个model可以使用多个接口。具体情况由创建model的方式来决定。下面用例中会从易到难交您如何创建这些model。 38 | 39 | # 快速开始 40 | --- 41 | 42 | ### 用例一 接口文件配置->引入接口配置文件->创建并使用model 43 | * 第一步 配置接口文件命名为:interface_sample.json,并将其放在工程根目录下。 44 | 注意:整个项目有且只有一个接口配置文件,其interfaces字段下定义了多个接口。在本例中,仅仅配置了一个主搜接口。 45 | 46 | ```json 47 | { 48 | "title": "pad淘宝项目数据接口集合定义", 49 | "version": "1.0.0", 50 | "engine": "mockjs", 51 | "rulebase": "./interfaceRules/", 52 | "status": "online", 53 | "interfaces": [ { 54 | "name": "主搜索接口", 55 | "id": "Search.getItems", 56 | "urls": { 57 | "online": "http://s.m.taobao.com/client/search.do" 58 | } 59 | } ] 60 | } 61 | ``` 62 | 63 | * 第二步 在代码中引入ModelProxy模块,并且初始化引入接口配置文件(在实际项目中,引入初始化文件动作应伴随工程项目启动时完成,有且只有一次) 64 | 65 | ```js 66 | // 引入模块 67 | var ModelProxy = require( 'modelproxy' ); 68 | 69 | // 初始化引入接口配置文件 (注意:初始化工作有且只有一次) 70 | ModelProxy.init( './interface_sample.json' ); 71 | ``` 72 | 73 | * 第三步 使用ModelProxy 74 | 75 | ```js 76 | // 创建model 77 | var searchModel = new ModelProxy( { 78 | searchItems: 'Search.getItems' // 自定义方法名: 配置文件中的定义的接口ID 79 | } ); 80 | // 或者这样创建: var searchModel = new ModelProxy( 'Search.getItems' ); 此时getItems 会作为方法名 81 | 82 | // 使用model, 注意: 调用方法所需要的参数即为实际接口所需要的参数。 83 | searchModel.searchItems( { keyword: 'iphone6' } ) 84 | // !注意 必须调用 done 方法指定回调函数,来取得上面异步调用searchItems获得的数据! 85 | .done( function( data ) { 86 | console.log( data ); 87 | } ) 88 | .error( function( err ) { 89 | console.log( err ); 90 | } ); 91 | ``` 92 | 93 | ### 用例二 model多接口配置及合并请求 94 | * 配置 95 | 96 | ```json 97 | { // 头部配置省略... 98 | "interfaces": [ { 99 | "name": "主搜索搜索接口", 100 | "id": "Search.list", 101 | "urls": { 102 | "online": "http://s.m.taobao.com/search.do" 103 | } 104 | }, { 105 | "name": "热词推荐接口", 106 | "id": "Search.suggest", 107 | "urls": { 108 | "online": "http://suggest.taobao.com/sug" 109 | } 110 | }, { 111 | "name": "导航获取接口", 112 | "id": "Search.getNav", 113 | "urls": { 114 | "online": "http://s.m.taobao.com/client/search.do" 115 | } 116 | } ] 117 | } 118 | ``` 119 | 120 | * 代码 121 | 122 | ```js 123 | // 更多创建方式,请参考后文API 124 | var model = new ModelProxy( 'Search.*' ); 125 | 126 | // 调用自动生成的不同方法 127 | model.list( { keyword: 'iphone6' } ) 128 | .done( function( data ) { 129 | console.log( data ); 130 | } ); 131 | 132 | model.suggest( { q: '女' } ) 133 | .done( function( data ) { 134 | console.log( data ); 135 | } ) 136 | .error( function( err ) { 137 | console.log( err ); 138 | } ); 139 | 140 | // 合并请求 141 | model.suggest( { q: '女' } ) 142 | .list( { keyword: 'iphone6' } ) 143 | .getNav( { key: '流行服装' } ) 144 | .done( function( data1, data2, data3 ) { 145 | // 参数顺序与方法调用顺序一致 146 | console.log( data1, data2, data3 ); 147 | } ); 148 | ``` 149 | 150 | ### 用例三 Model混合配置及依赖调用 151 | 152 | * 配置 153 | 154 | ```json 155 | { // 头部配置省略... 156 | "interfaces": [ { 157 | "name": "用户信息查询接口", 158 | "id": "Session.getUser", 159 | "urls": { 160 | "online": "http://taobao.com/getUser.do" 161 | } 162 | }, { 163 | "name": "订单获取接口", 164 | "id": "Order.getOrder", 165 | "urls": { 166 | "online": "http://taobao.com/getOrder" 167 | } 168 | } ] 169 | } 170 | ``` 171 | 172 | * 代码 173 | 174 | ``` js 175 | var model = new ModelProxy( { 176 | getUser: 'Session.getUser', 177 | getMyOrderList: 'Order.getOrder' 178 | } ); 179 | // 先获得用户id,然后再根据id号获得订单列表 180 | model.getUser( { sid: 'fdkaldjfgsakls0322yf8' } ) 181 | .done( function( data ) { 182 | var uid = data.uid; 183 | this.getMyOrderList( { id: uid } ) 184 | .done( function( data ) { 185 | console.log( data ); 186 | } ); 187 | } ); 188 | ``` 189 | 190 | ### 用例四 配置mock代理 191 | * 第一步 在相关接口配置段落中启用mock 192 | 193 | ```json 194 | { 195 | "title": "pad淘宝数据接口定义", 196 | "version": "1.0.0", 197 | "engine": "mockjs", <-- 指定mock引擎 198 | "rulebase": "./interfaceRules/", <-- 指定存放相关mock规则文件的目录 199 | "status": "online", 200 | "interfaces": [ { 201 | "name": "主搜索接口", 202 | "id": "Search.getItems", 203 | "ruleFile": "Search.getItems.rule.json", <-- 指定数据mock规则文件名,如果不配置,则将默认设置为 id + '.rule.json' 204 | "urls": { 205 | "online": "http://s.m.taobao.com/client/search.do", 206 | "prep": "http://s.m.taobao.com/client/search.do", 207 | "daily": "http://daily.taobao.net/client/search.do" 208 | }, 209 | status: 'mock' <-- 启用mock状态,覆盖全局status 210 | } ] 211 | } 212 | ``` 213 | 214 | * 第二步 添加接口对应的规则文件到ruleBase(./interfaceRules/)指定的文件夹。mock数据规则请参考 [http://mockjs.com]。 215 | 启动程序后,ModelProxy即返回相关mock数据。 216 | 217 | 218 | ### 用例五 使用ModelProxy拦截请求 219 | 220 | ```js 221 | var app = require( 'connect' )(); 222 | var ModelProxy = require( 'modelproxy' ); 223 | ModelProxy.init( './interface_sample.json' ); 224 | 225 | // 指定需要拦截的路径 226 | app.use( '/model', ModelProxy.Interceptor ); 227 | 228 | // 此时可直接通过浏览器访问 /model/[interfaceid] 调用相关接口(如果该接口定义中配置了 intercepted = false, 则无法访问) 229 | ``` 230 | 231 | ### 用例六 在浏览器端使用ModelProxy 232 | * 第一步 按照用例二配置接口文件 233 | 234 | * 第二步 按照用例五 启用拦截功能 235 | 236 | * 第三步 在浏览器端使用ModelProxy 237 | 238 | ```html 239 | 240 | 241 | ``` 242 | 243 | ```html 244 | 267 | ``` 268 | 269 | ### 用例七 代理带cookie的请求并且回写cookie (注:请求是否需要带cookie或者回写取决于接口提供者) 270 | 271 | * 关键代码(app 由express创建) 272 | 273 | ```js 274 | app.get( '/getMycart', function( req, res ) { 275 | var cookie = req.headers.cookie; 276 | var cart = ModelProxy.create( 'Cart.*' ); 277 | cart.getMyCart() 278 | // 在调用done之前带上cookie 279 | .withCookie( cookie ) 280 | // done 回调函数中最后一个参数总是需要回写的cookie,不需要回写时可以忽略 281 | .done( function( data , setCookies ) { 282 | // 回写cookie 283 | res.setHeader( 'Set-Cookie', setCookies ); 284 | res.send( data ); 285 | }, function( err ) { 286 | res.send( 500, err ); 287 | } ); 288 | } ); 289 | 290 | ``` 291 | 292 | # 配置文件详解 293 | --- 294 | 295 | ``` js 296 | { 297 | "title": "pad淘宝项目数据接口集合定义", // [必填][string] 接口文档标题 298 | "version": "1.0.0", // [必填][string] 版本号 299 | "engine": "river-mock", // [选填][string] mock 引擎,取值可以是river-mock 和mockjs。不需要mock数据时可以不配置 300 | "rulebase": "./interfaceRules/", // [选填][string] mock规则文件夹路径。不需要mock数据时可以不配置。 301 | // 默认会设置为与本配置文件同级别的文件夹下名位 interfaceRules的文件夹 302 | "status": "online", // [必填][string] 全局代理状态,取值只能是 interface.urls中出现过的键值或者mock 303 | "interfaces": [ { 304 | "name": "获取购物车信息", // [选填][string] 接口名称 生成文档有用 305 | "desc": "接口负责人", // [选填][string] 接口描述 生成文档有用 306 | "version": "0.0.1", // [选填][string] 接口版本号 发送请求时会带上版本号字段 307 | "id": "cart.getCart", // [必填][string] 接口ID,必须由英文单词+点号组成 308 | "urls": { // [如果ruleFile不存在, 则必须有一个地址存在][object] 可供切换的url集合 309 | "online": "http://url1", // 线上地址 310 | "prep": "http://url2", // 预发地址 311 | "daily": "http://url3", // 日常地址 312 | }, 313 | "ruleFile": "cart.getCart.rule.json",// [选填][string] 对应的数据规则文件,当Proxy Mock状态开启时回返回mock数据, 314 | // 不配置时默认为id + ".rule.json"。 315 | "isRuleStatic": true, // [选填][boolean] 数据规则文件是否为静态,即在开启mock状态时,程序会将ruleFile 316 | // 按照静态文件读取, 而非解析该规则文件生成数据,默认为false 317 | "status": "online", // [选填][string] 当前代理状态,可以是urls中的某个键值(online, prep, daily)或者mock 318 | // 或mockerr。如果不填,则代理状态依照全局设置的代理状态;如果设置为mock,则返回ruleFile中定义 319 | // response 内容;如果设置为mockerr,则返回ruleFile中定义的responseError内容。 320 | "method": "post", // [选填][string] 请求方式,取值post|get 默认get 321 | "dataType": "json", // [选填][string] 返回的数据格式, 取值 json|text, 默认为json 322 | "isCookieNeeded": true, // [选填][boolean] 是否需要传递cookie 默认false 323 | "encoding": "utf8" // [选填][string] 代理的数据源编码类型。取值可以是常用编码类型'utf8', 'gbk', 324 | // 'gb2312' 或者 'raw' 如果设置为raw则直接返回2进制buffer,默认为utf8。 325 | // 注意,不论数据源原来为何种编码,代理之后皆以utf8编码输出。 326 | "timeout": 5000, // [选填][number] 延时设置,默认10000 327 | "intercepted": true // [选填][boolean] 是否拦截请求,默认为true 328 | // format // 未完待续 329 | // filter... // 未完待续 330 | }, { 331 | ... 332 | } ], 333 | combo: { 334 | // 未完待续 335 | } 336 | } 337 | ``` 338 | 339 | # API 340 | --- 341 | ### ModelProxy 对象创建方式 342 | 343 | * 直接new 344 | 345 | ```js 346 | var model = new ModelProxy( profile ); 347 | 348 | ``` 349 | 350 | * 工厂创建 351 | 352 | ```js 353 | var model = ModelProxy.create( profile ); 354 | ``` 355 | 356 | ### 创建ModelProxy对象时指定的 profile 相关形式 357 | * 接口ID 生成的对象会取ID最后'.'号后面的单词作为方法名 358 | 359 | ```js 360 | ModelProxy.create( 'Search.getItem' ); 361 | ``` 362 | 363 | * 键值JSON对象 自定义方法名: 接口ID 364 | 365 | ```js 366 | ModelProxy.create( { 367 | getName: 'Session.getUserName', 368 | getMyCarts: 'Cart.getCarts' 369 | } ); 370 | ``` 371 | 372 | * 数组形式 取最后 . 号后面的单词作为方法名 373 | 下例中生成的方法调用名依次为: Cart_getItem, getItem, suggest, getName 374 | 375 | ```js 376 | ModelProxy.create( [ 'Cart.getItem', 'Search.getItem', 'Search.suggest', 'Session.User.getName' ] ); 377 | 378 | ``` 379 | 380 | * 前缀形式 (推荐使用) 381 | 382 | ```js 383 | ModelProxy.create( 'Search.*' ); 384 | ``` 385 | 386 | ### ModelProxy对象方法 387 | 388 | * .method( params ) 389 | method为创建model时动态生成,参数 params{Object}, 为请求接口所需要的参数键值对。 390 | 391 | * .done( callback, errCallback ) 392 | 接口调用完成函数,callback函数的参数与done之前调用的方法请求结果保持一致.最后一个参数为请求回写的cookie。callback函数中的 this 指向ModelProxy对象本身,方便做进一步调用。errCallback 即出错回调函数(可能会被调用多次)。 393 | 394 | * .withCookie( cookies ) 395 | 如果接口需要提供cookie才能返回数据,则调用此方法来设置请求的cookie{String} (如何使用请查看用例七) 396 | 397 | * .error( errCallback ) 398 | 指定全局调用出错处理函数, errCallback 的参数为Error对象。 399 | 400 | 401 | # Mock 功能相关说明 402 | --- 403 | ### rule.json 文件 404 | 当mock状态开启时,mock引擎会读取与接口定义相对应的rule.json规则文件,生成相应的数据。该文件应该位于interface.json配置文件中 405 | ruleBase字段所指定的文件夹中。 (建议该文件夹与interface配置文件同级) 406 | 407 | 408 | ### rule.json 文件样式 409 | 410 | ```js 411 | { 412 | "request": { // 请求参数列表 413 | "参数名1": "规则一", // 具体规则取决于采用何种引擎 414 | "参数名2": "规则二", 415 | ... 416 | }, 417 | "response": 响应内容规则, // 响应内容规则取决于采用何种引擎 418 | "responseError": 响应失败规则 // 响应内容规则取决于采用何种引擎 419 | } 420 | 421 | ``` 422 | 423 | ## [Test Coverage] 424 | --- 425 | 426 | **Overview: `96%` coverage `272` SLOC** 427 | 428 | [modelproxy.js](lib/modelproxy.js) : `98%` coverage `57` SLOC 429 | 430 | [interfacemanager.js](lib/interfacemanager.js): `98%` coverage `76` SLOC 431 | 432 | [proxyfactory](lib/proxyfactory.js) : `93%` coverage `139` SLOC 433 | 434 | ======= 435 | >>>>>>> 8ce533e0358055a823c470a608996e654b00682e 436 | 437 | 438 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require( './lib/modelproxy' ); -------------------------------------------------------------------------------- /lib/interfacemanager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * InterfaceManager 3 | * This Class is provided to parse the interface configuration file so that 4 | * the Proxy class can easily access the structure of the configuration. 5 | * @author ShanFan 6 | * @created 24-3-2014 7 | **/ 8 | 9 | var fs = require( 'fs' ); 10 | 11 | /** 12 | * InterfaceManager 13 | * @param {String|Object} path The file path of inteface configuration or the interface object 14 | */ 15 | function InterfaceManager( path ) { 16 | this._path = path; 17 | 18 | // {Object} Interface Mapping, The key is interface id and 19 | // the value is a json profile for this interface. 20 | this._interfaceMap = {}; 21 | 22 | // {Object} A interface Mapping for client, the key is interface id and 23 | // the value is a json profile for this interface. 24 | this._clientInterfaces = {}; 25 | 26 | // {String} The path of rulebase where the interface rules is stored. This value will be override 27 | // if user specified the path of rulebase in interface.json. 28 | this._rulebase = typeof path === 'string' ? path.replace( /\/[^\/]*$/, '/interfaceRules' ) : ''; 29 | 30 | typeof path === 'string' 31 | ? this._loadProfilesFromPath( path ) 32 | : this._loadProfiles( path ); 33 | } 34 | 35 | // InterfaceManager prototype 36 | InterfaceManager.prototype = { 37 | 38 | // @throws errors 39 | _loadProfilesFromPath: function( path ) { 40 | console.info( 'Loading interface profiles.\nPath = ', path ); 41 | 42 | try { 43 | var profiles = fs.readFileSync( path, { encoding: 'utf8' } ); 44 | } catch ( e ) { 45 | throw new Error( 'Fail to load interface profiles.' + e ); 46 | } 47 | try { 48 | profiles = JSON.parse( profiles ); 49 | } catch( e ) { 50 | throw new Error( 'Interface profiles has syntax error:' + e ); 51 | } 52 | this._loadProfiles( profiles ); 53 | }, 54 | 55 | _loadProfiles: function( profiles ) { 56 | if ( !profiles ) return; 57 | console.info( 'Title:', profiles.title, 'Version:', profiles.version ); 58 | 59 | this._rulebase = profiles.rulebase 60 | ? ( profiles.rulebase || './' ).replace(/\/$/, '') 61 | : this._rulebase; 62 | 63 | // {String} The mock engine name. 64 | this._engine = profiles.engine || 'mockjs'; 65 | 66 | if ( profiles.status === undefined ) { 67 | throw new Error( 'There is no status specified in interface configuration!' ); 68 | } 69 | 70 | // {String} The interface status in using. 71 | this._status = profiles.status; 72 | 73 | var interfaces = profiles.interfaces || []; 74 | for ( var i = interfaces.length - 1; i >= 0; i-- ) { 75 | this._addProfile( interfaces[i] ) 76 | && console.info( 'Interface[' + interfaces[i].id + '] is loaded.' ); 77 | } 78 | }, 79 | getProfile: function( interfaceId ) { 80 | return this._interfaceMap[ interfaceId ]; 81 | }, 82 | getClientInterfaces: function() { 83 | return this._clientInterfaces; 84 | }, 85 | // @throws errors 86 | getRule: function( interfaceId ) { 87 | if ( !interfaceId || !this._interfaceMap[ interfaceId ] ) { 88 | throw new Error( 'The interface profile ' + interfaceId + " is not found." ); 89 | } 90 | path = this._interfaceMap[ interfaceId ].ruleFile; 91 | if ( !fs.existsSync( path ) ) { 92 | throw new Error( 'The rule file is not existed.\npath = ' + path ); 93 | } 94 | try { 95 | var rulefile = fs.readFileSync( path, { encoding: 'utf8' } ); 96 | } catch ( e ) { 97 | throw new Error( 'Fail to read rulefile of path ' + path ); 98 | } 99 | try { 100 | return JSON.parse( rulefile ); 101 | } catch( e ) { 102 | throw new Error( 'Rule file has syntax error. ' + e + '\npath=' + path ); 103 | } 104 | }, 105 | getEngine: function() { 106 | return this._engine; 107 | }, 108 | getStatus: function( name ) { 109 | return this._status; 110 | }, 111 | // @return Array 112 | getInterfaceIdsByPrefix: function( pattern ) { 113 | if ( !pattern ) return []; 114 | var ids = [], map = this._interfaceMap, len = pattern.length; 115 | for ( var id in map ) { 116 | if ( id.slice( 0, len ) == pattern ) { 117 | ids.push( id ); 118 | } 119 | } 120 | return ids; 121 | }, 122 | 123 | isProfileExisted: function( interfaceId ) { 124 | return !!this._interfaceMap[ interfaceId ]; 125 | }, 126 | _addProfile: function( prof ) { 127 | if ( !prof || !prof.id ) { 128 | console.error( "Can not add interface profile without id!" ); 129 | return false; 130 | } 131 | if ( !/^((\w+\.)*\w+)$/.test( prof.id ) ) { 132 | console.error( "Invalid id: " + prof.id ); 133 | return false; 134 | } 135 | if ( this.isProfileExisted( prof.id ) ) { 136 | console.error( "Can not repeat to add interface [" + prof.id 137 | + "]! Please check your interface configuration file!" ); 138 | return false; 139 | } 140 | 141 | prof.ruleFile = this._rulebase + '/' 142 | + ( prof.ruleFile || ( prof.id + ".rule.json" ) ); 143 | 144 | if ( !this._isUrlsValid( prof.urls ) 145 | && !fs.existsSync( prof.ruleFile ) ) { 146 | console.error( 'Profile is deprecated:\n', 147 | prof, '\nNo urls is configured and No ruleFile is available' ); 148 | return false; 149 | } 150 | if (!( prof.status in prof.urls || prof.status === 'mock' 151 | || prof.status === 'mockerr')) { 152 | prof.status = this._status; 153 | } 154 | 155 | prof.method = { POST: 'POST', GET:'GET' } 156 | [ (prof.method || 'GET').toUpperCase() ]; 157 | prof.dataType = { json: 'json', text: 'text', jsonp: 'jsonp' } 158 | [ (prof.dataType || 'json').toLowerCase() ]; 159 | prof.isRuleStatic = !!prof.isRuleStatic || false; 160 | prof.isCookieNeeded = !!prof.isCookieNeeded || false; 161 | prof.signed = !!prof.signed || false; 162 | prof.timeout = prof.timeout || 10000; 163 | 164 | // prof.format 165 | // prof.filter = ... 166 | this._interfaceMap[ prof.id ] = prof; 167 | 168 | this._clientInterfaces[ prof.id ] = { 169 | id: prof.id, 170 | method: prof.method, 171 | dataType: prof.dataType 172 | }; 173 | 174 | return true; 175 | }, 176 | _isUrlsValid: function( urls ) { 177 | if ( !urls ) return false; 178 | for ( var i in urls ) { 179 | return true; 180 | } 181 | return false; 182 | } 183 | }; 184 | 185 | module.exports = InterfaceManager; -------------------------------------------------------------------------------- /lib/modelproxy-client.js: -------------------------------------------------------------------------------- 1 | KISSY.add( 'modelproxy', function ( S, IO ) { 2 | function Proxy( options ) { 3 | this._opt = options; 4 | } 5 | Proxy.prototype = { 6 | request: function( params, callback, errCallback ) { 7 | IO( { 8 | url: Proxy.base + '/' + this._opt.id, 9 | data: params, 10 | type: this._opt.method, 11 | dataType: this._opt.dataType, 12 | success: callback, 13 | error: errCallback 14 | } ); 15 | }, 16 | getOptions: function() { 17 | return this._opt; 18 | } 19 | }; 20 | 21 | Proxy.objects = {}; 22 | 23 | Proxy.create = function( id ) { 24 | if ( this.objects[ id ] ) { 25 | return this.objects[ id ]; 26 | } 27 | var options = this._interfaces[ id ]; 28 | if ( !options ) { 29 | throw new Error( 'No such interface id defined: ' 30 | + id + ', please check your interface configuration file' ); 31 | } 32 | return this.objects[ id ] = new this( options ); 33 | }, 34 | 35 | Proxy.configBase = function( base ) { 36 | if ( this.base ) return; 37 | this.base = ( base || '' ).replace( /\/$/, '' ); 38 | var self = this; 39 | // load interfaces definition. 40 | IO( { 41 | url: this.base + '/$interfaces', 42 | async: false, 43 | type: 'get', 44 | dataType: 'json', 45 | success: function( interfaces ) { 46 | self.config( interfaces ); 47 | }, 48 | error: function( err ) { 49 | throw err; 50 | } 51 | } ); 52 | }; 53 | 54 | Proxy.config = function( interfaces ) { 55 | this._interfaces = interfaces; 56 | }; 57 | 58 | Proxy.getInterfaceIdsByPrefix = function( pattern ) { 59 | if ( !pattern ) return []; 60 | var ids = [], map = this._interfaces, len = pattern.length; 61 | for ( var id in map ) { 62 | if ( id.slice( 0, len ) == pattern ) { 63 | ids.push( id ); 64 | } 65 | } 66 | return ids; 67 | }; 68 | 69 | function ModelProxy( profile ) { 70 | if ( !profile ) return; 71 | 72 | if ( typeof profile === 'string' ) { 73 | if ( /^(\w+\.)+\*$/.test( profile ) ) { 74 | profile = Proxy 75 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) ); 76 | 77 | } else { 78 | profile = [ profile ]; 79 | } 80 | } 81 | if ( profile instanceof Array ) { 82 | var prof = {}, methodName; 83 | for ( var i = profile.length - 1; i >= 0; i-- ) { 84 | methodName = profile[ i ]; 85 | methodName = methodName 86 | .substring( methodName.lastIndexOf( '.' ) + 1 ); 87 | if ( !prof[ methodName ] ) { 88 | prof[ methodName ] = profile[ i ]; 89 | 90 | } else { 91 | methodName = profile[ i ].replace( /\./g, '_' ); 92 | prof[ methodName ] = profile[ i ]; 93 | } 94 | } 95 | profile = prof; 96 | } 97 | 98 | for ( var method in profile ) { 99 | this[ method ] = ( function( methodName, interfaceId ) { 100 | var proxy = Proxy.create( interfaceId ); 101 | return function( params ) { 102 | params = params || {}; 103 | if ( !this._queue ) { 104 | this._queue = []; 105 | } 106 | this._queue.push( { 107 | params: params, 108 | proxy: proxy 109 | } ); 110 | return this; 111 | }; 112 | } )( method, profile[ method ] ); 113 | } 114 | } 115 | 116 | ModelProxy.prototype = { 117 | done: function( f, ef ) { 118 | if ( typeof f !== 'function' ) return; 119 | 120 | if ( !this._queue ) { 121 | f.apply( this ); 122 | return; 123 | } 124 | this._sendRequestsParallel( this._queue, f, ef ); 125 | 126 | this._queue = null; 127 | return this; 128 | }, 129 | _sendRequestsParallel: function( queue, callback, errCallback ) { 130 | var args = [], self = this; 131 | 132 | var cnt = queue.length; 133 | 134 | for ( var i = 0; i < queue.length; i++ ) { 135 | ( function( reqObj, k ) { 136 | reqObj.proxy.request( reqObj.params, function( data ) { 137 | args[ k ] = data; 138 | --cnt || callback.apply( self, args ); 139 | }, function( err ) { 140 | errCallback = errCallback || self._errCallback; 141 | if ( typeof errCallback === 'function' ) { 142 | errCallback( err ); 143 | 144 | } else { 145 | console.error( 'Error occured when sending request =' 146 | , reqObj.proxy.getOptions(), '\nCaused by:\n', err ); 147 | } 148 | } ); 149 | } )( queue[i], i ); 150 | } 151 | }, 152 | error: function( f ) { 153 | this._errCallback = f; 154 | } 155 | }; 156 | 157 | ModelProxy.create = function( profile ) { 158 | return new this( profile ); 159 | }; 160 | 161 | ModelProxy.configBase = function( path ) { 162 | Proxy.configBase( path ); 163 | }; 164 | 165 | return ModelProxy; 166 | 167 | }, { requires: ['io'] } ); -------------------------------------------------------------------------------- /lib/modelproxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ModelProxy 3 | * As named, this class is provided to model the proxy. 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | **/ 7 | 8 | // Dependencies 9 | var InterfaceManager = require( './interfacemanager' ) 10 | , ProxyFactory = require( './proxyfactory' ); 11 | 12 | /** 13 | * ModelProxy Constructor 14 | * @param {Object|Array|String} profile. This profile describes what the model looks 15 | * like. eg: 16 | * profile = { 17 | * getItems: 'Search.getItems', 18 | * getCart: 'Cart.getCart' 19 | * } 20 | * profile = ['Search.getItems', 'Cart.getCart'] 21 | * profile = 'Search.getItems' 22 | * profile = 'Search.*' 23 | */ 24 | function ModelProxy( profile ) { 25 | if ( !profile ) return; 26 | var that = this; 27 | if ( typeof profile === 'string' ) { 28 | 29 | // Get ids via prefix pattern like 'packageName.*' 30 | if ( /^(\w+\.)+\*$/.test( profile ) ) { 31 | profile = ProxyFactory 32 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) ); 33 | 34 | } else { 35 | profile = [ profile ]; 36 | } 37 | } 38 | if ( profile instanceof Array ) { 39 | var prof = {}, methodName; 40 | for ( var i = profile.length - 1; i >= 0; i-- ) { 41 | methodName = profile[ i ]; 42 | methodName = methodName 43 | .substring( methodName.lastIndexOf( '.' ) + 1 ); 44 | if ( !prof[ methodName ] ) { 45 | prof[ methodName ] = profile[ i ]; 46 | 47 | // The method name is duplicated, so the full interface id is set 48 | // as the method name. 49 | } else { 50 | methodName = profile[ i ].replace( /\./g, '_' ); 51 | prof[ methodName ] = profile[ i ]; 52 | } 53 | } 54 | profile = prof; 55 | } 56 | that.profile = profile; //wmm 57 | // Construct the model following the profile 58 | for ( var method in profile ) { 59 | this[ method ] = ( function( methodName, interfaceId ) { 60 | var proxy = ProxyFactory.create( interfaceId ); 61 | return function( params, expire ) { 62 | params = params || {}; 63 | 64 | if ( !that._queue ) { 65 | that._queue = []; 66 | } 67 | // Push this method call into request queue. Once the done method 68 | // is called, all requests in this queue will be sent. 69 | that._queue.push( { 70 | params: params, 71 | proxy: proxy, 72 | expire: expire || 0 73 | } ); 74 | return this; 75 | }; 76 | } )( method, profile[ method ] ); 77 | // this._addMethod( method, profile[ method ] ); 78 | } 79 | } 80 | 81 | ModelProxy.prototype = { 82 | done: function( f, ef ) { 83 | if ( typeof f !== 'function' ) return; 84 | 85 | // No request pushed in _queue, so callback directly and return. 86 | if ( !this._queue ) { 87 | f.apply( this ); 88 | return; 89 | } 90 | // Send requests parallel 91 | this._sendRequestsParallel( this._queue, f, ef ); 92 | 93 | // Clear queue 94 | this._queue = null; 95 | return this; 96 | }, 97 | // 针对动态调用方法,做个封装 98 | invoke:function(map,key){ 99 | var intfs = (map[key] && map[key].interfaces) || []; 100 | for (var i = 0; i < intfs.length; i++) { 101 | var intf = intfs[i]; 102 | if(!intf.name){ 103 | continue; 104 | }else{ 105 | var proxy = ProxyFactory.create(this.profile[intf.name]); 106 | var params = intf.params || []; 107 | var expire = intf.expire || 0; 108 | if ( !this._queue ) { 109 | this._queue = []; 110 | } 111 | this._queue.push( { 112 | params: params, 113 | proxy: proxy, 114 | expire: expire 115 | }); 116 | } 117 | } 118 | 119 | return this; 120 | }, 121 | 122 | withCookie: function( cookie ) { 123 | this._cookies = cookie; 124 | return this; 125 | }, 126 | 127 | _sendRequestsParallel: function( queue, callback, errCallback ) { 128 | 129 | // The final data array 130 | var args = [], setcookies = [], self = this; 131 | 132 | // Count the number of callback; 133 | var cnt = queue.length; 134 | 135 | // Send each request 136 | for ( var i = 0; i < queue.length; i++ ) { 137 | ( function( reqObj, k, cookie ) { 138 | //console.log("reqObj:"+JSON.stringify(reqObj)); 139 | 140 | reqObj.proxy.request( reqObj, function( data, setcookie ) { 141 | // console.log(113,reqObj.params); 142 | // fill data for callback 143 | args[ k ] = data; 144 | // concat setcookie for cookie rewriting 145 | setcookies = setcookies.concat( setcookie ); 146 | args.push( setcookies ); 147 | 148 | // push the set-cookies as the last parameter for the callback function. 149 | --cnt || callback.apply( self, args.push( setcookies ) && args ); 150 | 151 | }, function( err ) { 152 | errCallback = errCallback || self._errCallback; 153 | if ( typeof errCallback === 'function' ) { 154 | errCallback( err ); 155 | 156 | } else { 157 | console.error( 'Error occured when sending request =' 158 | , reqObj.params, '\nCaused by:\n', err ); 159 | } 160 | }, cookie ); // request with cookie. 161 | 162 | } )( queue[i], i, self._cookies ); 163 | } 164 | // clear cookie of this request. 165 | self._cookies = undefined; 166 | }, 167 | error: function( f ) { 168 | this._errCallback = f; 169 | } 170 | }; 171 | 172 | /** 173 | * ModelProxy.init 174 | * @param {String} path The path refers to the interface configuration file. 175 | */ 176 | ModelProxy.init = function( path ) { 177 | ProxyFactory.use( new InterfaceManager( path ) ); 178 | }; 179 | 180 | 181 | ModelProxy.create = function( profile ) { 182 | return new this( profile ); 183 | }; 184 | 185 | ModelProxy.Interceptor = function( req, res ) { 186 | // todo: need to handle the case that the request url is multiple 187 | // interfaces combined which configured in interface.json. 188 | ProxyFactory.Interceptor( req, res ); 189 | }; 190 | 191 | module.exports = ModelProxy; 192 | -------------------------------------------------------------------------------- /lib/modelproxy.js.bak: -------------------------------------------------------------------------------- 1 | /** 2 | * ModelProxy 3 | * As named, this class is provided to model the proxy. 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | **/ 7 | 8 | // Dependencies 9 | var InterfaceManager = require( './interfacemanager' ) 10 | , ProxyFactory = require( './proxyfactory' ); 11 | 12 | /** 13 | * ModelProxy Constructor 14 | * @param {Object|Array|String} profile. This profile describes what the model looks 15 | * like. eg: 16 | * profile = { 17 | * getItems: 'Search.getItems', 18 | * getCart: 'Cart.getCart' 19 | * } 20 | * profile = ['Search.getItems', 'Cart.getCart'] 21 | * profile = 'Search.getItems' 22 | * profile = 'Search.*' 23 | */ 24 | function ModelProxy( profile ) { 25 | if ( !profile ) return; 26 | var that = this; 27 | if ( typeof profile === 'string' ) { 28 | 29 | // Get ids via prefix pattern like 'packageName.*' 30 | if ( /^(\w+\.)+\*$/.test( profile ) ) { 31 | profile = ProxyFactory 32 | .getInterfaceIdsByPrefix( profile.replace( /\*$/, '' ) ); 33 | 34 | } else { 35 | profile = [ profile ]; 36 | } 37 | } 38 | if ( profile instanceof Array ) { 39 | var prof = {}, methodName; 40 | for ( var i = profile.length - 1; i >= 0; i-- ) { 41 | methodName = profile[ i ]; 42 | methodName = methodName 43 | .substring( methodName.lastIndexOf( '.' ) + 1 ); 44 | if ( !prof[ methodName ] ) { 45 | prof[ methodName ] = profile[ i ]; 46 | 47 | // The method name is duplicated, so the full interface id is set 48 | // as the method name. 49 | } else { 50 | methodName = profile[ i ].replace( /\./g, '_' ); 51 | prof[ methodName ] = profile[ i ]; 52 | } 53 | } 54 | profile = prof; 55 | } 56 | that.profile = profile; //wmm 57 | // Construct the model following the profile 58 | for ( var method in profile ) { 59 | this[ method ] = ( function( methodName, interfaceId ) { 60 | var proxy = ProxyFactory.create( interfaceId ); 61 | return function( params ) { 62 | params = params || {}; 63 | 64 | if ( !that._queue ) { 65 | that._queue = []; 66 | } 67 | // Push this method call into request queue. Once the done method 68 | // is called, all requests in this queue will be sent. 69 | that._queue.push( { 70 | params: params, 71 | proxy: proxy 72 | } ); 73 | return this; 74 | }; 75 | } )( method, profile[ method ] ); 76 | // this._addMethod( method, profile[ method ] ); 77 | } 78 | } 79 | 80 | ModelProxy.prototype = { 81 | done: function( f, ef ) { 82 | if ( typeof f !== 'function' ) return; 83 | 84 | // No request pushed in _queue, so callback directly and return. 85 | if ( !this._queue ) { 86 | f.apply( this ); 87 | return; 88 | } 89 | // Send requests parallel 90 | this._sendRequestsParallel( this._queue, f, ef ); 91 | 92 | // Clear queue 93 | this._queue = null; 94 | return this; 95 | }, 96 | // 针对动态调用方法,做个封装 97 | invoke:function(map,key,params){ 98 | var funs = (map[key] && map[key].interfaces) || []; 99 | for(var f = 0; f < funs.length; f++){ 100 | var fname = funs[f]; 101 | var proxy = ProxyFactory.create( this.profile[fname]); 102 | params = params || {}; 103 | if ( !this._queue ) { 104 | this._queue = []; 105 | } 106 | this._queue.push( { 107 | params: params, 108 | proxy: proxy 109 | }); 110 | } 111 | return this; 112 | }, 113 | 114 | withCookie: function( cookie ) { 115 | this._cookies = cookie; 116 | return this; 117 | }, 118 | _sendRequestsParallel: function( queue, callback, errCallback ) { 119 | 120 | // The final data array 121 | var args = [], setcookies = [], self = this; 122 | 123 | // Count the number of callback; 124 | var cnt = queue.length; 125 | 126 | // Send each request 127 | for ( var i = 0; i < queue.length; i++ ) { 128 | ( function( reqObj, k, cookie ) { 129 | console.log("reqObj:"+reqObj); 130 | reqObj.proxy.request( reqObj.params, function( data, setcookie ) { 131 | // console.log(113,reqObj.params); 132 | // fill data for callback 133 | args[ k ] = data; 134 | 135 | // concat setcookie for cookie rewriting 136 | setcookies = setcookies.concat( setcookie ); 137 | args.push( setcookies ); 138 | 139 | // push the set-cookies as the last parameter for the callback function. 140 | --cnt || callback.apply( self, args.push( setcookies ) && args ); 141 | 142 | }, function( err ) { 143 | errCallback = errCallback || self._errCallback; 144 | if ( typeof errCallback === 'function' ) { 145 | errCallback( err ); 146 | 147 | } else { 148 | console.error( 'Error occured when sending request =' 149 | , reqObj.params, '\nCaused by:\n', err ); 150 | } 151 | }, cookie ); // request with cookie. 152 | 153 | } )( queue[i], i, self._cookies ); 154 | } 155 | // clear cookie of this request. 156 | self._cookies = undefined; 157 | }, 158 | error: function( f ) { 159 | this._errCallback = f; 160 | } 161 | }; 162 | 163 | /** 164 | * ModelProxy.init 165 | * @param {String} path The path refers to the interface configuration file. 166 | */ 167 | ModelProxy.init = function( path ) { 168 | ProxyFactory.use( new InterfaceManager( path ) ); 169 | }; 170 | 171 | 172 | ModelProxy.create = function( profile ) { 173 | return new this( profile ); 174 | }; 175 | 176 | ModelProxy.Interceptor = function( req, res ) { 177 | // todo: need to handle the case that the request url is multiple 178 | // interfaces combined which configured in interface.json. 179 | ProxyFactory.Interceptor( req, res ); 180 | }; 181 | 182 | module.exports = ModelProxy; 183 | -------------------------------------------------------------------------------- /lib/proxyfactory.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ProxyFactory, Proxy 3 | * This class is provided to create proxy objects following the configuration 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | */ 7 | 8 | // Dependencies 9 | var fs = require( 'fs' ) 10 | , http = require( 'http' ) 11 | , url = require( 'url' ) 12 | , querystring = require( 'querystring' ) 13 | , iconv = require( 'iconv-lite' ) 14 | , BufferHelper = require( 'bufferhelper' ); 15 | 16 | var InterfacefManager = require( './interfacemanager' ); 17 | 18 | // Instance of InterfaceManager, will be intialized when the proxy.use() is called. 19 | var interfaceManager; 20 | 21 | var STATUS_MOCK = 'mock'; 22 | var STATUS_MOCK_ERR = 'mockerr'; 23 | var ENCODING_RAW = 'raw'; 24 | 25 | // Current Proxy Status 26 | // var CurrentStatus; 27 | 28 | // Proxy constructor 29 | function Proxy( options ) { 30 | // console.log(30,options); 31 | this._opt = options || {}; 32 | this._urls = this._opt.urls || {}; 33 | if ( this._opt.status === STATUS_MOCK || this._opt.status === STATUS_MOCK_ERR ) { 34 | return; 35 | } 36 | var currUrl = this._urls[ this._opt.status ]; 37 | 38 | if ( !currUrl ) { 39 | throw new Error( 'No url can be proxied!' ); 40 | }; 41 | 42 | var urlObj = url.parse( currUrl ); 43 | this._opt.hostname = urlObj.hostname; 44 | this._opt.port = urlObj.port || 80; 45 | this._opt.path = urlObj.path; 46 | this._opt.method = (this._opt.method || 'GET').toUpperCase(); 47 | } 48 | 49 | /** 50 | * use 51 | * @param {InterfaceManager} ifmgr 52 | * @throws errors 53 | */ 54 | Proxy.use = function( ifmgr ) { 55 | 56 | if ( ifmgr instanceof InterfacefManager ) { 57 | interfaceManager = ifmgr; 58 | } else { 59 | throw new Error( 'Proxy can only use instance of InterfacefManager!' ); 60 | } 61 | 62 | this._engineName = interfaceManager.getEngine(); 63 | 64 | return this; 65 | }; 66 | 67 | Proxy.getMockEngine = function() { 68 | if ( this._mockEngine ) { 69 | return this._mockEngine; 70 | } 71 | return this._mockEngine = require( this._engineName ); 72 | }; 73 | 74 | Proxy.getInterfaceIdsByPrefix = function( pattern ) { 75 | return interfaceManager.getInterfaceIdsByPrefix( pattern ); 76 | }; 77 | 78 | // @throws errors 79 | Proxy.getRule = function( interfaceId ) { 80 | return interfaceManager.getRule( interfaceId ); 81 | }; 82 | 83 | // {Object} An object map to store created proxies. The key is interface id 84 | // and the value is the proxy instance. 85 | Proxy.objects = {}; 86 | 87 | // Proxy factory 88 | // @throws errors 89 | Proxy.create = function( interfaceId ) { 90 | if ( !!this.objects[ interfaceId ] ) { 91 | return this.objects[ interfaceId ]; 92 | } 93 | var opt = interfaceManager.getProfile( interfaceId ); 94 | if ( !opt ) { 95 | throw new Error( 'Invalid interface id: ' + interfaceId ); 96 | } 97 | return this.objects[ interfaceId ] = new this( opt ); 98 | }; 99 | 100 | Proxy.prototype = { 101 | addRedisCache:function(reqObj,data){ 102 | /* 讲数据添加到redis缓存 */ 103 | var _key = reqObj.proxy._opt.id, 104 | _expire = reqObj.expire, 105 | _value = data; 106 | }, 107 | 108 | checkRedisCache:function(reqObj, requestCallback,successCallback,cookie){ 109 | if(!!!reqObj.expire || reqObj.expire<=0){ 110 | return requestCallback && requestCallback(); 111 | } 112 | 113 | var _key = reqObj.proxy._opt.id; 114 | }, 115 | 116 | request: function( reqObj, callback, errCallback, cookie ) { 117 | // if ( typeof callback !== 'function' ) { 118 | // console.error( 'No callback function for request = ', this._opt ); 119 | // return; 120 | // } 121 | var params = reqObj.params; 122 | 123 | if ( this._opt.isCookieNeeded === true && cookie === undefined ) { 124 | throw new Error( 'This request is cookie needed, you must set a cookie for it before request. id = ' + this._opt.id ); 125 | } 126 | 127 | errCallback = typeof errCallback !== 'function' 128 | ? function( e ) { console.error( e ); } 129 | : errCallback; 130 | 131 | if ( this._opt.status === STATUS_MOCK 132 | || this._opt.status === STATUS_MOCK_ERR ) { 133 | this._mockRequest( params, callback, errCallback ); 134 | return; 135 | } 136 | var self = this; 137 | // self._opt.hostname="new.juxiangyou.cm"; 138 | var options = { 139 | hostname: self._opt.hostname, 140 | port: self._opt.port, 141 | path: self._opt.path, 142 | method: self._opt.method, 143 | headers: { 'Cookie': cookie } 144 | }; 145 | var querystring = self._queryStringify( params ); 146 | console.log("querystring:"+JSON.stringify(querystring)); 147 | 148 | // // Set cookie 149 | // options.headers = { 150 | // 'Cookie': cookie 151 | // } 152 | cookie = cookie || []; 153 | cookie.concat["name=wmmang"]; 154 | if ( self._opt.method === 'POST' ) { 155 | options.headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded'; 156 | options.headers[ 'Content-Length' ] = querystring.length; 157 | options.headers[ 'Cookie' ] = cookie; 158 | 159 | } else if ( self._opt.method === 'GET' ) { 160 | options.path += '?' + querystring; 161 | options.headers[ 'Content-Type' ] = "application/json;charset=UTF-8"; 162 | options.headers[ 'Cookie' ] = cookie; 163 | } 164 | 165 | 166 | for(var key in options.headers){ 167 | console.log("key:"+key+",value:"+options.headers[key]); 168 | } 169 | 170 | //检测是否存在redis缓存 171 | self.checkRedisCache(reqObj,function(){ 172 | var req = http.request( options, function( res ) { 173 | 174 | var timer = setTimeout( function() { 175 | errCallback( new Error( 'timeout' ) ); 176 | }, self._opt.timeout || 5000 ); 177 | 178 | var bufferHelper = new BufferHelper(); 179 | 180 | res.on( 'data', function( chunk ) { 181 | bufferHelper.concat( chunk ); 182 | } ); 183 | res.on( 'end', function() { 184 | var buffer = bufferHelper.toBuffer(); 185 | try { 186 | var result = self._opt.encoding === ENCODING_RAW 187 | ? buffer 188 | : ( self._opt.dataType !== 'json' 189 | ? iconv.fromEncoding( buffer, self._opt.encoding ) 190 | : JSON.parse( iconv.fromEncoding( buffer, self._opt.encoding ) ) ); 191 | } catch ( e ) { 192 | clearTimeout( timer ); 193 | errCallback( new Error( "The result has syntax error. " + e ) ); 194 | return; 195 | } 196 | clearTimeout( timer ); 197 | //get data sucessfully 198 | 199 | //add redis cache 200 | var _key = reqObj.proxy._opt.id || ""; 201 | 202 | console.log("\033[32mfrom request:"+_key+"\033[0m"); 203 | 204 | reqObj.expire && reqObj.expire>0 && self.addRedisCache(reqObj,result); 205 | 206 | callback( result, res.headers['set-cookie'] ); 207 | 208 | } ); 209 | 210 | } ); 211 | 212 | self._opt.method !== 'POST' || req.write( querystring ); 213 | 214 | req.on( 'error', function( e ) { 215 | errCallback( e ); 216 | } ); 217 | 218 | req.end(); 219 | 220 | },callback,cookie); 221 | 222 | }, 223 | getOption: function( name ) { 224 | return this._opt[ name ]; 225 | }, 226 | _queryStringify: function( params ) { 227 | if ( !params || typeof params === 'string' ) { 228 | return params || ''; 229 | } else if ( params instanceof Array ) { 230 | return params.join( '&' ); 231 | } 232 | var qs = [], val; 233 | for ( var i in params ) { 234 | val = typeof params[i] === 'object' 235 | ? JSON.stringify( params[ i ] ) 236 | : params[ i ]; 237 | qs.push( i + '=' + encodeURIComponent(val) ); 238 | } 239 | return qs.join( '&' ); 240 | }, 241 | _mockRequest: function( params, callback, errCallback ) { 242 | try { 243 | var engine = Proxy.getMockEngine(); 244 | if ( !this._rule ) { 245 | this._rule = Proxy.getRule( this._opt.id ); 246 | } 247 | if ( this._opt.isRuleStatic ) { 248 | callback( this._opt.status === STATUS_MOCK 249 | ? this._rule.response 250 | : this._rule.responseError ); 251 | return; 252 | } 253 | 254 | // special code for river-mock 255 | if ( Proxy._engineName === 'river-mock' ) { 256 | callback( engine.spec2mock( this._rule ) ); 257 | return; 258 | } 259 | // special code for mockjs 260 | callback( this._opt.status === STATUS_MOCK 261 | ? engine.mock( this._rule.response ) 262 | : engine.mock( this._rule.responseError ) 263 | ); 264 | } catch ( e ) { 265 | errCallback( e ); 266 | } 267 | }, 268 | interceptRequest: function( req, res ) { 269 | if ( this._opt.status === STATUS_MOCK 270 | || this._opt.status === STATUS_MOCK_ERR ) { 271 | this._mockRequest( {}, function( data ) { 272 | res.end( typeof data === 'string' ? data : JSON.stringify( data ) ); 273 | }, function( e ) { 274 | // console.error( 'Error ocurred when mocking data', e ); 275 | res.statusCode = 500; 276 | res.end( 'Error orccured when mocking data' ); 277 | } ); 278 | return; 279 | } 280 | var self = this; 281 | var options = { 282 | hostname: self._opt.hostname, 283 | port: self._opt.port, 284 | path: self._opt.path + '?' + req.url.replace( /^[^\?]*\?/, '' ), 285 | method: self._opt.method, 286 | headers: req.headers 287 | }; 288 | 289 | options.headers.host = self._opt.hostname; 290 | // delete options.headers.referer; 291 | // delete options.headers['x-requested-with']; 292 | // delete options.headers['connection']; 293 | // delete options.headers['accept']; 294 | delete options.headers['accept-encoding']; 295 | 296 | var req2 = http.request( options, function( res2 ) { 297 | var bufferHelper = new BufferHelper(); 298 | 299 | res2.on( 'data', function( chunk ) { 300 | bufferHelper.concat( chunk ); 301 | } ); 302 | res2.on( 'end', function() { 303 | var buffer = bufferHelper.toBuffer(); 304 | var result; 305 | try { 306 | result = self._opt.encoding === ENCODING_RAW 307 | ? buffer 308 | : iconv.fromEncoding( buffer, self._opt.encoding ); 309 | 310 | } catch ( e ) { 311 | res.statusCode = 500; 312 | res.end( e + '' ); 313 | return; 314 | } 315 | res.setHeader( 'Set-Cookie', res2.headers['set-cookie'] ); 316 | res.setHeader( 'Content-Type' 317 | , ( self._opt.dataType === 'json' ? 'application/json' : 'text/html' ) 318 | + ';charset=UTF-8' ); 319 | res.end( result ); 320 | } ); 321 | } ); 322 | 323 | req2.on( 'error', function( e ) { 324 | res.statusCode = 500; 325 | res.end( e + '' ); 326 | } ); 327 | req.on( 'data', function( chunck ) { 328 | req2.write( chunck ); 329 | } ); 330 | req.on( 'end', function() { 331 | req2.end(); 332 | } ); 333 | 334 | } 335 | }; 336 | 337 | var ProxyFactory = Proxy; 338 | 339 | ProxyFactory.Interceptor = function( req, res ) { 340 | var interfaceId = req.url.split( /\?|\// )[1]; 341 | if ( interfaceId === '$interfaces' ) { 342 | var interfaces = interfaceManager.getClientInterfaces(); 343 | res.end( JSON.stringify( interfaces ) ); 344 | return; 345 | } 346 | 347 | try { 348 | proxy = this.create( interfaceId ); 349 | if ( proxy.getOption( 'intercepted' ) === false ) { 350 | throw new Error( 'This url is not intercepted by proxy.' ); 351 | } 352 | } catch ( e ) { 353 | res.statusCode = 404; 354 | res.end( 'Invalid url: ' + req.url + '\n' + e ); 355 | return; 356 | } 357 | proxy.interceptRequest( req, res ); 358 | }; 359 | 360 | module.exports = ProxyFactory; 361 | 362 | -------------------------------------------------------------------------------- /lib/proxyfactory.js_redis: -------------------------------------------------------------------------------- 1 | /** 2 | * ProxyFactory, Proxy 3 | * This class is provided to create proxy objects following the configuration 4 | * @author ShanFan 5 | * @created 24-3-2014 6 | */ 7 | 8 | // Dependencies 9 | var fs = require( 'fs' ) 10 | , http = require( 'http' ) 11 | , url = require( 'url' ) 12 | , querystring = require( 'querystring' ) 13 | , iconv = require( 'iconv-lite' ) 14 | , BufferHelper = require( 'bufferhelper' ); 15 | 16 | var InterfacefManager = require( './interfacemanager' ); 17 | 18 | // Instance of InterfaceManager, will be intialized when the proxy.use() is called. 19 | var interfaceManager; 20 | 21 | var STATUS_MOCK = 'mock'; 22 | var STATUS_MOCK_ERR = 'mockerr'; 23 | var ENCODING_RAW = 'raw'; 24 | 25 | // Current Proxy Status 26 | // var CurrentStatus; 27 | 28 | // Proxy constructor 29 | function Proxy( options ) { 30 | // console.log(30,options); 31 | this._opt = options || {}; 32 | this._urls = this._opt.urls || {}; 33 | if ( this._opt.status === STATUS_MOCK || this._opt.status === STATUS_MOCK_ERR ) { 34 | return; 35 | } 36 | var currUrl = this._urls[ this._opt.status ]; 37 | 38 | if ( !currUrl ) { 39 | throw new Error( 'No url can be proxied!' ); 40 | }; 41 | 42 | var urlObj = url.parse( currUrl ); 43 | this._opt.hostname = urlObj.hostname; 44 | this._opt.port = urlObj.port || 80; 45 | this._opt.path = urlObj.path; 46 | this._opt.method = (this._opt.method || 'GET').toUpperCase(); 47 | } 48 | 49 | /** 50 | * use 51 | * @param {InterfaceManager} ifmgr 52 | * @throws errors 53 | */ 54 | Proxy.use = function( ifmgr ) { 55 | 56 | if ( ifmgr instanceof InterfacefManager ) { 57 | interfaceManager = ifmgr; 58 | } else { 59 | throw new Error( 'Proxy can only use instance of InterfacefManager!' ); 60 | } 61 | 62 | this._engineName = interfaceManager.getEngine(); 63 | 64 | return this; 65 | }; 66 | 67 | Proxy.getMockEngine = function() { 68 | if ( this._mockEngine ) { 69 | return this._mockEngine; 70 | } 71 | return this._mockEngine = require( this._engineName ); 72 | }; 73 | 74 | Proxy.getInterfaceIdsByPrefix = function( pattern ) { 75 | return interfaceManager.getInterfaceIdsByPrefix( pattern ); 76 | }; 77 | 78 | // @throws errors 79 | Proxy.getRule = function( interfaceId ) { 80 | return interfaceManager.getRule( interfaceId ); 81 | }; 82 | 83 | // {Object} An object map to store created proxies. The key is interface id 84 | // and the value is the proxy instance. 85 | Proxy.objects = {}; 86 | 87 | // Proxy factory 88 | // @throws errors 89 | Proxy.create = function( interfaceId ) { 90 | if ( !!this.objects[ interfaceId ] ) { 91 | return this.objects[ interfaceId ]; 92 | } 93 | var opt = interfaceManager.getProfile( interfaceId ); 94 | if ( !opt ) { 95 | throw new Error( 'Invalid interface id: ' + interfaceId ); 96 | } 97 | return this.objects[ interfaceId ] = new this( opt ); 98 | }; 99 | 100 | Proxy.prototype = { 101 | addRedisCache:function(reqObj,data){ 102 | /* 讲数据添加到redis缓存 */ 103 | var _key = reqObj.proxy._opt.id, 104 | _expire = reqObj.expire, 105 | _value = data; 106 | }, 107 | 108 | checkRedisCache:function(reqObj, requestCallback,successCallback,cookie){ 109 | if(!!!reqObj.expire || reqObj.expire<=0){ 110 | return requestCallback && requestCallback(); 111 | } 112 | 113 | var _key = reqObj.proxy._opt.id; 114 | }, 115 | 116 | request: function( reqObj, callback, errCallback, cookie ) { 117 | // if ( typeof callback !== 'function' ) { 118 | // console.error( 'No callback function for request = ', this._opt ); 119 | // return; 120 | // } 121 | // console.log(106,params); 122 | var params = reqObj.params; 123 | 124 | if ( this._opt.isCookieNeeded === true && cookie === undefined ) { 125 | throw new Error( 'This request is cookie needed, you must set a cookie for it before request. id = ' + this._opt.id ); 126 | } 127 | 128 | errCallback = typeof errCallback !== 'function' 129 | ? function( e ) { console.error( e ); } 130 | : errCallback; 131 | 132 | if ( this._opt.status === STATUS_MOCK 133 | || this._opt.status === STATUS_MOCK_ERR ) { 134 | this._mockRequest( params, callback, errCallback ); 135 | return; 136 | } 137 | var self = this; 138 | var options = { 139 | hostname: self._opt.hostname, 140 | port: self._opt.port, 141 | path: self._opt.path, 142 | method: self._opt.method, 143 | headers: { 'Cookie': cookie } 144 | }; 145 | var querystring = self._queryStringify( params ); 146 | 147 | // // Set cookie 148 | // options.headers = { 149 | // 'Cookie': cookie 150 | // } 151 | cookie = cookie || []; 152 | cookie.concat["name=wmmang"]; 153 | if ( self._opt.method === 'POST' ) { 154 | options.headers[ 'Content-Type' ] = 'application/x-www-form-urlencoded'; 155 | options.headers[ 'Content-Length' ] = querystring.length; 156 | options.headers[ 'Cookie' ] = cookie; 157 | 158 | } else if ( self._opt.method === 'GET' ) { 159 | options.path += '?' + querystring; 160 | options.headers[ 'Content-Type' ] = "application/json;charset=UTF-8"; 161 | options.headers[ 'Cookie' ] = cookie; 162 | } 163 | 164 | 165 | for(var key in options.headers){ 166 | console.log("key:"+key+",value:"+options.headers[key]); 167 | } 168 | 169 | console.log("options:"+JSON.stringify(options)); 170 | //检测是否存在redis缓存 171 | self.checkRedisCache(reqObj,function(){ 172 | var req = http.request( options, function( res ) { 173 | 174 | var timer = setTimeout( function() { 175 | errCallback( new Error( 'timeout' ) ); 176 | }, self._opt.timeout || 5000 ); 177 | 178 | var bufferHelper = new BufferHelper(); 179 | 180 | res.on( 'data', function( chunk ) { 181 | bufferHelper.concat( chunk ); 182 | } ); 183 | res.on( 'end', function() { 184 | var buffer = bufferHelper.toBuffer(); 185 | try { 186 | var result = self._opt.encoding === ENCODING_RAW 187 | ? buffer 188 | : ( self._opt.dataType !== 'json' 189 | ? iconv.fromEncoding( buffer, self._opt.encoding ) 190 | : JSON.parse( iconv.fromEncoding( buffer, self._opt.encoding ) ) ); 191 | } catch ( e ) { 192 | clearTimeout( timer ); 193 | errCallback( new Error( "The result has syntax error. " + e ) ); 194 | return; 195 | } 196 | clearTimeout( timer ); 197 | //get data sucessfully 198 | 199 | //add redis cache 200 | var _key = reqObj.proxy._opt.id || ""; 201 | 202 | console.log("\033[32mfrom request:"+_key+"\033[0m"); 203 | 204 | reqObj.expire && reqObj.expire>0 && self.addRedisCache(reqObj,result); 205 | 206 | callback( result, res.headers['set-cookie'] ); 207 | 208 | } ); 209 | 210 | } ); 211 | 212 | self._opt.method !== 'POST' || req.write( querystring ); 213 | 214 | req.on( 'error', function( e ) { 215 | errCallback( e ); 216 | } ); 217 | 218 | req.end(); 219 | 220 | },callback,cookie); 221 | 222 | }, 223 | getOption: function( name ) { 224 | return this._opt[ name ]; 225 | }, 226 | _queryStringify: function( params ) { 227 | if ( !params || typeof params === 'string' ) { 228 | return params || ''; 229 | } else if ( params instanceof Array ) { 230 | return params.join( '&' ); 231 | } 232 | var qs = [], val; 233 | for ( var i in params ) { 234 | val = typeof params[i] === 'object' 235 | ? JSON.stringify( params[ i ] ) 236 | : params[ i ]; 237 | qs.push( i + '=' + encodeURIComponent(val) ); 238 | } 239 | return qs.join( '&' ); 240 | }, 241 | _mockRequest: function( params, callback, errCallback ) { 242 | try { 243 | var engine = Proxy.getMockEngine(); 244 | if ( !this._rule ) { 245 | this._rule = Proxy.getRule( this._opt.id ); 246 | } 247 | if ( this._opt.isRuleStatic ) { 248 | callback( this._opt.status === STATUS_MOCK 249 | ? this._rule.response 250 | : this._rule.responseError ); 251 | return; 252 | } 253 | 254 | // special code for river-mock 255 | if ( Proxy._engineName === 'river-mock' ) { 256 | callback( engine.spec2mock( this._rule ) ); 257 | return; 258 | } 259 | // special code for mockjs 260 | callback( this._opt.status === STATUS_MOCK 261 | ? engine.mock( this._rule.response ) 262 | : engine.mock( this._rule.responseError ) 263 | ); 264 | } catch ( e ) { 265 | errCallback( e ); 266 | } 267 | }, 268 | interceptRequest: function( req, res ) { 269 | if ( this._opt.status === STATUS_MOCK 270 | || this._opt.status === STATUS_MOCK_ERR ) { 271 | this._mockRequest( {}, function( data ) { 272 | res.end( typeof data === 'string' ? data : JSON.stringify( data ) ); 273 | }, function( e ) { 274 | // console.error( 'Error ocurred when mocking data', e ); 275 | res.statusCode = 500; 276 | res.end( 'Error orccured when mocking data' ); 277 | } ); 278 | return; 279 | } 280 | var self = this; 281 | var options = { 282 | hostname: self._opt.hostname, 283 | port: self._opt.port, 284 | path: self._opt.path + '?' + req.url.replace( /^[^\?]*\?/, '' ), 285 | method: self._opt.method, 286 | headers: req.headers 287 | }; 288 | 289 | options.headers.host = self._opt.hostname; 290 | // delete options.headers.referer; 291 | // delete options.headers['x-requested-with']; 292 | // delete options.headers['connection']; 293 | // delete options.headers['accept']; 294 | delete options.headers['accept-encoding']; 295 | 296 | var req2 = http.request( options, function( res2 ) { 297 | var bufferHelper = new BufferHelper(); 298 | 299 | res2.on( 'data', function( chunk ) { 300 | bufferHelper.concat( chunk ); 301 | } ); 302 | res2.on( 'end', function() { 303 | var buffer = bufferHelper.toBuffer(); 304 | var result; 305 | try { 306 | result = self._opt.encoding === ENCODING_RAW 307 | ? buffer 308 | : iconv.fromEncoding( buffer, self._opt.encoding ); 309 | 310 | } catch ( e ) { 311 | res.statusCode = 500; 312 | res.end( e + '' ); 313 | return; 314 | } 315 | res.setHeader( 'Set-Cookie', res2.headers['set-cookie'] ); 316 | res.setHeader( 'Content-Type' 317 | , ( self._opt.dataType === 'json' ? 'application/json' : 'text/html' ) 318 | + ';charset=UTF-8' ); 319 | res.end( result ); 320 | } ); 321 | } ); 322 | 323 | req2.on( 'error', function( e ) { 324 | res.statusCode = 500; 325 | res.end( e + '' ); 326 | } ); 327 | req.on( 'data', function( chunck ) { 328 | req2.write( chunck ); 329 | } ); 330 | req.on( 'end', function() { 331 | req2.end(); 332 | } ); 333 | 334 | } 335 | }; 336 | 337 | var ProxyFactory = Proxy; 338 | 339 | ProxyFactory.Interceptor = function( req, res ) { 340 | var interfaceId = req.url.split( /\?|\// )[1]; 341 | if ( interfaceId === '$interfaces' ) { 342 | var interfaces = interfaceManager.getClientInterfaces(); 343 | res.end( JSON.stringify( interfaces ) ); 344 | return; 345 | } 346 | 347 | try { 348 | proxy = this.create( interfaceId ); 349 | if ( proxy.getOption( 'intercepted' ) === false ) { 350 | throw new Error( 'This url is not intercepted by proxy.' ); 351 | } 352 | } catch ( e ) { 353 | res.statusCode = 404; 354 | res.end( 'Invalid url: ' + req.url + '\n' + e ); 355 | return; 356 | } 357 | proxy.interceptRequest( req, res ); 358 | }; 359 | 360 | module.exports = ProxyFactory; 361 | 362 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modelproxy-copy", 3 | "description": "midway modelproxy module", 4 | "version": "0.0.3", 5 | "author": "sosout", 6 | "contributors": [{ 7 | "name": "sosout", 8 | "email": "sosout@yeah.net" 9 | }], 10 | "dependencies": { 11 | "mockjs": "0.1.1", 12 | "iconv-lite": "0.2.11", 13 | "bufferhelper": "0.2.0" 14 | }, 15 | "devDependencies": { 16 | "mocha": "1.17.1", 17 | "jscoverage": "0.3.8", 18 | "express": "3.4.8" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/sosout/modelproxy" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | $ node mockserver.js 2 | $ jscoverage lib lib-cov 3 | $ mocha *.test.js -R html-cov > coverage.html 4 | 5 | open coverage.html -------------------------------------------------------------------------------- /tests/interfaceRules/Cart.getMyCart.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": {}, 3 | "response": { 4 | "success": true, 5 | "globalData": { 6 | "h": "g,ge3telrsgexdknzogizdm", 7 | "tpId": 0, 8 | "totalSize": 7, 9 | "invalidSize": 1, 10 | "isAllCItem": false, 11 | "isAllBItem": false, 12 | "diffTairCount": 0, 13 | "login": false, 14 | "openNoAttenItem": false, 15 | "startTime": "2013-11-08 16:51:10", 16 | "endNotEqualTime": "2014-03-07 11:36:24", 17 | "isNext": true, 18 | "page": 1, 19 | "currentPageSize": 7, 20 | "quickSettlement": false, 21 | "weakenCart": false, 22 | "cartUrl": "http://cart.taobao.com/cart.htm?from=bmini", 23 | "bundRela": { 24 | "list": [ 25 | { 26 | "id": 399573899, 27 | "title": "湾儿家", 28 | "bundles": [ 29 | "s_399573899" 30 | ] 31 | }, 32 | { 33 | "id": 695609583, 34 | "title": "傲世奢侈品交易平", 35 | "bundles": [ 36 | "s_695609583" 37 | ] 38 | }, 39 | { 40 | "id": 435179831, 41 | "title": "美美——", 42 | "bundles": [ 43 | "s_435179831" 44 | ] 45 | }, 46 | { 47 | "id": 408561270, 48 | "title": "陈秀羽服饰", 49 | "bundles": [ 50 | "s_408561270" 51 | ] 52 | }, 53 | { 54 | "id": 94269988, 55 | "title": "我的B2C小店4", 56 | "bundles": [ 57 | "s_94269988" 58 | ], 59 | "promo": { 60 | "title": "省10.00元:满就送", 61 | "discount": 1000, 62 | "point": 0, 63 | "usePoint": 0, 64 | "isSelected": true, 65 | "value": "mjs-94269988_132268118" 66 | } 67 | }, 68 | { 69 | "id": 592995015, 70 | "title": "妖精づ鞋柜", 71 | "bundles": [ 72 | "s_592995015" 73 | ] 74 | }, 75 | { 76 | "id": 84815057, 77 | "title": "优木园艺", 78 | "bundles": [ 79 | "s_84815057" 80 | ] 81 | }, 82 | { 83 | "id": 28011101, 84 | "title": "菲然小屋", 85 | "bundles": [ 86 | "s_28011101" 87 | ] 88 | }, 89 | { 90 | "id": 682508620, 91 | "title": "淑美玩具", 92 | "bundles": [ 93 | "s_682508620" 94 | ] 95 | }, 96 | { 97 | "id": 1103141522, 98 | "title": "维依恋旗舰店", 99 | "bundles": [ 100 | "s_1103141522" 101 | ], 102 | "promo": { 103 | "title": "省0.00元:3.8生活节", 104 | "discount": 0, 105 | "point": 0, 106 | "usePoint": 0, 107 | "isSelected": true, 108 | "value": "Tmall$tspAll-27484074" 109 | } 110 | }, 111 | { 112 | "id": 383999134, 113 | "title": "新星轩精品男童装", 114 | "bundles": [ 115 | "s_383999134" 116 | ] 117 | } 118 | ] 119 | }, 120 | "showRedEnvelope": false, 121 | "showGodDialogue": false 122 | }, 123 | "list": [ 124 | { 125 | "id": "s_399573899", 126 | "title": "湾儿家", 127 | "type": "shop", 128 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=399573899", 129 | "seller": "shichongdi", 130 | "host": "C", 131 | "shopId": 61498446, 132 | "sellerId": "399573899", 133 | "isValid": true, 134 | "hasPriceVolume": false, 135 | "extraItem": { 136 | "totalExtraItemNum": 0, 137 | "remainExtraItemNum": 0 138 | }, 139 | "bundles": [ 140 | { 141 | "id": "s_399573899_0", 142 | "orders": [ 143 | { 144 | "id": "51293509168", 145 | "itemId": "19584914202", 146 | "skuId": "45758911856", 147 | "cartId": "51293509168", 148 | "isValid": true, 149 | "url": "http://item.taobao.com/item.htm?id=19584914202", 150 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/13899026596314756/T11TybFlXfXXXXXXXX_!!0-item_pic.jpg", 151 | "title": "特价包邮 吸盘浴室 毛巾架 壁挂卫生间 厨房卫浴室 三层架置物架", 152 | "weight": 0, 153 | "skus": { 154 | "颜色分类": "光滑经典吸盘" 155 | }, 156 | "shopId": "61498446", 157 | "shopName": "湾儿家", 158 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=399573899", 159 | "seller": "shichongdi", 160 | "sellerId": 399573899, 161 | "price": { 162 | "now": 6000, 163 | "origin": 12800, 164 | "oriPromo": 6000, 165 | "descend": 0, 166 | "save": 6800, 167 | "sum": 24000, 168 | "actual": 0, 169 | "prepay": 0, 170 | "finalpay": 0, 171 | "extraCharges": 0 172 | }, 173 | "amount": { 174 | "now": 4, 175 | "max": 53, 176 | "limit": 9223372036854776000, 177 | "multiple": 1, 178 | "supply": 0, 179 | "demand": 0 180 | }, 181 | "itemIcon": { 182 | "CART_EBOOK": [], 183 | "CART_XIAOBAO": [ 184 | { 185 | "desc": "如实描述", 186 | "title": "消费者保障服务,卖家承诺商品如实描述", 187 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 188 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 189 | }, 190 | { 191 | "desc": "7天退换", 192 | "title": "消费者保障服务,卖家承诺7天无理由退换货", 193 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=", 194 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png" 195 | } 196 | ], 197 | "CART_YULIU": [ 198 | { 199 | "title": "支持信用卡支付", 200 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 201 | } 202 | ], 203 | "CART_IDENTITY": [] 204 | }, 205 | "isDouble11": false, 206 | "isDouble11halfDiscount": false, 207 | "isCod": false, 208 | "isAttention": true, 209 | "promos": [ 210 | [ 211 | { 212 | "style": "tbcard", 213 | "title": "省240元:年末巨惠-...", 214 | "usedPoint": 0 215 | } 216 | ] 217 | ], 218 | "createTime": 1394419698000, 219 | "attr": ";op:12800;cityCode:330100;", 220 | "preference": false, 221 | "isSellerPayPostfee": false, 222 | "leafCategory": 0, 223 | "cumulativeSales": 0, 224 | "skuStatus": 2, 225 | "cartActiveInfo": { 226 | "isDefault": false, 227 | "wantStatus": 0, 228 | "endTime": 0, 229 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1394419698000", 230 | "type": 0 231 | } 232 | } 233 | ], 234 | "type": "shop", 235 | "valid": true 236 | } 237 | ] 238 | }, 239 | { 240 | "id": "s_695609583", 241 | "title": "傲世奢侈品交易平", 242 | "type": "shop", 243 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 244 | "seller": "edt無道", 245 | "host": "C", 246 | "shopId": 106555610, 247 | "sellerId": "695609583", 248 | "isValid": true, 249 | "hasPriceVolume": false, 250 | "bundles": [ 251 | { 252 | "id": "s_695609583_0", 253 | "orders": [ 254 | { 255 | "id": "45179047633", 256 | "itemId": "36369001122", 257 | "skuId": "41467959538", 258 | "cartId": "45179047633", 259 | "isValid": true, 260 | "url": "http://item.taobao.com/item.htm?id=36369001122", 261 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/T1FP62FkRbXXXXXXXX_!!0-item_pic.jpg", 262 | "title": "愛馬H扣手掌紋皮帶", 263 | "weight": 0, 264 | "skus": { 265 | "颜色分类": "黑色" 266 | }, 267 | "shopId": "106555610", 268 | "shopName": "傲世奢侈品交易平臺", 269 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 270 | "seller": "edt無道", 271 | "sellerId": 695609583, 272 | "price": { 273 | "now": 148500, 274 | "origin": 148500, 275 | "oriPromo": 148500, 276 | "descend": 0, 277 | "save": 0, 278 | "sum": 742500, 279 | "actual": 0, 280 | "prepay": 0, 281 | "finalpay": 0, 282 | "extraCharges": 0 283 | }, 284 | "amount": { 285 | "now": 5, 286 | "max": 3, 287 | "limit": 9223372036854775807, 288 | "multiple": 1, 289 | "supply": 0, 290 | "demand": 0 291 | }, 292 | "itemIcon": { 293 | "CART_EBOOK": [], 294 | "CART_XIAOBAO": [ 295 | { 296 | "desc": "如实描述", 297 | "title": "消费者保障服务,卖家承诺商品如实描述", 298 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 299 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 300 | } 301 | ], 302 | "CART_YULIU": [], 303 | "CART_IDENTITY": [] 304 | }, 305 | "isDouble11": false, 306 | "isDouble11halfDiscount": false, 307 | "isCod": false, 308 | "isAttention": true, 309 | "createTime": 1389251633000, 310 | "attr": ";op:148500;cityCode:330100;", 311 | "preference": false, 312 | "isSellerPayPostfee": false, 313 | "leafCategory": 0, 314 | "cumulativeSales": 0, 315 | "skuStatus": 2, 316 | "cartActiveInfo": { 317 | "isDefault": false, 318 | "wantStatus": 0, 319 | "endTime": 0, 320 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1389251633000", 321 | "type": 0 322 | } 323 | }, 324 | { 325 | "id": "40865005024", 326 | "itemId": "36367062613", 327 | "skuId": "36240702186", 328 | "cartId": "40865005024", 329 | "isValid": true, 330 | "url": "http://item.taobao.com/item.htm?id=36367062613", 331 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/19583033169071904/T1LFMLFXdXXXXXXXXX_!!0-item_pic.jpg", 332 | "title": "全新蘭博基尼LP700-4 含購置稅", 333 | "weight": 0, 334 | "skus": { 335 | "颜色分类": "桔色" 336 | }, 337 | "shopId": "106555610", 338 | "shopName": "傲世奢侈品交易平臺", 339 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 340 | "seller": "edt無道", 341 | "sellerId": 695609583, 342 | "price": { 343 | "now": 755000000, 344 | "origin": 770000000, 345 | "oriPromo": 770000000, 346 | "descend": 0, 347 | "save": 15000000, 348 | "sum": 7550000000, 349 | "actual": 0, 350 | "prepay": 0, 351 | "finalpay": 0, 352 | "extraCharges": 0 353 | }, 354 | "amount": { 355 | "now": 10, 356 | "max": 10, 357 | "limit": 9223372036854775807, 358 | "multiple": 1, 359 | "supply": 0, 360 | "demand": 0 361 | }, 362 | "itemIcon": { 363 | "CART_EBOOK": [], 364 | "CART_XIAOBAO": [ 365 | { 366 | "desc": "如实描述", 367 | "title": "消费者保障服务,卖家承诺商品如实描述", 368 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 369 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 370 | } 371 | ], 372 | "CART_YULIU": [], 373 | "CART_IDENTITY": [] 374 | }, 375 | "isDouble11": false, 376 | "isDouble11halfDiscount": false, 377 | "isCod": false, 378 | "isAttention": true, 379 | "createTime": 1386645841000, 380 | "attr": ";op:770000000;cityCode:330100;", 381 | "preference": false, 382 | "isSellerPayPostfee": false, 383 | "leafCategory": 0, 384 | "cumulativeSales": 0, 385 | "skuStatus": 2, 386 | "cartActiveInfo": { 387 | "isDefault": false, 388 | "wantStatus": 0, 389 | "endTime": 0, 390 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386645841000", 391 | "type": 0 392 | } 393 | }, 394 | { 395 | "id": "41100499431", 396 | "itemId": "36321918718", 397 | "skuId": "36104438665", 398 | "cartId": "41100499431", 399 | "isValid": false, 400 | "url": "http://item.taobao.com/item.htm?id=36321918718", 401 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/19583031080830054/T1KCzNFadXXXXXXXXX_!!0-item_pic.jpg", 402 | "title": "邁凱輪mp4-12c全新中文版預定", 403 | "weight": 0, 404 | "skus": { 405 | "颜色分类": "红色" 406 | }, 407 | "shopId": "106555610", 408 | "shopName": "傲世奢侈品交易平臺", 409 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 410 | "seller": "edt無道", 411 | "sellerId": 695609583, 412 | "price": { 413 | "now": 396000000, 414 | "origin": 396000000, 415 | "oriPromo": 396000000, 416 | "descend": 0, 417 | "save": 0, 418 | "sum": 396000000, 419 | "actual": 0, 420 | "prepay": 0, 421 | "finalpay": 0, 422 | "extraCharges": 0 423 | }, 424 | "amount": { 425 | "now": 1, 426 | "max": 2, 427 | "limit": 9223372036854775807, 428 | "multiple": 1, 429 | "supply": 0, 430 | "demand": 0 431 | }, 432 | "itemIcon": { 433 | "CART_EBOOK": [], 434 | "CART_XIAOBAO": [ 435 | { 436 | "desc": "如实描述", 437 | "title": "消费者保障服务,卖家承诺商品如实描述", 438 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 439 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 440 | } 441 | ], 442 | "CART_YULIU": [], 443 | "CART_IDENTITY": [] 444 | }, 445 | "isDouble11": false, 446 | "isDouble11halfDiscount": false, 447 | "isCod": false, 448 | "isAttention": true, 449 | "createTime": 1386734805000, 450 | "attr": ";op:396000000;cityCode:330100;", 451 | "preference": false, 452 | "isSellerPayPostfee": false, 453 | "leafCategory": 0, 454 | "cumulativeSales": 0, 455 | "skuStatus": 3, 456 | "cartActiveInfo": { 457 | "isDefault": false, 458 | "wantStatus": 0, 459 | "endTime": 0, 460 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386734805000", 461 | "type": 0 462 | } 463 | }, 464 | { 465 | "id": "40865005023", 466 | "itemId": "36250290617", 467 | "skuId": "52649772565", 468 | "cartId": "40865005023", 469 | "isValid": false, 470 | "url": "http://item.taobao.com/item.htm?id=36250290617", 471 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/695609583/T2OGnNXfpbXXXXXXXX_!!695609583.jpg", 472 | "title": "兰博基尼LP560-4", 473 | "weight": 0, 474 | "skus": { 475 | "颜色分类": "白色" 476 | }, 477 | "shopId": "106555610", 478 | "shopName": "傲世奢侈品交易平臺", 479 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=695609583", 480 | "seller": "edt無道", 481 | "sellerId": 695609583, 482 | "price": { 483 | "now": 348000000, 484 | "origin": 348000000, 485 | "oriPromo": 348000000, 486 | "descend": 0, 487 | "save": 0, 488 | "sum": 3480000000, 489 | "actual": 0, 490 | "prepay": 0, 491 | "finalpay": 0, 492 | "extraCharges": 0 493 | }, 494 | "amount": { 495 | "now": 10, 496 | "max": 10, 497 | "limit": 9223372036854775807, 498 | "multiple": 1, 499 | "supply": 0, 500 | "demand": 0 501 | }, 502 | "itemIcon": { 503 | "CART_EBOOK": [], 504 | "CART_XIAOBAO": [ 505 | { 506 | "desc": "如实描述", 507 | "title": "消费者保障服务,卖家承诺商品如实描述", 508 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 509 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 510 | } 511 | ], 512 | "CART_YULIU": [], 513 | "CART_IDENTITY": [] 514 | }, 515 | "isDouble11": false, 516 | "isDouble11halfDiscount": false, 517 | "isCod": false, 518 | "isAttention": true, 519 | "createTime": 1386645818000, 520 | "attr": ";op:348000000;cityCode:330100;", 521 | "preference": false, 522 | "isSellerPayPostfee": false, 523 | "leafCategory": 0, 524 | "cumulativeSales": 0, 525 | "skuStatus": 3, 526 | "cartActiveInfo": { 527 | "isDefault": false, 528 | "wantStatus": 0, 529 | "endTime": 0, 530 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386645818000", 531 | "type": 0 532 | } 533 | } 534 | ], 535 | "type": "shop", 536 | "valid": true 537 | } 538 | ] 539 | }, 540 | { 541 | "id": "s_435179831", 542 | "title": "美美——", 543 | "type": "shop", 544 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831", 545 | "seller": "c测试账号136", 546 | "host": "C", 547 | "shopId": 62384964, 548 | "sellerId": "435179831", 549 | "isValid": true, 550 | "scrollPromos": [ 551 | "
  • 单笔订单满2件,免邮
  • ", 552 | "
  • 单笔订单满3件,免邮免邮
  • ", 553 | "
  • 单笔订单满4件,免邮免邮免邮
  • " 554 | ], 555 | "hasPriceVolume": false, 556 | "bundles": [ 557 | { 558 | "id": "s_435179831_0", 559 | "orders": [ 560 | { 561 | "id": "45051164095", 562 | "itemId": "35327382733", 563 | "skuId": "33649369706", 564 | "cartId": "45051164095", 565 | "isValid": true, 566 | "url": "http://item.taobao.com/item.htm?id=35327382733", 567 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/19831041340172207/T1w1tEFatdXXXXXXXX_!!0-item_pic.jpg", 568 | "title": "测试商品不要折--商品优惠券请不要拍", 569 | "weight": 0, 570 | "skus": { 571 | "颜色分类": "深紫色" 572 | }, 573 | "shopId": "62384964", 574 | "shopName": "美美——", 575 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831", 576 | "seller": "c测试账号136", 577 | "sellerId": 435179831, 578 | "price": { 579 | "now": 1, 580 | "origin": 10000, 581 | "oriPromo": 5000, 582 | "descend": 0, 583 | "save": 9999, 584 | "sum": 15, 585 | "actual": 0, 586 | "prepay": 0, 587 | "finalpay": 0, 588 | "extraCharges": 0 589 | }, 590 | "amount": { 591 | "now": 15, 592 | "max": 87, 593 | "limit": 9223372036854775807, 594 | "multiple": 1, 595 | "supply": 0, 596 | "demand": 0 597 | }, 598 | "itemIcon": { 599 | "CART_EBOOK": [], 600 | "CART_XIAOBAO": [ 601 | { 602 | "desc": "如实描述", 603 | "title": "消费者保障服务,卖家承诺商品如实描述", 604 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 605 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 606 | } 607 | ], 608 | "CART_YULIU": [], 609 | "CART_IDENTITY": [] 610 | }, 611 | "isDouble11": false, 612 | "isDouble11halfDiscount": false, 613 | "isCod": false, 614 | "isAttention": true, 615 | "createTime": 1389238670000, 616 | "attr": ";op:10000;", 617 | "preference": false, 618 | "isSellerPayPostfee": false, 619 | "leafCategory": 0, 620 | "cumulativeSales": 0, 621 | "skuStatus": 2, 622 | "cartActiveInfo": { 623 | "isDefault": false, 624 | "wantStatus": 0, 625 | "endTime": 0, 626 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1389238670000", 627 | "type": 0 628 | } 629 | }, 630 | { 631 | "id": "40263066148", 632 | "itemId": "36411580369", 633 | "skuId": "0", 634 | "cartId": "40263066148", 635 | "isValid": true, 636 | "url": "http://item.taobao.com/item.htm?id=36411580369", 637 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/T1w1tEFatdXXXXXXXX_!!0-item_pic.jpg", 638 | "title": "[1212预演] 大促无SKU商品----不要拍测试请不要拍", 639 | "weight": 0, 640 | "shopId": "62384964", 641 | "shopName": "美美——", 642 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831", 643 | "seller": "c测试账号136", 644 | "sellerId": 435179831, 645 | "price": { 646 | "now": 10000, 647 | "origin": 10000, 648 | "oriPromo": 1900, 649 | "descend": 0, 650 | "save": 0, 651 | "sum": 10000, 652 | "actual": 0, 653 | "prepay": 0, 654 | "finalpay": 0, 655 | "extraCharges": 0 656 | }, 657 | "amount": { 658 | "now": 1, 659 | "max": 10, 660 | "limit": 9223372036854775807, 661 | "multiple": 1, 662 | "supply": 0, 663 | "demand": 0 664 | }, 665 | "itemIcon": { 666 | "CART_EBOOK": [], 667 | "CART_XIAOBAO": [ 668 | { 669 | "desc": "如实描述", 670 | "title": "消费者保障服务,卖家承诺商品如实描述", 671 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 672 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 673 | } 674 | ], 675 | "CART_YULIU": [], 676 | "CART_IDENTITY": [] 677 | }, 678 | "isDouble11": false, 679 | "isDouble11halfDiscount": false, 680 | "isCod": false, 681 | "isAttention": true, 682 | "createTime": 1386225437000, 683 | "attr": ";op:10000;", 684 | "preference": false, 685 | "isSellerPayPostfee": false, 686 | "leafCategory": 0, 687 | "cumulativeSales": 0, 688 | "skuStatus": 0, 689 | "cartActiveInfo": { 690 | "isDefault": false, 691 | "wantStatus": 0, 692 | "endTime": 0, 693 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386225437000", 694 | "type": 0 695 | } 696 | }, 697 | { 698 | "id": "40289287003", 699 | "itemId": "36313978495", 700 | "skuId": "36104069226", 701 | "cartId": "40289287003", 702 | "isValid": false, 703 | "url": "http://item.taobao.com/item.htm?id=36313978495", 704 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/19831031291397475/T1KEnLFj0XXXXXXXXX_!!2-item_pic.png", 705 | "title": "skuskuku测试数据不要拍请不要拍", 706 | "weight": 0, 707 | "skus": { 708 | "颜色分类": "巧克力色", 709 | "尺码": "大码XXL" 710 | }, 711 | "shopId": "62384964", 712 | "shopName": "美美——", 713 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=435179831", 714 | "seller": "c测试账号136", 715 | "sellerId": 435179831, 716 | "price": { 717 | "now": 8000, 718 | "origin": 8000, 719 | "oriPromo": 7744, 720 | "descend": 0, 721 | "save": 0, 722 | "sum": 8000, 723 | "actual": 0, 724 | "prepay": 0, 725 | "finalpay": 0, 726 | "extraCharges": 0 727 | }, 728 | "amount": { 729 | "now": 1, 730 | "max": 2, 731 | "limit": 9223372036854775807, 732 | "multiple": 1, 733 | "supply": 0, 734 | "demand": 0 735 | }, 736 | "itemIcon": { 737 | "CART_EBOOK": [], 738 | "CART_XIAOBAO": [ 739 | { 740 | "desc": "如实描述", 741 | "title": "消费者保障服务,卖家承诺商品如实描述", 742 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 743 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 744 | } 745 | ], 746 | "CART_YULIU": [], 747 | "CART_IDENTITY": [] 748 | }, 749 | "isDouble11": false, 750 | "isDouble11halfDiscount": false, 751 | "isCod": false, 752 | "isAttention": true, 753 | "createTime": 1386237405000, 754 | "attr": ";op:8000;cityCode:330100;", 755 | "preference": false, 756 | "isSellerPayPostfee": false, 757 | "leafCategory": 0, 758 | "cumulativeSales": 0, 759 | "skuStatus": 3, 760 | "cartActiveInfo": { 761 | "isDefault": false, 762 | "wantStatus": 0, 763 | "endTime": 0, 764 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386237405000", 765 | "type": 0 766 | } 767 | } 768 | ], 769 | "type": "shop", 770 | "valid": true 771 | } 772 | ] 773 | }, 774 | { 775 | "id": "s_408561270", 776 | "title": "陈秀羽服饰", 777 | "type": "shop", 778 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=408561270", 779 | "seller": "陀螺糖", 780 | "host": "C", 781 | "shopId": 61576170, 782 | "sellerId": "408561270", 783 | "isValid": true, 784 | "hasPriceVolume": true, 785 | "bundles": [ 786 | { 787 | "id": "s_408561270_0", 788 | "orders": [ 789 | { 790 | "id": "44237671039", 791 | "itemId": "36396805073", 792 | "skuId": "36384105920", 793 | "cartId": "44237671039", 794 | "isValid": true, 795 | "url": "http://item.taobao.com/item.htm?id=36396805073", 796 | "pic": "http://img02.taobaocdn.com/bao/uploaded/i2/408561270/T2s8tKXthaXXXXXXXX_!!408561270.jpg", 797 | "title": "慧川卉美 毛呢外套 2013冬季新款 蕾丝拼接 清新优雅中长款女装", 798 | "weight": 0, 799 | "skus": { 800 | "颜色分类": "西瓜红", 801 | "尺码": "S" 802 | }, 803 | "shopId": "61576170", 804 | "shopName": "陈秀羽服饰", 805 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=408561270", 806 | "seller": "陀螺糖", 807 | "sellerId": 408561270, 808 | "price": { 809 | "now": 29500, 810 | "origin": 31200, 811 | "oriPromo": 26500, 812 | "descend": 0, 813 | "save": 1700, 814 | "sum": 29500, 815 | "actual": 0, 816 | "prepay": 0, 817 | "finalpay": 0, 818 | "extraCharges": 0 819 | }, 820 | "amount": { 821 | "now": 1, 822 | "max": 10, 823 | "limit": 9223372036854775807, 824 | "multiple": 1, 825 | "supply": 0, 826 | "demand": 0 827 | }, 828 | "itemIcon": { 829 | "CART_EBOOK": [], 830 | "CART_XIAOBAO": [ 831 | { 832 | "desc": "如实描述", 833 | "title": "消费者保障服务,卖家承诺商品如实描述", 834 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 835 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 836 | } 837 | ], 838 | "CART_YULIU": [ 839 | { 840 | "title": "支持信用卡支付", 841 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 842 | } 843 | ], 844 | "CART_IDENTITY": [] 845 | }, 846 | "isDouble11": false, 847 | "isDouble11halfDiscount": false, 848 | "isCod": false, 849 | "isAttention": true, 850 | "createTime": 1388657451000, 851 | "attr": ";op:31200;cityCode:330100;", 852 | "preference": false, 853 | "isSellerPayPostfee": false, 854 | "leafCategory": 0, 855 | "cumulativeSales": 0, 856 | "skuStatus": 2, 857 | "cartActiveInfo": { 858 | "isDefault": false, 859 | "wantStatus": 0, 860 | "endTime": 0, 861 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1388657451000", 862 | "type": 0 863 | } 864 | } 865 | ], 866 | "type": "shop", 867 | "valid": true 868 | } 869 | ] 870 | }, 871 | { 872 | "id": "s_94269988", 873 | "title": "我的B2C小店4", 874 | "type": "shop", 875 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=94269988", 876 | "seller": "商家测试帐号9", 877 | "host": "B", 878 | "shopId": 57298338, 879 | "sellerId": "94269988", 880 | "isValid": true, 881 | "scrollPromos": [ 882 | "
  • \r\n \t\t \t\t\t\t\t\t\t\t\t满100元\r\n\t\t\t\t\t减10元\t\t\t\t\t,免运费\t\t\t\t\t,减后满100元\t\t\t\t\t,送1商城积分\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
  • " 883 | ], 884 | "hasPriceVolume": false, 885 | "promView": { 886 | "title": "省10.00元:满就送", 887 | "discount": 1000, 888 | "point": 0, 889 | "usePoint": 0, 890 | "isSelected": true, 891 | "value": "mjs-94269988_132268118" 892 | }, 893 | "bundles": [ 894 | { 895 | "id": "s_94269988_0", 896 | "orders": [ 897 | { 898 | "id": "40767079536", 899 | "itemId": "15733416213", 900 | "skuId": "0", 901 | "cartId": "40767079536", 902 | "isValid": true, 903 | "url": "http://detail.tmall.com/item.htm?id=15733416213", 904 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/T1F.yJXbRsXXbtr.wW_025126.jpg_sum.jpg", 905 | "title": "【官方测试商品,不要拍】走UMP单品优惠-琅华", 906 | "weight": 0, 907 | "shopId": "57298338", 908 | "shopName": "我的B2C小店4", 909 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=94269988", 910 | "seller": "商家测试帐号9", 911 | "sellerId": 94269988, 912 | "price": { 913 | "now": 6000000100, 914 | "origin": 6000000100, 915 | "oriPromo": 5999919800, 916 | "descend": 0, 917 | "save": 0, 918 | "sum": 30000000500, 919 | "actual": 0, 920 | "prepay": 0, 921 | "finalpay": 0, 922 | "extraCharges": 0 923 | }, 924 | "amount": { 925 | "now": 5, 926 | "max": 99, 927 | "limit": 9223372036854775807, 928 | "multiple": 1, 929 | "supply": 0, 930 | "demand": 0 931 | }, 932 | "itemIcon": { 933 | "MALL_CART_IDENTITY": [], 934 | "MALL_CART_YULIU": [], 935 | "MALL_CART_XIAOBAO": [ 936 | { 937 | "title": "消费者保障服务,卖家承诺7天退换", 938 | "link": "http://www.taobao.com/go/act/315/xb_20100707.php?ad_id=&am_id=1300268931aef04f0cdc&cm_id=&pm_id=#qitian", 939 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_7-day_return_16x16.png", 940 | "name": "七天退换" 941 | }, 942 | { 943 | "title": "消费者保障服务,卖家承诺如实描述", 944 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 945 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_true_description_16x16.png", 946 | "name": "如实描述" 947 | }, 948 | { 949 | "title": "消费者保障服务,卖家承诺假一赔三", 950 | "link": "http://www.taobao.com/go/act/315/xfzbz_jyps.php?ad_id=&am_id=1300118304240d56fca9&cm_id=&pm_id=", 951 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/an_authentic_item_16x16.png", 952 | "name": "假一赔三" 953 | } 954 | ] 955 | }, 956 | "campaignId": "0", 957 | "isDouble11": false, 958 | "isDouble11halfDiscount": false, 959 | "isCod": false, 960 | "isAttention": true, 961 | "createTime": 1386555127000, 962 | "attr": ";campaignId:0;op:6000000100;cityCode:330100;", 963 | "preference": false, 964 | "isSellerPayPostfee": false, 965 | "leafCategory": 0, 966 | "cumulativeSales": 0, 967 | "skuStatus": 0, 968 | "cartActiveInfo": { 969 | "isDefault": false, 970 | "wantStatus": 0, 971 | "endTime": 0, 972 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386555127000", 973 | "type": 0 974 | } 975 | } 976 | ], 977 | "type": "shop", 978 | "valid": true 979 | } 980 | ] 981 | }, 982 | { 983 | "id": "s_592995015", 984 | "title": "妖精づ鞋柜", 985 | "type": "shop", 986 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=592995015", 987 | "seller": "晨馨眼镜", 988 | "host": "C", 989 | "shopId": 63309206, 990 | "sellerId": "592995015", 991 | "isValid": true, 992 | "hasPriceVolume": false, 993 | "bundles": [ 994 | { 995 | "id": "s_592995015_0", 996 | "orders": [ 997 | { 998 | "id": "40168833186", 999 | "itemId": "19836221588", 1000 | "skuId": "36834499974", 1001 | "cartId": "40168833186", 1002 | "isValid": true, 1003 | "url": "http://item.taobao.com/item.htm?id=19836221588", 1004 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/15015039186552052/T1daGuFjXbXXXXXXXX_!!0-item_pic.jpg", 1005 | "title": "包邮2013秋季新款甜美蕾丝帆布鞋 女 可爱小熊高帮牛筋底学生板鞋", 1006 | "weight": 0, 1007 | "skus": { 1008 | "颜色分类": "浅蓝_小熊款", 1009 | "尺码": "38_标准码_现货" 1010 | }, 1011 | "shopId": "63309206", 1012 | "shopName": "妖精づ鞋柜", 1013 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=592995015", 1014 | "seller": "晨馨眼镜", 1015 | "sellerId": 592995015, 1016 | "price": { 1017 | "now": 3000, 1018 | "origin": 1600, 1019 | "oriPromo": 3000, 1020 | "descend": 0, 1021 | "save": 0, 1022 | "sum": 21000, 1023 | "actual": 0, 1024 | "prepay": 0, 1025 | "finalpay": 0, 1026 | "extraCharges": 0 1027 | }, 1028 | "amount": { 1029 | "now": 7, 1030 | "max": 877, 1031 | "limit": 9223372036854775807, 1032 | "multiple": 1, 1033 | "supply": 0, 1034 | "demand": 0 1035 | }, 1036 | "itemIcon": { 1037 | "CART_EBOOK": [], 1038 | "CART_XIAOBAO": [ 1039 | { 1040 | "desc": "如实描述", 1041 | "title": "消费者保障服务,卖家承诺商品如实描述", 1042 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 1043 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 1044 | } 1045 | ], 1046 | "CART_YULIU": [ 1047 | { 1048 | "title": "支持信用卡支付", 1049 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 1050 | } 1051 | ], 1052 | "CART_IDENTITY": [] 1053 | }, 1054 | "isDouble11": false, 1055 | "isDouble11halfDiscount": false, 1056 | "isCod": false, 1057 | "isAttention": true, 1058 | "createTime": 1386156674000, 1059 | "attr": ";op:1600;cityCode:330100;", 1060 | "preference": false, 1061 | "isSellerPayPostfee": false, 1062 | "leafCategory": 0, 1063 | "cumulativeSales": 0, 1064 | "skuStatus": 2, 1065 | "cartActiveInfo": { 1066 | "isDefault": false, 1067 | "wantStatus": 0, 1068 | "endTime": 0, 1069 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1386156674000", 1070 | "type": 0 1071 | } 1072 | } 1073 | ], 1074 | "type": "shop", 1075 | "valid": true 1076 | } 1077 | ] 1078 | }, 1079 | { 1080 | "id": "s_84815057", 1081 | "title": "优木园艺", 1082 | "type": "shop", 1083 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=84815057", 1084 | "seller": "imalexli", 1085 | "host": "C", 1086 | "shopId": 35851735, 1087 | "sellerId": "84815057", 1088 | "isValid": true, 1089 | "scrollPromos": [ 1090 | "
  • \r\n \t\t \t\t\t\t\t\t\t\t\t满49元\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t,送多菌灵\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
  • ", 1091 | "
  • \r\n \t\t \t\t\t\t\t\t\t\t\t满99元\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t,送多肉植物专用毛刷\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t
  • ", 1092 | "
  • \r\n\t\t\t\t\t\t\t\t\t\t\t满200元\r\n\t\t\t\t\t ,送2条10g装美乐棵 ,可在购物车换购商品\t\t\t\t\t\t\t\t\t\t\t\t\t\t
  • ", 1093 | "
  • \r\n\t\t\t\t\t\t\t\t\t\t\t满520元\r\n\t\t\t\t\t减10元 ,减后满520元 , 送5元店铺优惠券 ,送美乐棵液体肥 ,送1注彩票 ,可在购物车换购商品\t\t\t\t\t\t\t\t\t\t\t\t\t\t
  • " 1094 | ], 1095 | "hasPriceVolume": false, 1096 | "bundles": [ 1097 | { 1098 | "id": "s_84815057_0", 1099 | "orders": [ 1100 | { 1101 | "id": "39716263498", 1102 | "itemId": "18201654273", 1103 | "skuId": "0", 1104 | "cartId": "39716263498", 1105 | "isValid": true, 1106 | "url": "http://item.taobao.com/item.htm?id=18201654273", 1107 | "pic": "http://img03.taobaocdn.com/bao/uploaded/i3/T1fXQnFkVdXXXXXXXX_!!0-item_pic.jpg", 1108 | "title": "美乐棵 家庭园艺肥料 通用型 500g 大包装更划算", 1109 | "weight": 0, 1110 | "shopId": "35851735", 1111 | "shopName": "优木园艺", 1112 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=84815057", 1113 | "seller": "imalexli", 1114 | "sellerId": 84815057, 1115 | "price": { 1116 | "now": 4500, 1117 | "origin": 4500, 1118 | "oriPromo": 4500, 1119 | "descend": 0, 1120 | "save": 0, 1121 | "sum": 4500, 1122 | "actual": 0, 1123 | "prepay": 0, 1124 | "finalpay": 0, 1125 | "extraCharges": 0 1126 | }, 1127 | "amount": { 1128 | "now": 1, 1129 | "max": 11, 1130 | "limit": 9223372036854776000, 1131 | "multiple": 1, 1132 | "supply": 0, 1133 | "demand": 0 1134 | }, 1135 | "itemIcon": { 1136 | "CART_EBOOK": [], 1137 | "CART_XIAOBAO": [ 1138 | { 1139 | "desc": "如实描述", 1140 | "title": "消费者保障服务,卖家承诺商品如实描述", 1141 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 1142 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 1143 | } 1144 | ], 1145 | "CART_YULIU": [ 1146 | { 1147 | "title": "支持信用卡支付", 1148 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 1149 | } 1150 | ], 1151 | "CART_IDENTITY": [] 1152 | }, 1153 | "isDouble11": false, 1154 | "isDouble11halfDiscount": false, 1155 | "isCod": false, 1156 | "isAttention": true, 1157 | "createTime": 1385604205000, 1158 | "attr": ";op:4500;", 1159 | "preference": false, 1160 | "isSellerPayPostfee": false, 1161 | "leafCategory": 0, 1162 | "cumulativeSales": 0, 1163 | "skuStatus": 0, 1164 | "cartActiveInfo": { 1165 | "isDefault": false, 1166 | "wantStatus": 0, 1167 | "endTime": 0, 1168 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385604205000", 1169 | "type": 0 1170 | } 1171 | } 1172 | ], 1173 | "type": "shop", 1174 | "valid": true 1175 | } 1176 | ] 1177 | }, 1178 | { 1179 | "id": "s_28011101", 1180 | "title": "菲然小屋", 1181 | "type": "shop", 1182 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=28011101", 1183 | "seller": "aoyoucheny", 1184 | "host": "C", 1185 | "shopId": 33385218, 1186 | "sellerId": "28011101", 1187 | "isValid": true, 1188 | "hasPriceVolume": false, 1189 | "bundles": [ 1190 | { 1191 | "id": "s_28011101_0", 1192 | "orders": [ 1193 | { 1194 | "id": "39840397057", 1195 | "itemId": "20174283014", 1196 | "skuId": "32568327374", 1197 | "cartId": "39840397057", 1198 | "isValid": true, 1199 | "url": "http://item.taobao.com/item.htm?id=20174283014", 1200 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/28011101/T21BCmXdxcXXXXXXXX_!!28011101.jpg", 1201 | "title": "夏装2014新款卡其可可童装女童儿童蕾丝网纱裙公主裙礼服裙子童裙", 1202 | "weight": 0, 1203 | "skus": { 1204 | "颜色分类": "81933-蓝色#", 1205 | "参考身高": "100cm" 1206 | }, 1207 | "shopId": "33385218", 1208 | "shopName": "菲然小屋", 1209 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=28011101", 1210 | "seller": "aoyoucheny", 1211 | "sellerId": 28011101, 1212 | "price": { 1213 | "now": 8800, 1214 | "origin": 8800, 1215 | "oriPromo": 7200, 1216 | "descend": 0, 1217 | "save": 0, 1218 | "sum": 8800, 1219 | "actual": 0, 1220 | "prepay": 0, 1221 | "finalpay": 0, 1222 | "extraCharges": 0 1223 | }, 1224 | "amount": { 1225 | "now": 1, 1226 | "max": 1, 1227 | "limit": 9223372036854776000, 1228 | "multiple": 1, 1229 | "supply": 1, 1230 | "demand": 0 1231 | }, 1232 | "itemIcon": { 1233 | "CART_EBOOK": [], 1234 | "CART_XIAOBAO": [ 1235 | { 1236 | "desc": "如实描述", 1237 | "title": "消费者保障服务,卖家承诺商品如实描述", 1238 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 1239 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 1240 | }, 1241 | { 1242 | "desc": "7天退换", 1243 | "title": "消费者保障服务,卖家承诺7天无理由退换货", 1244 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=", 1245 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png" 1246 | } 1247 | ], 1248 | "CART_YULIU": [ 1249 | { 1250 | "title": "支持信用卡支付", 1251 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 1252 | } 1253 | ], 1254 | "CART_IDENTITY": [] 1255 | }, 1256 | "isDouble11": false, 1257 | "isDouble11halfDiscount": false, 1258 | "isCod": false, 1259 | "isAttention": true, 1260 | "createTime": 1385573556000, 1261 | "attr": ";op:8800;", 1262 | "preference": false, 1263 | "isSellerPayPostfee": false, 1264 | "leafCategory": 0, 1265 | "cumulativeSales": 0, 1266 | "skuStatus": 2, 1267 | "cartActiveInfo": { 1268 | "isDefault": false, 1269 | "wantStatus": 0, 1270 | "endTime": 0, 1271 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385573556000", 1272 | "type": 0 1273 | } 1274 | } 1275 | ], 1276 | "type": "shop", 1277 | "valid": true 1278 | } 1279 | ] 1280 | }, 1281 | { 1282 | "id": "s_682508620", 1283 | "title": "淑美玩具", 1284 | "type": "shop", 1285 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=682508620", 1286 | "seller": "cfwd2011", 1287 | "host": "C", 1288 | "shopId": 65608623, 1289 | "sellerId": "682508620", 1290 | "isValid": true, 1291 | "hasPriceVolume": true, 1292 | "bundles": [ 1293 | { 1294 | "id": "s_682508620_0", 1295 | "orders": [ 1296 | { 1297 | "id": "39840397058", 1298 | "itemId": "17305114097", 1299 | "skuId": "0", 1300 | "cartId": "39840397058", 1301 | "isValid": true, 1302 | "url": "http://item.taobao.com/item.htm?id=17305114097", 1303 | "pic": "http://img02.taobaocdn.com/bao/uploaded/i2/18620019764649808/T1KZ0eXBXhXXXXXXXX_!!0-item_pic.jpg", 1304 | "title": "志扬玩具 滑翔机 遥控飞机 直升机 固定翼EPP 航模 耐摔", 1305 | "weight": 0, 1306 | "shopId": "65608623", 1307 | "shopName": "淑美玩具", 1308 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=682508620", 1309 | "seller": "cfwd2011", 1310 | "sellerId": 682508620, 1311 | "price": { 1312 | "now": 11800, 1313 | "origin": 11800, 1314 | "oriPromo": 11800, 1315 | "descend": 0, 1316 | "save": 0, 1317 | "sum": 11800, 1318 | "actual": 0, 1319 | "prepay": 0, 1320 | "finalpay": 0, 1321 | "extraCharges": 0 1322 | }, 1323 | "amount": { 1324 | "now": 1, 1325 | "max": 93, 1326 | "limit": 9223372036854776000, 1327 | "multiple": 1, 1328 | "supply": 0, 1329 | "demand": 0 1330 | }, 1331 | "itemIcon": { 1332 | "CART_EBOOK": [], 1333 | "CART_XIAOBAO": [ 1334 | { 1335 | "desc": "如实描述", 1336 | "title": "消费者保障服务,卖家承诺商品如实描述", 1337 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 1338 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 1339 | }, 1340 | { 1341 | "desc": "7天退换", 1342 | "title": "消费者保障服务,卖家承诺7天无理由退换货", 1343 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=", 1344 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png" 1345 | } 1346 | ], 1347 | "CART_YULIU": [ 1348 | { 1349 | "title": "支持信用卡支付", 1350 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 1351 | } 1352 | ], 1353 | "CART_IDENTITY": [] 1354 | }, 1355 | "isDouble11": false, 1356 | "isDouble11halfDiscount": false, 1357 | "isCod": false, 1358 | "isAttention": true, 1359 | "createTime": 1385573541000, 1360 | "attr": ";op:11800;", 1361 | "preference": false, 1362 | "isSellerPayPostfee": false, 1363 | "leafCategory": 0, 1364 | "cumulativeSales": 0, 1365 | "skuStatus": 0, 1366 | "cartActiveInfo": { 1367 | "isDefault": false, 1368 | "wantStatus": 0, 1369 | "endTime": 0, 1370 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385573541000", 1371 | "type": 0 1372 | } 1373 | } 1374 | ], 1375 | "type": "shop", 1376 | "valid": true 1377 | } 1378 | ] 1379 | }, 1380 | { 1381 | "id": "s_1103141522", 1382 | "title": "维依恋旗舰店", 1383 | "type": "shop", 1384 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=1103141522", 1385 | "seller": "维依恋旗舰店", 1386 | "host": "B", 1387 | "shopId": 101488084, 1388 | "sellerId": "1103141522", 1389 | "isValid": true, 1390 | "hasPriceVolume": false, 1391 | "promView": { 1392 | "title": "省0.00元:3.8生活节", 1393 | "discount": 0, 1394 | "point": 0, 1395 | "usePoint": 0, 1396 | "isSelected": true, 1397 | "value": "Tmall$tspAll-27484074" 1398 | }, 1399 | "bundles": [ 1400 | { 1401 | "id": "s_1103141522_1", 1402 | "orders": [ 1403 | { 1404 | "id": "38105149506", 1405 | "itemId": "19182186172", 1406 | "skuId": "30291521265", 1407 | "cartId": "38105149506", 1408 | "isValid": true, 1409 | "url": "http://detail.tmall.com/item.htm?id=19182186172", 1410 | "pic": "http://img04.taobaocdn.com/bao/uploaded/i4/1103141522/T2FCMPXmpXXXXXXXXX_!!1103141522.jpg_sum.jpg", 1411 | "title": "维依恋2014春装新款气质女装包臀修身长袖连衣裙春秋针织打底裙子", 1412 | "weight": 0, 1413 | "skus": { 1414 | "颜色分类": "紫色", 1415 | "尺码": "M" 1416 | }, 1417 | "shopId": "101488084", 1418 | "shopName": "维依恋旗舰店", 1419 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=1103141522", 1420 | "seller": "维依恋旗舰店", 1421 | "sellerId": 1103141522, 1422 | "price": { 1423 | "now": 16800, 1424 | "origin": 29800, 1425 | "oriPromo": 16800, 1426 | "descend": 0, 1427 | "save": 13000, 1428 | "sum": 16800, 1429 | "actual": 0, 1430 | "prepay": 0, 1431 | "finalpay": 0, 1432 | "extraCharges": 0 1433 | }, 1434 | "amount": { 1435 | "now": 1, 1436 | "max": 21, 1437 | "limit": 9223372036854775807, 1438 | "multiple": 1, 1439 | "supply": 0, 1440 | "demand": 0 1441 | }, 1442 | "itemIcon": { 1443 | "MALL_CART_IDENTITY": [], 1444 | "PAYMENT": [ 1445 | { 1446 | "title": "支持信用卡支付", 1447 | "link": "", 1448 | "img": "http://a.tbcdn.cn/sys/common/icon/trade/xcard.png" 1449 | } 1450 | ], 1451 | "MALL_CART_YULIU": [], 1452 | "MALL_CART_XIAOBAO": [ 1453 | { 1454 | "title": "消费者保障服务,卖家承诺7天退换", 1455 | "link": "http://www.taobao.com/go/act/315/xb_20100707.php?ad_id=&am_id=1300268931aef04f0cdc&cm_id=&pm_id=#qitian", 1456 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_7-day_return_16x16.png", 1457 | "name": "七天退换" 1458 | }, 1459 | { 1460 | "title": "消费者保障服务,卖家承诺如实描述", 1461 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 1462 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/a_true_description_16x16.png", 1463 | "name": "如实描述" 1464 | }, 1465 | { 1466 | "title": "消费者保障服务,卖家承诺假一赔三", 1467 | "link": "http://www.taobao.com/go/act/315/xfzbz_jyps.php?ad_id=&am_id=1300118304240d56fca9&cm_id=&pm_id=", 1468 | "img": "http://a.tbcdn.cn/tbsp/icon/xiaobao/an_authentic_item_16x16.png", 1469 | "name": "假一赔三" 1470 | } 1471 | ] 1472 | }, 1473 | "campaignId": "0", 1474 | "isDouble11": false, 1475 | "isDouble11halfDiscount": false, 1476 | "isCod": false, 1477 | "isAttention": true, 1478 | "promos": [ 1479 | [ 1480 | { 1481 | "style": "Tmall$tmallItemPromotion", 1482 | "title": "省130元:三八节大促", 1483 | "usedPoint": 0 1484 | } 1485 | ] 1486 | ], 1487 | "createTime": 1383900681000, 1488 | "attr": ";campaignId:0;op:29800;cityCode:330100;", 1489 | "preference": false, 1490 | "isSellerPayPostfee": false, 1491 | "leafCategory": 0, 1492 | "cumulativeSales": 0, 1493 | "suggestInfo": { 1494 | "tjbSuggestInfo": "1:16500;" 1495 | }, 1496 | "skuStatus": 2, 1497 | "cartActiveInfo": { 1498 | "isDefault": false, 1499 | "wantStatus": 0, 1500 | "endTime": 0, 1501 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1383900681000", 1502 | "type": 0 1503 | }, 1504 | "logo": [ 1505 | { 1506 | "pos": 2, 1507 | "url": "http://img.taobaocdn.com/bao/uploaded/T1CgyXFxhaXXaCwpjX.png" 1508 | } 1509 | ] 1510 | } 1511 | ], 1512 | "type": "act", 1513 | "title": "3.8生活节", 1514 | "url": "http://marketing.tmall.com/home/tsp/page.htm?id=27484074_101488084", 1515 | "scrollPromos": [ 1516 | "满1件,享包邮" 1517 | ], 1518 | "gradeMessage": "", 1519 | "promos": [ 1520 | "包邮" 1521 | ], 1522 | "valid": true 1523 | } 1524 | ] 1525 | }, 1526 | { 1527 | "id": "s_383999134", 1528 | "title": "新星轩精品男童装", 1529 | "type": "shop", 1530 | "url": "http://store.taobao.com/shop/view_shop.htm?user_number_id=383999134", 1531 | "seller": "新星轩", 1532 | "host": "C", 1533 | "shopId": 60795716, 1534 | "sellerId": "383999134", 1535 | "isValid": false, 1536 | "hasPriceVolume": false, 1537 | "bundles": [ 1538 | { 1539 | "id": "s_383999134_0", 1540 | "orders": [ 1541 | { 1542 | "id": "39748572042", 1543 | "itemId": "20126729337", 1544 | "skuId": "37509411587", 1545 | "cartId": "39748572042", 1546 | "isValid": false, 1547 | "url": "http://item.taobao.com/item.htm?id=20126729337", 1548 | "pic": "http://img01.taobaocdn.com/bao/uploaded/i1/383999134/T2AsXWXvdXXXXXXXXX_!!383999134.jpg", 1549 | "title": "秋款童装 男童风衣外套中长款 儿童皮风衣韩版潮 春装2014新款", 1550 | "weight": 0, 1551 | "skus": { 1552 | "颜色分类": "黑色", 1553 | "参考身高": "110cm【110码】" 1554 | }, 1555 | "shopId": "60795716", 1556 | "shopName": "新星轩精品男童装", 1557 | "shopUrl": "http://store.taobao.com/shop/view_shop.htm?user_number_id=383999134", 1558 | "seller": "新星轩", 1559 | "sellerId": 383999134, 1560 | "price": { 1561 | "now": 25800, 1562 | "origin": 25800, 1563 | "oriPromo": 16800, 1564 | "descend": 0, 1565 | "save": 0, 1566 | "sum": 25800, 1567 | "actual": 0, 1568 | "prepay": 0, 1569 | "finalpay": 0, 1570 | "extraCharges": 0 1571 | }, 1572 | "amount": { 1573 | "now": 1, 1574 | "max": 0, 1575 | "limit": 9223372036854775807, 1576 | "multiple": 1, 1577 | "supply": 0, 1578 | "demand": 0 1579 | }, 1580 | "itemIcon": { 1581 | "CART_EBOOK": [], 1582 | "CART_XIAOBAO": [ 1583 | { 1584 | "desc": "如实描述", 1585 | "title": "消费者保障服务,卖家承诺商品如实描述", 1586 | "link": "http://www.taobao.com/go/act/315/xfzbz_rsms.php?ad_id=&am_id=130011830696bce9eda3&cm_id=&pm_id=", 1587 | "img": "http://img03.taobaocdn.com/tps/i3/T1bnR4XEBhXXcQVo..-14-16.png" 1588 | }, 1589 | { 1590 | "desc": "7天退换", 1591 | "title": "消费者保障服务,卖家承诺7天无理由退换货", 1592 | "link": "http://www.taobao.com/go/act/315/xbqt090304.php?ad_id=&am_id=130011831021c2f3caab&cm_id=&pm_id=", 1593 | "img": "http://img04.taobaocdn.com/tps/i4/T16lKeXwFcXXadE...-14-15.png" 1594 | } 1595 | ], 1596 | "CART_YULIU": [ 1597 | { 1598 | "title": "支持信用卡支付", 1599 | "img": "http://assets.taobaocdn.com/sys/common/icon/trade/xcard.png" 1600 | } 1601 | ], 1602 | "CART_IDENTITY": [] 1603 | }, 1604 | "isDouble11": false, 1605 | "isDouble11halfDiscount": false, 1606 | "isCod": false, 1607 | "isAttention": true, 1608 | "createTime": 1385627760000, 1609 | "attr": ";op:25800;", 1610 | "preference": false, 1611 | "isSellerPayPostfee": false, 1612 | "leafCategory": 0, 1613 | "cumulativeSales": 0, 1614 | "skuStatus": 3, 1615 | "cartActiveInfo": { 1616 | "isDefault": false, 1617 | "wantStatus": 0, 1618 | "endTime": 0, 1619 | "cartBcParams": "buyerCondition~0~~cartCreateTime~1385627760000", 1620 | "type": 0 1621 | } 1622 | } 1623 | ], 1624 | "type": "shop", 1625 | "valid": true 1626 | } 1627 | ] 1628 | } 1629 | ] 1630 | }, 1631 | "responseError": "this is error data" 1632 | } -------------------------------------------------------------------------------- /tests/interfaceRules/Search.getNav.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | 4 | }, 5 | "response": "This is a mock text", 6 | "responseError": "This is a mock error" 7 | } -------------------------------------------------------------------------------- /tests/interfaceRules/Search.list.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "keywords": "", 4 | "catId|100000-999999": 1 5 | }, 6 | "response": { 7 | "data|1-10": [ { 8 | "id|+1": 1, 9 | "grade1|1-100": 1, 10 | "grade2|90-100": 1, 11 | "float1|.1-10": 10, 12 | "float2|1-100.1-10": 1, 13 | "float3|999.1-10": 1, 14 | "float4|.3-10": 123.123, 15 | "star|1-5": "★", 16 | "cn|1-5": "汉字", 17 | "repeat|10": "A", 18 | "published|1": false, 19 | "email": "@EMAIL", 20 | "date": "@DATE", 21 | "time": "@TIME", 22 | "datetime": "@DATETIME", 23 | "method|1": ["GET", "POST", "HEAD", "DELETE"], 24 | "size": "@AD_SIZE", 25 | "img1": "@IMG(200x200)", 26 | "img2": "@IMG", 27 | "img3": "@IMG(@size)", 28 | "img4": "@IMG(@AD_SIZE)", 29 | "dummyimage": { 30 | "size": "@AD_SIZE", 31 | "background": "@COLOR", 32 | "foreground": "@COLOR", 33 | "format|1": ["png", "gif", "jpg"], 34 | "text": "@WORD", 35 | "url": "http://dummyimage.com/@size/@background/@foreground.@format&text=@text" 36 | }, 37 | "param": "abc=123", 38 | "url1": "@img3?@param", 39 | "url2": "@img4?@ID&id=@id" 40 | } ] 41 | }, 42 | "responseError": { 43 | "CODE": "5301" 44 | } 45 | } -------------------------------------------------------------------------------- /tests/interfaceRules/Search.suggest.rule.json: -------------------------------------------------------------------------------- 1 | error rule -------------------------------------------------------------------------------- /tests/interface_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "midway example interface configuration", 3 | "version": "1.0.0", 4 | "engine": "mockjs", 5 | "rulebase": "./interfaceRules/", 6 | "status": "online", 7 | "interfaces": [ { 8 | "name": "我的购物车", 9 | "id": "Cart.getMyCart", 10 | "urls": { 11 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 12 | }, 13 | "isCookieNeeded": true, 14 | "timeout": 100, 15 | "encoding": "gbk" 16 | }, { 17 | "name": "主搜索接口", 18 | "id": "Search.list", 19 | "urls": { 20 | "online": "http://api.s.m.taobao.com/search.json" 21 | }, 22 | "status": "mock" 23 | }, { 24 | "name": "热词推荐接口", 25 | "id": "Search.suggest", 26 | "urls": { 27 | "online": "http://suggest.taobao.com/sug" 28 | } 29 | }, { 30 | "name": "导航获取接口", 31 | "id": "Search.getNav", 32 | "urls": { 33 | "online": "http://s.m.taobao.com/client/search.do" 34 | }, 35 | "status": "mock", 36 | "isRuleStatic": true 37 | }, { 38 | "name": "重复的导航获取接口", 39 | "id": "D.getNav", 40 | "urls": { 41 | "online": "http://s.m.taobao.com/client/search.do" 42 | }, 43 | "status": "online", 44 | "isRuleStatic": true, 45 | "intercepted": false 46 | }, { 47 | "name": "post测试接口", 48 | "id": "Test.post", 49 | "urls": { 50 | "online": "http://127.0.0.1:3001/post" 51 | }, 52 | "method": "post", 53 | "dataType": "text" 54 | },{ 55 | "name": "timeout测试接口", 56 | "id": "Test.timeoutUrl", 57 | "urls": { 58 | "online": "http://127.0.0.1:3001/timeoutUrl" 59 | }, 60 | "method": "get", 61 | "dataType": "text", 62 | "timeout": 1000 63 | } ], 64 | "combo": { 65 | "getMyData": [ "Cart.getCart", "Search.suggest" ] 66 | } 67 | } -------------------------------------------------------------------------------- /tests/interfacemanager.test.js: -------------------------------------------------------------------------------- 1 | var assert = require( 'assert' ); 2 | 3 | var InterfaceManager = require( '../lib-cov/interfacemanager' ); 4 | var interfaceManager; 5 | 6 | describe( 'InterfaceManager', function() { 7 | it( 'can not be initalized by error path', function() { 8 | assert.throws( function() { 9 | new InterfaceManager( 'error/path' ); 10 | }, function( err ) { 11 | return err.toString() 12 | .indexOf( 'Fail to load interface profiles.Error' ) !== -1; 13 | } ); 14 | 15 | interfaceManager = new InterfaceManager( '../tests/interface_test.json' ); 16 | assert.equal( interfaceManager instanceof InterfaceManager, true ); 17 | } ); 18 | 19 | it( 'can not be initalized when no status is specified', function() { 20 | assert.throws( function() { 21 | new InterfaceManager({}); 22 | }, function( err ) { 23 | return err.toString() 24 | .indexOf( 'There is no status specified' ) !== -1; 25 | } ); 26 | } ); 27 | 28 | it( 'should throw error if the interface configuration is not a json ', function() { 29 | assert.throws( function() { 30 | new InterfaceManager( '../tests/README.md' ); 31 | }, function( err ) { 32 | return err.toString() 33 | .indexOf( 'syntax error' ) !== -1; 34 | } ); 35 | } ); 36 | } ); 37 | 38 | describe( 'interfaceManager', function() { 39 | var ifmgr = new InterfaceManager( { 40 | status: 'online' 41 | } ); 42 | 43 | describe( '#_addProfile()', function() { 44 | it( 'can not be added without id', function() { 45 | assert.equal( ifmgr._addProfile( { 46 | urls: { 47 | online: 'http://url1' 48 | } 49 | } ), false ); 50 | } ); 51 | 52 | it( 'can not be added when the interface id does not match ^((\\w+\\.)*\\w+)$', function() { 53 | assert.equal( ifmgr._addProfile( { 54 | id: 'Abc-methodName', 55 | urls: { 56 | online: 'http://url1' 57 | } 58 | } ), false ); 59 | 60 | assert.equal( ifmgr._addProfile( { 61 | id: 'Abc.methodName.', 62 | urls: { 63 | online: 'http://url1' 64 | } 65 | } ), false ); 66 | 67 | assert.equal( ifmgr._addProfile( { 68 | id: 'Abc.methodName', 69 | urls: { 70 | online: 'http://url1' 71 | } 72 | } ), true ); 73 | } ); 74 | 75 | it( 'can not add duplicated interface id', function() { 76 | assert.equal( ifmgr._addProfile( { 77 | id: 'Abc.methodName', 78 | urls: { 79 | online: 'htpp://url1' 80 | } 81 | } ), false ); 82 | 83 | assert.equal( ifmgr._addProfile( { 84 | id: 'Abc.method1', 85 | urls: { 86 | online: 'htpp://url1' 87 | } 88 | } ), true ); 89 | } ); 90 | 91 | it( 'must have at least one url in urls or its rulefile is available', function() { 92 | ifmgr._rulebase = '.'; 93 | 94 | assert.equal( ifmgr._addProfile( { 95 | id: 'Abc.method2', 96 | urls: {} 97 | } ), false ); 98 | 99 | assert.equal( ifmgr._addProfile( { 100 | id: 'Abc.method2', 101 | ruleFile: 'unavailable.rule.json' 102 | } ), false ); 103 | } ); 104 | } ); 105 | 106 | describe( '#getInterfaceIdsByPrefix()', function() { 107 | it( 'should have nothing matched when the prefix is not proper', function() { 108 | assert.equal( interfaceManager.getInterfaceIdsByPrefix( 'Prefix' ).length, 0 ); 109 | } ); 110 | it ( 'should return an array of interface when the prefix is right', function() { 111 | assert.notEqual( interfaceManager.getInterfaceIdsByPrefix( 'Search.' ).length, 0 ); 112 | } ); 113 | } ); 114 | 115 | describe( '#isProfileExisted()', function() { 116 | it( 'should return false when the interface id does not exist', function() { 117 | assert.equal( interfaceManager.isProfileExisted( 'Search.getItem' ), false ); 118 | } ); 119 | it( 'should return true when the interface id exists', function() { 120 | assert.equal( interfaceManager.isProfileExisted( 'Search.getNav' ), true ); 121 | } ); 122 | } ); 123 | 124 | describe( '#_isUrlsValid()', function() { 125 | it( 'should return false when the urls is null or an empty object', function() { 126 | assert.equal( interfaceManager._isUrlsValid( null ), false ); 127 | assert.equal( interfaceManager._isUrlsValid( {} ), false ); 128 | } ); 129 | it( 'should return true when the urls is not null and has at least one url', function() { 130 | assert.equal( interfaceManager._isUrlsValid( { daily: 'http://url1' } ), true ); 131 | assert.equal( interfaceManager._isUrlsValid( { daily: 'http://url1', online: 'http://url2' } ), true ); 132 | } ); 133 | } ); 134 | 135 | describe( '#getProfile()', function() { 136 | it( 'should return undefined if the given id is not existed', function() { 137 | assert.strictEqual( interfaceManager.getProfile( 'Search.item' ), undefined ); 138 | } ); 139 | 140 | it( 'should renturn the profile if the given interface id is existed', function() { 141 | assert.equal( typeof interfaceManager.getProfile( 'Cart.getMyCart' ), 'object' ); 142 | } ); 143 | } ); 144 | 145 | describe( '#getRule()', function() { 146 | it( 'should return rule of the given id', function() { 147 | assert.equal( typeof interfaceManager.getRule( 'Search.list' ), 'object' ); 148 | } ); 149 | 150 | it( 'should throw error when the rule file does not exist', function() { 151 | assert.throws( function() { 152 | interfaceManager.getRule( 'Test.post' ); 153 | }, function( err ) { 154 | return err.toString() 155 | .indexOf( 'The rule file is not existed.' ) !== -1; 156 | } ); 157 | } ); 158 | 159 | it( 'should throw error when the rule file is not a json', function() { 160 | assert.throws( function() { 161 | interfaceManager.getRule( 'Search.suggest' ); 162 | }, function( err ) { 163 | return err.toString() 164 | .indexOf( 'Rule file has syntax error' ) !== -1; 165 | } ); 166 | } ); 167 | 168 | it( 'should throw error when the interface id is not found', function() { 169 | assert.throws( function() { 170 | interfaceManager.getRule( 'Error.id' ); 171 | }, function( err ) { 172 | return err.toString() 173 | .indexOf( 'is not found' ) !== -1; 174 | } ); 175 | } ); 176 | } ); 177 | 178 | describe( '#getEngine()', function() { 179 | it( 'should return mockjs', function() { 180 | assert.strictEqual( interfaceManager.getEngine(), 'mockjs' ); 181 | } ); 182 | } ); 183 | 184 | describe( '#getStatus()', function() { 185 | it( 'should return online', function() { 186 | assert.strictEqual( interfaceManager.getStatus(), 'online' ); 187 | } ); 188 | } ); 189 | 190 | it( 'clientInterfaces should be initalized after the object of interfaceManager is created', function() { 191 | var clientInterfaces = interfaceManager.getClientInterfaces(); 192 | assert.notEqual( clientInterfaces, null ); 193 | var cnt = 0; 194 | for ( var i in clientInterfaces ) cnt++; 195 | assert.equal( cnt, 7 ); 196 | } ); 197 | 198 | } ); -------------------------------------------------------------------------------- /tests/mockserver.js: -------------------------------------------------------------------------------- 1 | var express = require( 'express' ); 2 | var app = express(); 3 | 4 | app.post( '/post', function( req, res ) { 5 | var d = ''; 6 | req.on( 'data', function( chunk ) { 7 | d += chunk; 8 | } ); 9 | req.on( 'end', function() { 10 | console.log( d ); 11 | } ); 12 | res.send( 'this is the msg from mockserver!' ); 13 | } ); 14 | 15 | app.get( '/timeoutUrl', function( req, res ) { 16 | setTimeout( function() { 17 | res.send( 'sorry' ); 18 | }, 10000 ) 19 | } ) 20 | app.listen( 3001 ); -------------------------------------------------------------------------------- /tests/modelproxy.test.js: -------------------------------------------------------------------------------- 1 | var assert = require( 'assert' ); 2 | 3 | var ModelProxy = require( '../lib-cov/modelproxy' ); 4 | 5 | describe( 'ModelProxy', function() { 6 | describe( '#init()', function() { 7 | it( 'throws exception when the path of the interface is not right', function() { 8 | assert.throws( function() { 9 | ModelProxy.init( 'error/path/interface_test.json' ); 10 | }, function( err ) { 11 | return err.toString().indexOf( 'no such file or directory' ) !== -1; 12 | } ); 13 | } ); 14 | } ); 15 | ModelProxy.init( '../tests/interface_test.json' ); 16 | describe( '#create()', function() { 17 | it( 'should return an object with methods specified by the profile', function() { 18 | var m = ModelProxy.create( 'Search.suggest' ); 19 | assert( typeof m.suggest === 'function' ); 20 | 21 | m = ModelProxy.create( [ 'Search.suggest', 'Cart.getMyCart' ] ); 22 | assert( typeof m.suggest === 'function' ); 23 | assert( typeof m.getMyCart === 'function' ); 24 | 25 | m = ModelProxy.create( { 26 | suggest: 'Search.suggest', 27 | getCart: 'Cart.getMyCart' 28 | } ); 29 | assert( typeof m.suggest === 'function' ); 30 | assert( typeof m.getCart === 'function' ); 31 | 32 | m = ModelProxy.create( 'Search.*' ); 33 | assert( typeof m.list === 'function' ); 34 | assert( typeof m.suggest === 'function' ); 35 | assert( typeof m.getNav === 'function' ); 36 | 37 | m = ModelProxy.create( [ 'Search.getNav', 'D.getNav' ] ); 38 | assert( typeof m.Search_getNav === 'function' ); 39 | assert( typeof m.getNav === 'function' ); 40 | 41 | } ); 42 | 43 | it('should throw exception if the specified interface id does not exist', function() { 44 | assert.throws( function() { 45 | var m = ModelProxy.create( 'Search.getItems' ); 46 | }, function( err ) { 47 | return err.toString().indexOf( 'Invalid interface id' ) !== -1; 48 | } ); 49 | assert.throws( function() { 50 | var m = ModelProxy.create( ['Search.getItems'] ); 51 | }, function( err ) { 52 | return err.toString().indexOf( 'Invalid interface id' ) !== -1; 53 | } ); 54 | 55 | assert.throws( function() { 56 | var m = ModelProxy.create( { 57 | getItems: 'Search.getItems', 58 | getMyCart: 'Cart.getMyCart' 59 | } ); 60 | }, function( err ) { 61 | return err.toString().indexOf( 'Invalid interface id' ) !== -1; 62 | } ); 63 | } ); 64 | } ); 65 | 66 | } ); 67 | 68 | describe( 'modelProxy', function() { 69 | 70 | describe( '#method()', function() { 71 | var m = ModelProxy.create( 'Search.suggest' ); 72 | it( 'should return itself', function() { 73 | var m = ModelProxy.create( 'Search.suggest' ); 74 | assert( m.suggest( { q: 'i'} ) === m ); 75 | } ); 76 | } ); 77 | 78 | describe( '#done()', function() { 79 | var m = ModelProxy.create( 'Search.*' ); 80 | it( 'should send all requests pushed before done and fetch the corresponding data into the callback' 81 | , function( done ) { 82 | m.suggest( {q: 'i'} ) 83 | .list( {q: 'i'} ) 84 | .done( function( data1, data2 ) { 85 | assert( typeof data1 === 'object' ); 86 | assert( typeof data2 === 'object' ); 87 | done(); 88 | } ); 89 | } ); 90 | 91 | it( 'should catch error when the request has error', function( done ) { 92 | var m = ModelProxy.create( 'Cart.*' ); 93 | m.getMyCart() 94 | .withCookie( 'a=b' ) 95 | .done( function( data ) { 96 | 97 | }, function( err ) { 98 | assert( err instanceof Error ); 99 | done(); 100 | } ); 101 | } ); 102 | 103 | it( 'should callback directly if no method is called before calling done()', function( done ) { 104 | var m = ModelProxy.create( 'Cart.*' ) 105 | m.done( function( nothing ) { 106 | assert( nothing === undefined ); 107 | done(); 108 | } ); 109 | } ); 110 | 111 | it( 'should output the error when no errCallback is specified', function() { 112 | var m = ModelProxy.create( 'Cart.*' ); 113 | m.getMyCart( {q:'a',key: 'b'} ) 114 | .withCookie( 'a=b' ) 115 | .done( function( data ) { 116 | 117 | } ); 118 | } ); 119 | 120 | it( 'should write the data into request body when the method type is POST', function( done ) { 121 | var model = ModelProxy.create( 'Test.post' ); 122 | model.post( { 123 | a: 'abc', 124 | b: 'bcd', 125 | c: '{"a":"b"}' 126 | } ).done( function( data ) { 127 | assert.strictEqual( data, 'this is the msg from mockserver!' ); 128 | done(); 129 | } ); 130 | } ); 131 | 132 | it( 'should inspire errCallback due to the response is timeout', function( done ) { 133 | var model = ModelProxy.create( { 134 | 'getData': 'Test.timeoutUrl' 135 | } ); 136 | 137 | model.getData() 138 | .done( function( data ) { 139 | }, function( err ) { 140 | console.log( err + 'timeout, timeout timeout timeout' ); 141 | done(); 142 | } ) 143 | .error( function( err ) { 144 | console.log( err ); 145 | done(); 146 | } ); 147 | 148 | } ); 149 | 150 | } ); 151 | 152 | describe( '#withCookie()', function() { 153 | it( 'should be called to set cookie before request the interface which is cookie needed' 154 | , function( done ) { 155 | var m = ModelProxy.create( 'Cart.*' ); 156 | var cookie = 'ali_ab=42.120.74.193.1395041649126.7; l=c%E6%B5%8B%E8%AF%95%E8%B4%A6%E5%8F%B7135::1395387929931::11; cna=KcVJCxpk1XkCAX136Nv5aaC4; _tb_token_=DcE1K7Gbq9n; x=e%3D1%26p%3D*%26s%3D0%26c%3D0%26f%3D0%26g%3D0%26t%3D0%26__ll%3D-1%26_ato%3D0; whl=-1%260%260%260; ck1=; lzstat_uv=16278696413116954092|2511607@2511780@2738597@3258521@878758@2735853@2735859@2735862@2735864@2341454@2868200@2898598; lzstat_ss=3744453007_0_1396468901_2868200|970990289_0_1396468901_2898598; uc3=nk2=AKigXc46EgNui%2FwL&id2=Vy%2BbYvqj0fGT&vt3=F8dHqR%2F5HOhOUWkAFIo%3D&lg2=UtASsssmOIJ0bQ%3D%3D; lgc=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; tracknick=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; _cc_=U%2BGCWk%2F7og%3D%3D; tg=0; mt=ci=3_1&cyk=0_0; cookie2=1c5db2f359099faff00e14d7f39e16f2; t=e8bd0dbbf4bdb8f3704a1974b8a166b5; v=0; uc1=cookie14=UoLVYyvcdJF0aw%3D%3D'; 157 | m.getMyCart() 158 | .withCookie( cookie ) 159 | .done( function( data ) { 160 | done(); 161 | }, function( err ) { 162 | console.log( err ); 163 | done(); 164 | } ); 165 | } ); 166 | } ); 167 | 168 | describe( '#error()', function() { 169 | it( 'should specify the final error callback if there is no errCallback specified when done() is called' 170 | , function( done ) { 171 | var m = ModelProxy.create( 'Cart.*' ); 172 | m.getMyCart() 173 | .withCookie( 'a=b' ) 174 | .done( function( data ) { 175 | 176 | } ).error( function( err ) { 177 | done(); 178 | } ); 179 | } ); 180 | } ); 181 | 182 | } ); -------------------------------------------------------------------------------- /tests/proxyfactory.test.js: -------------------------------------------------------------------------------- 1 | var assert = require( 'assert' ); 2 | 3 | var ProxyFactory = require( '../lib-cov/proxyfactory' ); 4 | var InterfaceManager = require( '../lib-cov/interfacemanager' ); 5 | 6 | var ifmgr = new InterfaceManager( '../tests/interface_test.json' ); 7 | var cookie = 'ali_ab=42.120.74.193.1395041649126.7; l=c%E6%B5%8B%E8%AF%95%E8%B4%A6%E5%8F%B7135::1395387929931::11; cna=KcVJCxpk1XkCAX136Nv5aaC4; _tb_token_=DcE1K7Gbq9n; x=e%3D1%26p%3D*%26s%3D0%26c%3D0%26f%3D0%26g%3D0%26t%3D0%26__ll%3D-1%26_ato%3D0; whl=-1%260%260%260; ck1=; lzstat_uv=16278696413116954092|2511607@2511780@2738597@3258521@878758@2735853@2735859@2735862@2735864@2341454@2868200@2898598; lzstat_ss=3744453007_0_1396468901_2868200|970990289_0_1396468901_2898598; uc3=nk2=AKigXc46EgNui%2FwL&id2=Vy%2BbYvqj0fGT&vt3=F8dHqR%2F5HOhOUWkAFIo%3D&lg2=UtASsssmOIJ0bQ%3D%3D; lgc=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; tracknick=c%5Cu6D4B%5Cu8BD5%5Cu8D26%5Cu53F7135; _cc_=U%2BGCWk%2F7og%3D%3D; tg=0; mt=ci=3_1&cyk=0_0; cookie2=1c5db2f359099faff00e14d7f39e16f2; t=e8bd0dbbf4bdb8f3704a1974b8a166b5; v=0; uc1=cookie14=UoLVYyvcdJF0aw%3D%3D'; 8 | 9 | describe( 'ProxyFactory', function() { 10 | it( 'can only use object of InterfaceManager to initial the factory', function() { 11 | assert.throws( function() { 12 | ProxyFactory.use( {} ); 13 | }, function( err ) { 14 | return err.toString() 15 | .indexOf( 'Proxy can only use instance of InterfacefManager' ) !== -1 16 | } ); 17 | } ); 18 | ProxyFactory.use( ifmgr ); 19 | 20 | describe( '#getMockEngine()', function() { 21 | it( 'should renturn engine object specified by the field of engine in interface configuration' 22 | , function() { 23 | assert( typeof ProxyFactory.getMockEngine().mock === 'function' ); 24 | } ); 25 | } ); 26 | 27 | describe( '#getInterfaceIdsByPrefix()', function() { 28 | it( 'should return an id array', function() { 29 | assert.equal( ProxyFactory.getInterfaceIdsByPrefix( 'Search.' ).length, 3 ); 30 | } ); 31 | } ); 32 | 33 | describe( '#getRule()', function() { 34 | it( 'should return a rule object of this interface', function() { 35 | assert( typeof ProxyFactory.getRule( 'Search.list' ) === 'object' ); 36 | } ); 37 | 38 | } ); 39 | 40 | describe( '#create()', function() { 41 | it( 'should throw exception when the interface id is invalid', function() { 42 | assert.throws( function() { 43 | ProxyFactory.create( 'Search.getItems' ); 44 | }, function( err ) { 45 | return err.toString() 46 | .indexOf( 'Invalid interface id: Search.getItems' ) !== -1; 47 | } ); 48 | } ); 49 | } ); 50 | 51 | describe( '#Interceptor()', function() { 52 | it( 'should intercept the request which interface id is matched', function( done ) { 53 | var req = { 54 | headers: { 55 | cookie: '' 56 | }, 57 | url: '/Search.suggest?q=a', 58 | on: function( eventName, callback ) { 59 | if ( eventName === 'data' ) { 60 | callback( 'mock chunk' ); 61 | } else if ( eventName === 'end' ) { 62 | callback(); 63 | } 64 | } 65 | }; 66 | var res = { 67 | headers: { 68 | 69 | }, 70 | end: function( data ) { 71 | assert.notEqual( data.length, 0 ); 72 | done(); 73 | }, 74 | setHeader: function( key, value ) { 75 | this.headers[key] = value; 76 | }, 77 | on: function( eventName, callback ) { 78 | if ( eventName === 'data' ) { 79 | callback( 'mock chunk' ); 80 | } else if ( eventName === 'end' ) { 81 | callback(); 82 | } 83 | } 84 | }; 85 | ProxyFactory.Interceptor( req, res ); 86 | } ); 87 | 88 | it( 'should response 404 when the interface id is not matched', function() { 89 | var req = { 90 | headers: { 91 | cookie: '' 92 | }, 93 | url: '/Search.what?q=a' 94 | }; 95 | var res = { 96 | headers: { 97 | 98 | }, 99 | end: function( data ) { 100 | assert.strictEqual( this.statusCode, 404 ); 101 | }, 102 | setHeader: function( key, value ) { 103 | this.headers[key] = value; 104 | } 105 | }; 106 | ProxyFactory.Interceptor( req, res ); 107 | } ); 108 | 109 | it( 'should response 404 when the interface id is matched but the intercepted field is configurated as false' 110 | , function() { 111 | var req = { 112 | headers: { 113 | cookie: '' 114 | }, 115 | url: '/D.getNav?q=c' 116 | }; 117 | var res = { 118 | headers: { 119 | 120 | }, 121 | end: function( data ) { 122 | assert.strictEqual( this.statusCode, 404 ); 123 | }, 124 | setHeader: function( key, value ) { 125 | this.headers[key] = value; 126 | } 127 | }; 128 | ProxyFactory.Interceptor( req, res ); 129 | } ); 130 | 131 | it( 'should response client interfaces', function( done ) { 132 | var req = { 133 | headers: { 134 | cookie: '' 135 | }, 136 | url: '/$interfaces', 137 | on: function( eventName, callback ) { 138 | if ( eventName === 'data' ) { 139 | callback( 'mock chunk' ); 140 | } else if ( eventName === 'end' ) { 141 | callback(); 142 | } 143 | } 144 | }; 145 | var res = { 146 | end: function( data ) { 147 | assert.notEqual( data.length, 0 ); 148 | done(); 149 | } 150 | }; 151 | ProxyFactory.Interceptor( req, res ); 152 | } ) 153 | 154 | } ); 155 | 156 | } ); 157 | 158 | var Proxy = ProxyFactory; 159 | 160 | describe( 'Proxy', function() { 161 | 162 | it( 'should construct a new Proxy object', function() { 163 | var p = new Proxy( { 164 | id: 'Search.getItems', 165 | urls: { 166 | online: 'http://www.modelproxy.com' 167 | }, 168 | status: 'online' 169 | } ); 170 | assert( p instanceof Proxy ); 171 | console.log( p._opt ); 172 | } ); 173 | 174 | it( 'should throw error when no url is available', function() { 175 | assert.throws( function() { 176 | var p = new Proxy( { 177 | id: 'Search.getItems', 178 | status: 'online' 179 | } ); 180 | }, function( err ) { 181 | return err.toString() 182 | .indexOf( 'No url can be proxied!' ) !== -1; 183 | } ); 184 | 185 | assert.throws( function() { 186 | var p = new Proxy( { 187 | id: 'Search.getItems', 188 | urls: { 189 | online: 'http://www.modelproxy.com' 190 | } 191 | } ); 192 | }, function( err ) { 193 | return err.toString() 194 | .indexOf( 'No url can be proxied!' ) !== -1; 195 | } ); 196 | } ); 197 | 198 | describe( '#getOption()', function() { 199 | it( 'should return status of this proxy', function() { 200 | var p = new Proxy( { 201 | id: 'Search.getItems', 202 | status: 'online', 203 | urls: { 204 | online: 'http://www.modelproxy.com' 205 | } 206 | } ); 207 | assert.strictEqual( p.getOption( 'id' ), 'Search.getItems' ); 208 | } ); 209 | } ); 210 | 211 | describe( '#_queryStringify()', function() { 212 | var p = new Proxy( { 213 | id: 'Search.getItems', 214 | status: 'online', 215 | urls: { 216 | online: 'http://www.modelproxy.com' 217 | } 218 | } ); 219 | 220 | it( 'should return empty string if the params is null undefined', function() { 221 | assert.strictEqual( p._queryStringify( null ), '' ); 222 | assert.strictEqual( p._queryStringify( undefined ), '' ); 223 | } ); 224 | 225 | it( 'should return the params itself if the type of params is string', function() { 226 | assert.strictEqual( p._queryStringify( 'a=b&c=d' ), 'a=b&c=d'); 227 | } ); 228 | 229 | it( 'should return the joined string with & if the params is instance of Array', function() { 230 | assert.strictEqual( p._queryStringify( [ 'a=b', 'c=d' ] ), 'a=b&c=d' ); 231 | assert.strictEqual( p._queryStringify( [] ), '' ); 232 | } ); 233 | 234 | it( 'should return the joined string with & if the params is a key-value object', function() { 235 | assert.strictEqual( p._queryStringify( {a:'b', c:'d'} ), 'a=b&c=d' ); 236 | assert.strictEqual( p._queryStringify( {} ), '' ); 237 | assert.strictEqual( p._queryStringify( {a:'b', c:"{'d':'f'}"} ) 238 | , "a=b&c=" + encodeURIComponent("{'d':'f'}") ); 239 | assert.strictEqual( p._queryStringify( {a:'b', c:"['d','e']"} ) 240 | , "a=b&c=" + encodeURIComponent("['d','e']") ); 241 | } ); 242 | } ); 243 | 244 | describe( '#interceptRequest()', function() { 245 | it( 'should response mock data if the proxy status is mock', function( done ) { 246 | var p = new Proxy( { 247 | "name": "我的购物车", 248 | "id": "Cart.getMyCart", 249 | "urls": { 250 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 251 | }, 252 | "status": "mock" 253 | } ); 254 | var res = { 255 | end: function( data ) { 256 | assert.equal( typeof data, 'string' ); 257 | done(); 258 | } 259 | } 260 | p.interceptRequest( null, res ); 261 | } ); 262 | 263 | it( 'should response mockerr data if the proxy status is mockerr.', function( done ) { 264 | var p = new Proxy( { 265 | "name": "我的购物车", 266 | "id": "Cart.getMyCart", 267 | "urls": { 268 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 269 | }, 270 | "ruleFile": "Cart.getCart.rule.json", 271 | "status": "mockerr" 272 | } ); 273 | var res = { 274 | end: function( data ) { 275 | assert.notEqual( data.indexOf( 'this is error data' ), -1 ); 276 | done(); 277 | } 278 | } 279 | p.interceptRequest( null, res ); 280 | 281 | } ); 282 | 283 | it( 'should response error msg and set the status code as 500 when there is error.' 284 | , function( done ) { 285 | var p = new Proxy( { 286 | "name": "我的购物车1", 287 | "id": "Cart.getCart1", 288 | "urls": { 289 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 290 | }, 291 | "status": "mock" 292 | } ); 293 | var res = { 294 | end: function( data ) { 295 | assert.strictEqual( this.statusCode, 500 ); 296 | done(); 297 | } 298 | }; 299 | p.interceptRequest( null, res ); 300 | } ); 301 | 302 | it( 'should intercept the request and response data', function( done ) { 303 | var p = ProxyFactory.create( 'Search.suggest' ); 304 | var req = { 305 | headers: { 306 | cookie: '' 307 | }, 308 | url: 'Search.suggest?q=a', 309 | on: function( eventName, callback ) { 310 | if ( eventName === 'data' ) { 311 | callback( 'mock chunk' ); 312 | } else if ( eventName === 'end' ) { 313 | callback(); 314 | } 315 | } 316 | }; 317 | var res = { 318 | headers: { 319 | 320 | }, 321 | end: function( data ) { 322 | assert.notEqual( data.length, 0 ); 323 | assert.notEqual( typeof this.headers['Content-Type'], 'undefined' ); 324 | done(); 325 | }, 326 | setHeader: function( key, value ) { 327 | this.headers[key] = value; 328 | } 329 | }; 330 | p.interceptRequest( req, res ); 331 | 332 | } ); 333 | 334 | } ); 335 | 336 | 337 | describe( '#request()', function() { 338 | var p = ProxyFactory.create( 'Search.suggest' ); 339 | it( 'should get result from the remote', function( done ) { 340 | p.request( {q: 'i'}, function( result ) { 341 | done(); 342 | } ); 343 | } ); 344 | 345 | it( 'should get the result from mock', function( done ) { 346 | ProxyFactory.create( 'Search.getNav' ) 347 | .request( {q: 'i'}, function( result ) { 348 | assert.strictEqual( result, 'This is a mock text' ); 349 | done(); 350 | } ); 351 | } ); 352 | 353 | it( 'should get the result from mockerr', function( done ) { 354 | new Proxy( { 355 | 'name': '导航获取接口', 356 | 'id': 'Search.getNav', 357 | 'urls': { 358 | 'online': 'http://s.m.taobao.com/client/search.do' 359 | }, 360 | 'status': 'mockerr', 361 | 'isRuleStatic': true 362 | } ).request( null, function( result ) { 363 | assert.strictEqual( result, 'This is a mock error' ); 364 | done(); 365 | } ); 366 | } ); 367 | 368 | it( 'should get the mocked result by mockEngine', function( done ) { 369 | ProxyFactory.create( 'Search.list' ) 370 | .request( {q: 'i'}, function( result ) { 371 | assert.strictEqual( typeof result, 'object' ); 372 | done(); 373 | } ); 374 | } ); 375 | 376 | it( 'should get a buffer if the encoding of the result is set as raw', function( done ) { 377 | new Proxy( { 378 | 'name': '热词推荐接口', 379 | 'id': 'Search.suggest', 380 | 'urls': { 381 | 'online': 'http://suggest.taobao.com/sug' 382 | }, 383 | 'encoding': 'raw', 384 | 'status': 'online' 385 | } ).request( {q: 'i'}, function( result ) { 386 | assert( typeof result === 'object' ); 387 | done(); 388 | } ); 389 | 390 | } ); 391 | 392 | it( 'should get a json if the data type of the result is set as json', function( done ) { 393 | new Proxy( { 394 | 'name': '热词推荐接口', 395 | 'id': 'Search.suggest', 396 | 'urls': { 397 | 'online': 'http://suggest.taobao.com/sug' 398 | }, 399 | 'dataType': 'json', 400 | 'status': 'online' 401 | } ).request( {q: 'i'}, function( result ) { 402 | assert( typeof result === 'object' ); 403 | done(); 404 | } ); 405 | } ); 406 | 407 | it( 'should get a string if the data type of the result is set as text', function( done ) { 408 | new Proxy( { 409 | 'name': '热词推荐接口', 410 | 'id': 'Search.suggest', 411 | 'urls': { 412 | 'online': 'http://suggest.taobao.com/sug' 413 | }, 414 | 'dataType': 'text', 415 | 'status': 'online' 416 | } ).request( {q: 'i'}, function( result ) { 417 | assert( typeof result === 'string' ); 418 | done(); 419 | } ); 420 | } ); 421 | 422 | it( 'should get nothing if the request is cookie needed but no cookie is set for this request', 423 | function( done ) { 424 | 425 | new Proxy( { 426 | "name": "我的购物车", 427 | "id": "Cart.getMyCart", 428 | "urls": { 429 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 430 | }, 431 | "status": "online", 432 | "encoding": "gbk" 433 | } ).request( {q: 'i'}, function( result ) { 434 | assert( result === '' ); 435 | done(); 436 | }, function( err ) { 437 | console.log( err ); 438 | }, cookie ); 439 | 440 | new Proxy( { 441 | "name": "我的购物车", 442 | "id": "Cart.getMyCart", 443 | "urls": { 444 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 445 | }, 446 | "status": "online", 447 | "encoding": "gbk" 448 | } ).request( {q: 'i'}, function( result ) { 449 | console.log( result ); 450 | assert( !result ); 451 | done(); 452 | }, function( err ) { 453 | console.log( err ); 454 | } ); 455 | } ); 456 | 457 | it( 'should throw exception if the isCookieNeeded is set as true but no cookie is set for this request', 458 | function() { 459 | assert.throws( function() { 460 | new Proxy( { 461 | "name": "我的购物车", 462 | "id": "Cart.getMyCart", 463 | "urls": { 464 | "online": "http://cart.taobao.com/json/asyncGetMyCart.do" 465 | }, 466 | "status": "online", 467 | "encoding": "gbk", 468 | "isCookieNeeded": true 469 | } ).request( {q: 'i'}, function( result ) { 470 | console.log( result ); 471 | done(); 472 | }, function( err ) { 473 | console.log( err ); 474 | } ); 475 | }, function( err ) { 476 | return err.toString().indexOf( 'This request is cookie needed' ) !== -1; 477 | } ); 478 | } ); 479 | } ); 480 | 481 | } ); 482 | 483 | 484 | --------------------------------------------------------------------------------