sunyj 9 жил өмнө
parent
commit
35fb4ebcef
100 өөрчлөгдсөн 14408 нэмэгдсэн , 0 устгасан
  1. 771 0
      mall-search/init.sql
  2. 141 0
      mall-search/pom.xml
  3. 43 0
      mall-search/src/main/java/com/uas/search/Application.java
  4. 93 0
      mall-search/src/main/java/com/uas/search/DictionaryHelper.java
  5. 133 0
      mall-search/src/main/java/com/uas/search/DruidDBConfiguration.java
  6. 37 0
      mall-search/src/main/java/com/uas/search/LuceneProperties.java
  7. 29 0
      mall-search/src/main/java/com/uas/search/SystemProperties.java
  8. 73 0
      mall-search/src/main/java/com/uas/search/WebAppConfiguration.java
  9. 116 0
      mall-search/src/main/java/com/uas/search/analyzer/DefaultConfig.java
  10. 56 0
      mall-search/src/main/java/com/uas/search/analyzer/IKAnalyzer.java
  11. 60 0
      mall-search/src/main/java/com/uas/search/analyzer/IKTokenizer.java
  12. 24 0
      mall-search/src/main/java/com/uas/search/annotation/NotEmpty.java
  13. 111 0
      mall-search/src/main/java/com/uas/search/aop/ArgumentsCheckAspect.java
  14. 70 0
      mall-search/src/main/java/com/uas/search/aop/ExceptionHandlerAdvice.java
  15. 83 0
      mall-search/src/main/java/com/uas/search/aop/WebLogAspect.java
  16. 212 0
      mall-search/src/main/java/com/uas/search/constant/SearchConstants.java
  17. 66 0
      mall-search/src/main/java/com/uas/search/constant/model/BaseOrder.java
  18. 71 0
      mall-search/src/main/java/com/uas/search/constant/model/BaseOrderDetail.java
  19. 31 0
      mall-search/src/main/java/com/uas/search/constant/model/CollectField.java
  20. 107 0
      mall-search/src/main/java/com/uas/search/constant/model/PageInfo.java
  21. 236 0
      mall-search/src/main/java/com/uas/search/constant/model/PageParams.java
  22. 100 0
      mall-search/src/main/java/com/uas/search/constant/model/SPage.java
  23. 105 0
      mall-search/src/main/java/com/uas/search/constant/model/Sort.java
  24. 137 0
      mall-search/src/main/java/com/uas/search/controller/IndexController.java
  25. 139 0
      mall-search/src/main/java/com/uas/search/controller/OrderSearchController.java
  26. 54 0
      mall-search/src/main/java/com/uas/search/controller/ScheduleController.java
  27. 325 0
      mall-search/src/main/java/com/uas/search/controller/SearchController.java
  28. 52 0
      mall-search/src/main/java/com/uas/search/controller/UploadController.java
  29. 24 0
      mall-search/src/main/java/com/uas/search/dao/BrandDao.java
  30. 18 0
      mall-search/src/main/java/com/uas/search/dao/ComponentDao.java
  31. 125 0
      mall-search/src/main/java/com/uas/search/dao/GoodsDao.java
  32. 24 0
      mall-search/src/main/java/com/uas/search/dao/KindDao.java
  33. 23 0
      mall-search/src/main/java/com/uas/search/dao/OrderDao.java
  34. 23 0
      mall-search/src/main/java/com/uas/search/dao/OrderInvoiceDao.java
  35. 14 0
      mall-search/src/main/java/com/uas/search/dao/ProductsDao.java
  36. 23 0
      mall-search/src/main/java/com/uas/search/dao/PurchaseDao.java
  37. 23 0
      mall-search/src/main/java/com/uas/search/dao/PurchaseInvoiceDao.java
  38. 18 0
      mall-search/src/main/java/com/uas/search/dao/StoreDao.java
  39. 20 0
      mall-search/src/main/java/com/uas/search/dao/TradeGoodsDao.java
  40. 90 0
      mall-search/src/main/java/com/uas/search/exception/SearchException.java
  41. 101 0
      mall-search/src/main/java/com/uas/search/grouping/DistinctGroupCollector.java
  42. 77 0
      mall-search/src/main/java/com/uas/search/grouping/GoodsGroupCollector.java
  43. 152 0
      mall-search/src/main/java/com/uas/search/jms/JmsListener.java
  44. 153 0
      mall-search/src/main/java/com/uas/search/jms/LuceneMessage.java
  45. 46 0
      mall-search/src/main/java/com/uas/search/jms/LuceneMessageDao.java
  46. 23 0
      mall-search/src/main/java/com/uas/search/jms/LuceneMessageService.java
  47. 68 0
      mall-search/src/main/java/com/uas/search/jms/LuceneMessageServiceImpl.java
  48. 253 0
      mall-search/src/main/java/com/uas/search/jms/QueueMessageParser.java
  49. 73 0
      mall-search/src/main/java/com/uas/search/model/BaseOrderDetail.java
  50. 135 0
      mall-search/src/main/java/com/uas/search/model/Brand.java
  51. 231 0
      mall-search/src/main/java/com/uas/search/model/Component.java
  52. 58 0
      mall-search/src/main/java/com/uas/search/model/Enterprise.java
  53. 86 0
      mall-search/src/main/java/com/uas/search/model/Goods.java
  54. 120 0
      mall-search/src/main/java/com/uas/search/model/Kind.java
  55. 176 0
      mall-search/src/main/java/com/uas/search/model/Order.java
  56. 119 0
      mall-search/src/main/java/com/uas/search/model/OrderDetail.java
  57. 156 0
      mall-search/src/main/java/com/uas/search/model/OrderInvoice.java
  58. 119 0
      mall-search/src/main/java/com/uas/search/model/OrderInvoiceDetail.java
  59. 86 0
      mall-search/src/main/java/com/uas/search/model/ParsedQueueMessage.java
  60. 57 0
      mall-search/src/main/java/com/uas/search/model/Products.java
  61. 68 0
      mall-search/src/main/java/com/uas/search/model/Property.java
  62. 120 0
      mall-search/src/main/java/com/uas/search/model/PropertyValue.java
  63. 138 0
      mall-search/src/main/java/com/uas/search/model/Purchase.java
  64. 119 0
      mall-search/src/main/java/com/uas/search/model/PurchaseDetail.java
  65. 140 0
      mall-search/src/main/java/com/uas/search/model/PurchaseInvoice.java
  66. 119 0
      mall-search/src/main/java/com/uas/search/model/PurchaseInvoiceDetail.java
  67. 70 0
      mall-search/src/main/java/com/uas/search/model/Store.java
  68. 179 0
      mall-search/src/main/java/com/uas/search/model/TradeGoods.java
  69. 17 0
      mall-search/src/main/java/com/uas/search/schedule/Executable.java
  70. 156 0
      mall-search/src/main/java/com/uas/search/schedule/TaskInformation.java
  71. 74 0
      mall-search/src/main/java/com/uas/search/schedule/TaskLog.java
  72. 65 0
      mall-search/src/main/java/com/uas/search/schedule/TaskService.java
  73. 210 0
      mall-search/src/main/java/com/uas/search/schedule/TaskServiceImpl.java
  74. 99 0
      mall-search/src/main/java/com/uas/search/service/IndexService.java
  75. 208 0
      mall-search/src/main/java/com/uas/search/service/OrderSearchService.java
  76. 429 0
      mall-search/src/main/java/com/uas/search/service/SearchService.java
  77. 687 0
      mall-search/src/main/java/com/uas/search/service/impl/IndexServiceImpl.java
  78. 411 0
      mall-search/src/main/java/com/uas/search/service/impl/OrderSearchServiceImpl.java
  79. 1349 0
      mall-search/src/main/java/com/uas/search/service/impl/SearchServiceImpl.java
  80. 271 0
      mall-search/src/main/java/com/uas/search/sort/CustomSimilarity.java
  81. 97 0
      mall-search/src/main/java/com/uas/search/sort/DefaultFieldComparator.java
  82. 73 0
      mall-search/src/main/java/com/uas/search/sort/SimilarValuesFieldComparatorSource.java
  83. 127 0
      mall-search/src/main/java/com/uas/search/sort/StringFieldComparatorSource.java
  84. 389 0
      mall-search/src/main/java/com/uas/search/support/DownloadHelper.java
  85. 144 0
      mall-search/src/main/java/com/uas/search/support/DownloadService.java
  86. 245 0
      mall-search/src/main/java/com/uas/search/support/IndexSearcherManager.java
  87. 113 0
      mall-search/src/main/java/com/uas/search/support/IndexWriterManager.java
  88. 46 0
      mall-search/src/main/java/com/uas/search/util/ArrayUtils.java
  89. 27 0
      mall-search/src/main/java/com/uas/search/util/CharUtils.java
  90. 21 0
      mall-search/src/main/java/com/uas/search/util/CollectionUtils.java
  91. 77 0
      mall-search/src/main/java/com/uas/search/util/ContextUtils.java
  92. 423 0
      mall-search/src/main/java/com/uas/search/util/DocumentToObjectUtils.java
  93. 44 0
      mall-search/src/main/java/com/uas/search/util/ExceptionUtils.java
  94. 52 0
      mall-search/src/main/java/com/uas/search/util/FileUtils.java
  95. 56 0
      mall-search/src/main/java/com/uas/search/util/IpHelper.java
  96. 36 0
      mall-search/src/main/java/com/uas/search/util/NumberGenerator.java
  97. 507 0
      mall-search/src/main/java/com/uas/search/util/ObjectToDocumentUtils.java
  98. 183 0
      mall-search/src/main/java/com/uas/search/util/ObjectUtils.java
  99. 182 0
      mall-search/src/main/java/com/uas/search/util/OrderSearchUtils.java
  100. 448 0
      mall-search/src/main/java/com/uas/search/util/SearchUtils.java

+ 771 - 0
mall-search/init.sql

@@ -0,0 +1,771 @@
+-- 创建消息表
+drop table if exists lucene$message;
+create table lucene$message(
+    me_id bigint not null primary key auto_increment,
+    me_table_name varchar(64) not null,
+    me_method_type varchar(6) not null,
+    me_data_id int not null,
+    me_data text,
+    me_priority int default 0,
+    me_retry_count int default 0,
+    me_create_time datetime not null
+);
+
+-- 创建消息历史表
+drop table if exists lucene$message_history;
+create table lucene$message_history(
+    mh_id bigint not null primary key auto_increment,
+    mh_dequeue_time datetime not null,
+    me_id bigint not null,
+    me_table_name varchar(64) not null,
+    me_method_type varchar(6) not null,
+    me_data_id int not null,
+    me_data text,
+    me_priority int default 0,
+    me_create_time datetime not null
+);
+
+-- 创建存储过程 入队消息
+drop procedure if exists enqueue_lucene_message;
+delimiter $$
+create procedure enqueue_lucene_message(in v_table_name varchar(64), v_method_type varchar(6), v_data_id int, v_data text)
+begin
+	insert into lucene$message (me_table_name, me_method_type, me_data_id, me_data, me_create_time) values(v_table_name, v_method_type, v_data_id, v_data, sysdate());
+end;$$
+delimiter ;
+
+-- 创建存储过程 出队消息
+drop procedure if exists dequeue_lucene_message;
+delimiter $$
+create procedure dequeue_lucene_message(in v_id bigint)
+begin
+	insert into lucene$message_history (mh_dequeue_time, me_id, me_table_name, me_method_type, me_data_id, me_data, me_priority, me_create_time) select sysdate(), me_id, me_table_name, me_method_type, me_data_id, me_data, me_priority, me_create_time from lucene$message where me_id = v_id;
+	delete from lucene$message where me_id = v_id;
+end;$$
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$brand
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_brand_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_brand_i` AFTER INSERT ON `product$brand` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$brand';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.br_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$brand
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_brand_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_brand_u` AFTER UPDATE ON `product$brand` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$brand';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.br_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$brand
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_brand_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_brand_d` AFTER DELETE ON `product$brand` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$brand';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.br_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$component
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_component_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_component_i` AFTER INSERT ON `product$component` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$component';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.cmp_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+    
+    set v_table_name='v$product$cmpgoods';
+    set v_data='cmpId';
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$component
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_component_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_component_u` AFTER UPDATE ON `product$component` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$component';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.cmp_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+    
+    set v_table_name='v$product$cmpgoods';
+    set v_data='cmpId';
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$component
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_component_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_component_d` AFTER DELETE ON `product$component` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$component';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.cmp_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+    
+    set v_table_name='v$product$cmpgoods';
+    set v_data='cmpId';
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$kind
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_kind_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_kind_i` AFTER INSERT ON `product$kind` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$kind';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.ki_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$kind
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_kind_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_kind_u` AFTER UPDATE ON `product$kind` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$kind';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.ki_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$kind
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_kind_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_kind_d` AFTER DELETE ON `product$kind` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$kind';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.ki_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$propertyvalue
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_propertyvalue_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_propertyvalue_i` AFTER INSERT ON `product$propertyvalue` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$component';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.pv_componentid;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$propertyvalue
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_propertyvalue_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_propertyvalue_u` AFTER UPDATE ON `product$propertyvalue` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$component';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.pv_componentid;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table product$propertyvalue
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_propertyvalue_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_propertyvalue_d` AFTER DELETE ON `product$propertyvalue` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'product$component';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.pv_componentid;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table sec$enterprises
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_enterprises_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_enterprises_i` AFTER INSERT ON `sec$enterprises` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'sec$enterprises';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.en_uu;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table sec$enterprises
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_enterprises_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_enterprises_u` AFTER UPDATE ON `sec$enterprises` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'sec$enterprises';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.en_uu;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table sec$enterprises
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_enterprises_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_enterprises_d` AFTER DELETE ON `sec$enterprises` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'sec$enterprises';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.en_uu;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$goods
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_trade_goods_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_trade_goods_i` AFTER INSERT ON `trade$goods` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'v$product$cmpgoods';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default 'goId';
+    
+    set v_data_id=new.go_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$goods
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_trade_goods_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_trade_goods_u` AFTER UPDATE ON `trade$goods` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'v$product$cmpgoods';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default 'goId';
+    
+    set v_data_id=old.go_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$goods
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_trade_goods_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_trade_goods_d` AFTER DELETE ON `trade$goods` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'v$product$cmpgoods';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default 'goId';
+    
+    set v_data_id=old.go_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmor
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_inv_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_inv_i` AFTER INSERT ON `trade$invoice_fmor` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmor';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmor
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_inv_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_inv_u` AFTER UPDATE ON `trade$invoice_fmor` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmor';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmor
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_inv_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_inv_d` AFTER DELETE ON `trade$invoice_fmor` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmor';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmor_dt
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_inv_dt_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_inv_dt_i` AFTER INSERT ON `trade$invoice_fmor_dt` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmor';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.invoice_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmor_dt
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_inv_dt_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_inv_dt_u` AFTER UPDATE ON `trade$invoice_fmor_dt` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmor';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.invoice_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmor_dt
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_inv_dt_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_inv_dt_d` AFTER DELETE ON `trade$invoice_fmor_dt` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmor';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.invoice_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmpu
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_inv_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_inv_i` AFTER INSERT ON `trade$invoice_fmpu` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmpu';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmpu
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_inv_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_inv_u` AFTER UPDATE ON `trade$invoice_fmpu` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmpu';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmpu
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_inv_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_inv_d` AFTER DELETE ON `trade$invoice_fmpu` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmpu';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmpu_dt
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_inv_dt_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_inv_dt_i` AFTER INSERT ON `trade$invoice_fmpu_dt` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmpu';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.invoice_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmpu_dt
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_inv_dt_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_inv_dt_u` AFTER UPDATE ON `trade$invoice_fmpu_dt` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmpu';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.invoice_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$invoice_fmpu_dt
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_inv_dt_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_inv_dt_d` AFTER DELETE ON `trade$invoice_fmpu_dt` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$invoice_fmpu';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.invoice_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$order
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_i` AFTER INSERT ON `trade$order` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$order';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$order
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_u` AFTER UPDATE ON `trade$order` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$order';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$order
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_d` AFTER DELETE ON `trade$order` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$order';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$order_detail
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_dt_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_dt_i` AFTER INSERT ON `trade$order_detail` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$order';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.order_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$order_detail
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_dt_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_dt_u` AFTER UPDATE ON `trade$order_detail` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$order';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.order_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$order_detail
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_order_dt_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_order_dt_d` AFTER DELETE ON `trade$order_detail` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$order';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.order_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$purchase
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_i` AFTER INSERT ON `trade$purchase` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$purchase';
+    declare v_method_type varchar(6) default 'insert';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$purchase
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_u` AFTER UPDATE ON `trade$purchase` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$purchase';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$purchase
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_d` AFTER DELETE ON `trade$purchase` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$purchase';
+    declare v_method_type varchar(6) default 'delete';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$purchase_detail
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_dt_i`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_dt_i` AFTER INSERT ON `trade$purchase_detail` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$purchase';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=new.purchase_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$purchase_detail
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_dt_u`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_dt_u` AFTER UPDATE ON `trade$purchase_detail` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$purchase';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.purchase_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;
+
+-- ----------------------------
+-- Triggers structure for table trade$purchase_detail
+-- ----------------------------
+DROP TRIGGER IF EXISTS `lucene_purchase_dt_d`;
+delimiter ;;
+CREATE DEFINER = `root`@`%` TRIGGER `lucene_purchase_dt_d` AFTER DELETE ON `trade$purchase_detail` FOR EACH ROW begin
+	declare v_table_name varchar(64) default 'trade$purchase';
+    declare v_method_type varchar(6) default 'update';
+    declare v_data_id int;
+    declare v_data text default null;
+    
+    set v_data_id=old.purchase_id;
+    call enqueue_lucene_message(v_table_name, v_method_type, v_data_id, v_data);
+end
+;;
+delimiter ;

+ 141 - 0
mall-search/pom.xml

@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>1.4.1.RELEASE</version>
+	</parent>
+	<groupId>com.uas.search</groupId>
+	<artifactId>mall-search</artifactId>
+	<packaging>war</packaging>
+	<version>0.0.1-SNAPSHOT</version>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<maven.compiler.plugin.version>2.3.2</maven.compiler.plugin.version>
+		<spring.cloud.version>1.2.1.RELEASE</spring.cloud.version>
+		<fastjson.version>1.2.15</fastjson.version>
+		<druid.version>1.0.24</druid.version>
+		<lucene.version>5.2.0</lucene.version>
+		<ikanalyzer.version>2012_v5</ikanalyzer.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-security</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-context-support</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-tx</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.aspectj</groupId>
+			<artifactId>aspectjweaver</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.data</groupId>
+			<artifactId>spring-data-jpa</artifactId>
+		</dependency>
+		<!-- fastjson -->
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>fastjson</artifactId>
+			<version>${fastjson.version}</version>
+		</dependency>
+		<!-- jdbc -->
+		<dependency>
+			<groupId>mysql</groupId>
+			<artifactId>mysql-connector-java</artifactId>
+		</dependency>
+		<!-- druid -->
+		<dependency>
+			<groupId>com.alibaba</groupId>
+			<artifactId>druid</artifactId>
+			<version>${druid.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-ehcache</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.hibernate</groupId>
+			<artifactId>hibernate-entitymanager</artifactId>
+		</dependency>
+
+		<!-- lucene -->
+		<dependency>
+			<groupId>org.apache.lucene</groupId>
+			<artifactId>lucene-core</artifactId>
+			<version>${lucene.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.lucene</groupId>
+			<artifactId>lucene-queryparser</artifactId>
+			<version>${lucene.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.lucene</groupId>
+			<artifactId>lucene-analyzers-common</artifactId>
+			<version>${lucene.version}</version>
+		</dependency>
+
+		<!-- IKAnalyzer -->
+		<dependency>
+			<groupId>org.wltea.analyzer</groupId>
+			<artifactId>IKAnalyzer</artifactId>
+			<version>${ikanalyzer.version}</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.7</source>
+					<target>1.7</target>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+				<configuration>
+					<mainClass>com.uas.search.Application</mainClass>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+
+	<distributionManagement>
+		<!-- 发布release仓库 -->
+		<repository>
+			<id>platform-release</id>
+			<name>platform-release</name>
+			<url>http://113.105.74.141:8081/artifactory/libs-release-local</url>
+		</repository>
+		<!-- 发布快照版本 -->
+		<snapshotRepository>
+			<id>platform-snapshots</id>
+			<name>platform-snapshots</name>
+			<url>http://113.105.74.141:8081/artifactory/libs-snapshot-local</url>
+		</snapshotRepository>
+	</distributionManagement>
+</project>

+ 43 - 0
mall-search/src/main/java/com/uas/search/Application.java

@@ -0,0 +1,43 @@
+package com.uas.search;
+
+import com.uas.search.jms.JmsListener;
+import com.uas.search.util.ContextUtils;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+import org.springframework.web.servlet.config.annotation.EnableWebMvc;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+
+@EnableTransactionManagement
+@EnableWebMvc
+@SpringBootApplication
+public class Application {
+
+	public static void main(String[] args) throws FileNotFoundException {
+		File logFile = new File("logs/log.log");
+		if (!logFile.getParentFile().exists()) {
+			logFile.getParentFile().mkdir();
+		}
+		System.setErr(new PrintStream(new FileOutputStream(logFile, true)));
+		SpringApplication application = new SpringApplication(Application.class);
+		application.addListeners(new ContextUtils());
+		application.run(args);
+
+		startTask();
+	}
+
+	/**
+	 * 开启定时任务
+	 */
+	public static void startTask() {
+		SystemProperties systemProperties = ContextUtils.getBean(SystemProperties.class);
+		if (systemProperties.isTaskAutoStart()) {
+			JmsListener jmsListener = ContextUtils.getBean(JmsListener.class);
+			jmsListener.start(null);
+		}
+	}
+}

+ 93 - 0
mall-search/src/main/java/com/uas/search/DictionaryHelper.java

@@ -0,0 +1,93 @@
+package com.uas.search;
+
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.StringUtils;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.wltea.analyzer.lucene.IKAnalyzer;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 词典录入辅助类
+ *
+ * @author sunyj
+ * @since 2017/11/20 14:31
+ */
+public class DictionaryHelper {
+
+    public static void main(String[] args) throws IOException {
+        List<String> words = readDict(new File("C:\\Users\\sunyj-pc\\Desktop\\kind.txt"));
+//         List<String> words=readDict(new File("C:\\Users\\sunyj-pc\\Desktop\\brand.txt"));
+//        List<String> words= Arrays.asList("签约仪式前,秦光荣、李纪恒、仇和等一同会见了参加签约的企业家。",
+//                "王国强、高峰、汪洋、张朝阳光着头、韩寒、小四",
+//                "张浩和胡健康复员回家了",
+//                "王总和小丽结婚了",
+//                "编剧邵钧林和稽道青说",
+//                "这里有关天培的有关事迹",
+//                "龚学平等领导,邓颖超生前");
+        print(ik(words));
+//        suggest();
+    }
+
+    private static void print(List<String> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        for (String str : list) {
+            System.out.println(str);
+        }
+    }
+
+    private static List<String> readDict(File file) throws IOException {
+        List<String> words = new ArrayList<>();
+        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
+            Logger logger = LoggerFactory.getLogger(DictionaryHelper.class);
+            String line;
+            while ((line = bufferedReader.readLine()) != null) {
+                if (StringUtils.isEmpty(line)) {
+                    logger.error("line 为空 <" + line + ">");
+                    continue;
+                }
+                // 每行格式为 "宇瞻科技	Apacer"
+                String[] strings = line.split("\t");
+                for (String str : strings) {
+                    if (StringUtils.isEmpty(str) || str.equalsIgnoreCase("NULL")) {
+//                        logger.error("str 为空 <" + str + "> <" + line + ">");
+                        continue;
+                    }
+                    if (words.contains(str)) {
+                        logger.error("str 重复 <" + str + "> <" + line + ">");
+                        continue;
+                    }
+                    words.add(str);
+                }
+            }
+        }
+        return words;
+    }
+
+    private static List<String> ik(List<String> words) throws IOException {
+        List<String> result = new ArrayList<>();
+        for (String sentence : words) {
+            Analyzer analyzer = new IKAnalyzer(true);
+            TokenStream tokenStream = analyzer.tokenStream("", sentence);
+            tokenStream.reset();
+            CharTermAttribute cta = tokenStream.addAttribute(CharTermAttribute.class);
+            while (tokenStream.incrementToken()) {
+                result.add(cta.toString());
+            }
+            tokenStream.close();
+            analyzer.close();
+        }
+        return result;
+    }
+}

+ 133 - 0
mall-search/src/main/java/com/uas/search/DruidDBConfiguration.java

@@ -0,0 +1,133 @@
+package com.uas.search;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.support.http.StatViewServlet;
+import com.alibaba.druid.support.http.WebStatFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.sql.DataSource;
+import java.sql.SQLException;
+
+@Configuration
+public class DruidDBConfiguration {
+
+	private static Logger logger = LoggerFactory.getLogger(DruidDBConfiguration.class);
+
+	@Value("${datasource.url}")
+	private String dbUrl;
+
+	@Value("${datasource.username}")
+	private String username;
+
+	@Value("${datasource.password}")
+	private String password;
+
+	@Value("${datasource.driverClassName}")
+	private String driverClassName;
+
+	@Value("${datasource.initialSize}")
+	private int initialSize;
+
+	@Value("${datasource.minIdle}")
+	private int minIdle;
+
+	@Value("${datasource.maxActive}")
+	private int maxActive;
+
+	@Value("${datasource.maxWait}")
+	private int maxWait;
+
+	@Value("${datasource.timeBetweenEvictionRunsMillis}")
+	private int timeBetweenEvictionRunsMillis;
+
+	@Value("${datasource.minEvictableIdleTimeMillis}")
+	private int minEvictableIdleTimeMillis;
+
+	@Value("${datasource.validationQuery}")
+	private String validationQuery;
+
+	@Value("${datasource.testWhileIdle}")
+	private boolean testWhileIdle;
+
+	@Value("${datasource.testOnBorrow}")
+	private boolean testOnBorrow;
+
+	@Value("${datasource.testOnReturn}")
+	private boolean testOnReturn;
+
+	@Value("${datasource.timeBetweenLogStatsMillis}")
+	private int timeBetweenLogStatsMillis;
+
+	@Value("${datasource.poolPreparedStatements}")
+	private boolean poolPreparedStatements;
+
+	@Value("${datasource.maxPoolPreparedStatementPerConnectionSize}")
+	private int maxPoolPreparedStatementPerConnectionSize;
+
+	@Value("${datasource.filters}")
+	private String filters;
+
+	@Value("${datasource.connectionProperties}")
+	private String connectionProperties;
+
+	public int getMaxActive() {
+		return maxActive;
+	}
+
+	@Bean("dataSource")
+	@Primary
+	public DataSource dataSource() {
+		DruidDataSource dataSource = new DruidDataSource();
+
+		dataSource.setUrl(dbUrl);
+		dataSource.setUsername(username);
+		dataSource.setPassword(password);
+		dataSource.setDriverClassName(driverClassName);
+
+		// configuration
+		dataSource.setInitialSize(initialSize);
+		dataSource.setMinIdle(minIdle);
+		dataSource.setMaxActive(maxActive);
+		dataSource.setMaxWait(maxWait);
+		dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+		dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+		dataSource.setValidationQuery(validationQuery);
+		dataSource.setTestWhileIdle(testWhileIdle);
+		dataSource.setTestOnBorrow(testOnBorrow);
+		dataSource.setTestOnReturn(testOnReturn);
+		dataSource.setTimeBetweenLogStatsMillis(timeBetweenLogStatsMillis);
+		dataSource.setPoolPreparedStatements(poolPreparedStatements);
+		dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
+		try {
+			dataSource.setFilters(filters);
+		} catch (SQLException e) {
+			logger.error("druid configuration initialization filter", e);
+		}
+		dataSource.setConnectionProperties(connectionProperties);
+
+		return dataSource;
+	}
+
+	@Bean
+	public ServletRegistrationBean druidServlet() {
+		return new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
+	}
+
+	@Bean
+	public FilterRegistrationBean filterRegistrationBean() {
+		FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
+		filterRegistrationBean.setFilter(new WebStatFilter());
+		filterRegistrationBean.addUrlPatterns("/*");
+		filterRegistrationBean.addInitParameter("exclusions",
+				"*.js,*.gif,*.jpg,*.png,*.bmp,*.css,*.ico,*.html,/druid/*");
+		return filterRegistrationBean;
+	}
+
+}

+ 37 - 0
mall-search/src/main/java/com/uas/search/LuceneProperties.java

@@ -0,0 +1,37 @@
+package com.uas.search;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LuceneProperties {
+
+	@Value("${lucene.indexes.dir}")
+	private String indexesDir;
+
+	@Value("${lucene.data.dir}")
+	private String dataDir;
+
+	/**
+	 * @return 索引文件存储路径
+	 */
+	public String getIndexesDir() {
+		return indexesDir;
+	}
+
+	public void setIndexesDir(String indexesDir) {
+		this.indexesDir = indexesDir;
+	}
+
+	/**
+	 * @return 存放批次数据的文件,用于创建索引
+	 */
+	public String getDataDir() {
+		return dataDir;
+	}
+
+	public void setDataDir(String dataDir) {
+		this.dataDir = dataDir;
+	}
+
+}

+ 29 - 0
mall-search/src/main/java/com/uas/search/SystemProperties.java

@@ -0,0 +1,29 @@
+package com.uas.search;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 系统配置信息
+ * 
+ * @author sunyj
+ * @since 2016年12月13日 上午10:20:54
+ */
+@Component
+public class SystemProperties {
+
+	/**
+	 * 定时任务是否自动开启
+	 */
+	@Value("${schedule.auto-start}")
+	private boolean taskAutoStart;
+
+	public boolean isTaskAutoStart() {
+		return taskAutoStart;
+	}
+
+	public void setTaskAutoStart(boolean taskAutoStart) {
+		this.taskAutoStart = taskAutoStart;
+	}
+
+}

+ 73 - 0
mall-search/src/main/java/com/uas/search/WebAppConfiguration.java

@@ -0,0 +1,73 @@
+package com.uas.search;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.*;
+import org.springframework.web.servlet.view.InternalResourceViewResolver;
+
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.fastjson.support.config.FastJsonConfig;
+import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
+
+@Configuration
+public class WebAppConfiguration extends WebMvcConfigurerAdapter {
+
+	@Override
+	public void addResourceHandlers(ResourceHandlerRegistry registry) {
+		// Spring boot默认资源路径在src/main/resources下,而非/src/main/webapp
+		// 使用/src/main/webapp下资源均需在此添加
+		registry.addResourceHandler("/static/**").addResourceLocations("/resources/");
+		registry.addResourceHandler("/WEB-INF/**").addResourceLocations("/WEB-INF/");
+		super.addResourceHandlers(registry);
+	}
+
+	@Override
+	public void addViewControllers(ViewControllerRegistry registry) {
+		registry.addViewController("/").setViewName("index");
+		registry.addViewController("/console").setViewName("console");
+		registry.addViewController("/fileUpload").setViewName("fileUpload");
+		registry.addViewController("/listenDetails").setViewName("listenDetails");
+		super.addViewControllers(registry);
+	}
+
+	@Override
+	public void configureViewResolvers(ViewResolverRegistry registry) {
+		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
+		viewResolver.setPrefix("/WEB-INF/views/");
+		viewResolver.setSuffix(".html");
+		viewResolver.setContentType("text/html;charset=UTF-8");
+		registry.viewResolver(viewResolver);
+		super.configureViewResolvers(registry);
+	}
+
+	@Override
+	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+		FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
+		FastJsonConfig fastJsonConfig = new FastJsonConfig();
+		fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
+		fastConverter.setFastJsonConfig(fastJsonConfig);
+		fastConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON_UTF8));
+		converters.add(fastConverter);
+
+		StringHttpMessageConverter messageConverter = new StringHttpMessageConverter();
+		messageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML));
+		messageConverter.setDefaultCharset(Charset.forName("UTF-8"));
+		converters.add(messageConverter);
+
+		super.configureMessageConverters(converters);
+	}
+
+	/**
+	* 允许跨域访问
+	*/
+	@Override
+	public void addCorsMappings(CorsRegistry registry) {
+		registry.addMapping("/**").allowedOrigins("*");
+	}
+}

+ 116 - 0
mall-search/src/main/java/com/uas/search/analyzer/DefaultConfig.java

@@ -0,0 +1,116 @@
+package com.uas.search.analyzer;
+
+import org.wltea.analyzer.cfg.Configuration;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * 替代 {@link org.wltea.analyzer.cfg.DefaultConfig} , 增加 loadExtDic 参数
+ *
+ * @author sunyj
+ * @since 2017/11/21 17:00
+ */
+public class DefaultConfig implements Configuration {
+    private static final String PATH_DIC_MAIN = "org/wltea/analyzer/dic/main2012.dic";
+    private static final String PATH_DIC_QUANTIFIER = "org/wltea/analyzer/dic/quantifier.dic";
+    private static final String FILE_NAME = "IKAnalyzer.cfg.xml";
+    private static final String EXT_DICT = "ext_dict";
+    private static final String EXT_STOP = "ext_stopwords";
+    private Properties props = new Properties();
+    private boolean useSmart;
+    private boolean loadExtDic;
+
+    public DefaultConfig(boolean useSmart, boolean loadExtDic) {
+        this.useSmart = useSmart;
+        this.loadExtDic = loadExtDic;
+        // 如果指定需要加载自定义词典,再进行加载
+        if (loadExtDic) {
+            InputStream input = this.getClass().getClassLoader().getResourceAsStream("IKAnalyzer.cfg.xml");
+            if (input != null) {
+                try {
+                    this.props.loadFromXML(input);
+                } catch (InvalidPropertiesFormatException var3) {
+                    var3.printStackTrace();
+                } catch (IOException var4) {
+                    var4.printStackTrace();
+                }
+            }
+        }
+    }
+
+    public boolean useSmart() {
+        return this.useSmart;
+    }
+
+    public void setUseSmart(boolean useSmart) {
+        this.useSmart = useSmart;
+    }
+
+    public boolean loadExtDic() {
+        return loadExtDic;
+    }
+
+    public String getMainDictionary() {
+        return "org/wltea/analyzer/dic/main2012.dic";
+    }
+
+    public String getQuantifierDicionary() {
+        return "org/wltea/analyzer/dic/quantifier.dic";
+    }
+
+    public List<String> getExtDictionarys() {
+        ArrayList extDictFiles = new ArrayList(2);
+        String extDictCfg = this.props.getProperty("ext_dict");
+        if (extDictCfg != null) {
+            String[] filePaths = extDictCfg.split(";");
+            if (filePaths != null) {
+                String[] var7 = filePaths;
+                int var6 = filePaths.length;
+
+                for (int var5 = 0; var5 < var6; ++var5) {
+                    String filePath = var7[var5];
+                    if (filePath != null && !"".equals(filePath.trim())) {
+                        extDictFiles.add(filePath.trim());
+                    }
+                }
+            }
+        }
+
+        return extDictFiles;
+    }
+
+    public List<String> getExtStopWordDictionarys() {
+        ArrayList extStopWordDictFiles = new ArrayList(2);
+        String extStopWordDictCfg = this.props.getProperty("ext_stopwords");
+        if (extStopWordDictCfg != null) {
+            String[] filePaths = extStopWordDictCfg.split(";");
+            if (filePaths != null) {
+                String[] var7 = filePaths;
+                int var6 = filePaths.length;
+
+                for (int var5 = 0; var5 < var6; ++var5) {
+                    String filePath = var7[var5];
+                    if (filePath != null && !"".equals(filePath.trim())) {
+                        extStopWordDictFiles.add(filePath.trim());
+                    }
+                }
+            }
+        }
+
+        return extStopWordDictFiles;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass() || !(obj instanceof DefaultConfig)) {
+            return false;
+        }
+        DefaultConfig defaultConfig = (DefaultConfig) obj;
+        return Objects.equals(useSmart, defaultConfig.useSmart()) && Objects.equals(loadExtDic, defaultConfig.loadExtDic());
+    }
+}

+ 56 - 0
mall-search/src/main/java/com/uas/search/analyzer/IKAnalyzer.java

@@ -0,0 +1,56 @@
+package com.uas.search.analyzer;
+
+import org.apache.lucene.analysis.Analyzer;
+
+/**
+ * 替代 {@link org.wltea.analyzer.lucene.IKAnalyzer} , 增加 loadExtDic 属性
+ *
+ * @author sunyj
+ * @since 2017/11/21 16:46
+ */
+public final class IKAnalyzer extends Analyzer {
+
+    /**
+     * 是否以最大粒度进行分词
+     */
+    private boolean useSmart;
+
+    /**
+     * 是否加载自定义词典
+     */
+    private boolean loadExtDic;
+
+    public IKAnalyzer() {
+        this(false);
+    }
+
+    public IKAnalyzer(boolean useSmart) {
+        this(useSmart, true);
+    }
+
+    public IKAnalyzer(boolean useSmart, boolean loadExtDic) {
+        this.useSmart = useSmart;
+        this.loadExtDic = loadExtDic;
+    }
+
+    public boolean useSmart() {
+        return this.useSmart;
+    }
+
+    public void setUseSmart(boolean useSmart) {
+        this.useSmart = useSmart;
+    }
+
+    public boolean loadExtDic() {
+        return loadExtDic;
+    }
+
+    public void setLoadExtDic(boolean loadExtDic) {
+        this.loadExtDic = loadExtDic;
+    }
+
+    protected TokenStreamComponents createComponents(String fieldName) {
+        IKTokenizer _IKTokenizer = new IKTokenizer(this.useSmart(), this.loadExtDic());
+        return new TokenStreamComponents(_IKTokenizer);
+    }
+}

+ 60 - 0
mall-search/src/main/java/com/uas/search/analyzer/IKTokenizer.java

@@ -0,0 +1,60 @@
+package com.uas.search.analyzer;
+
+
+import org.apache.lucene.analysis.Tokenizer;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.OffsetAttribute;
+import org.apache.lucene.analysis.tokenattributes.TypeAttribute;
+import org.wltea.analyzer.cfg.Configuration;
+import org.wltea.analyzer.core.IKSegmenter;
+import org.wltea.analyzer.core.Lexeme;
+
+import java.io.IOException;
+
+/**
+ * 替代 {@link org.wltea.analyzer.lucene.IKTokenizer} , 增加 loadExtDic 参数
+ *
+ * @author sunyj
+ * @since 2017/11/21 16:48
+ */
+public final class IKTokenizer extends Tokenizer {
+    private final CharTermAttribute termAtt = (CharTermAttribute) this.addAttribute(CharTermAttribute.class);
+    private final OffsetAttribute offsetAtt = (OffsetAttribute) this.addAttribute(OffsetAttribute.class);
+    private final TypeAttribute typeAtt = (TypeAttribute) this.addAttribute(TypeAttribute.class);
+    private IKSegmenter _IKImplement;
+    private int endPosition;
+
+    public IKTokenizer(boolean useSmart) {
+        this(useSmart, true);
+    }
+
+    public IKTokenizer(boolean useSmart, boolean loadExtDic) {
+        Configuration configuration = new DefaultConfig(useSmart, loadExtDic);
+        this._IKImplement = new IKSegmenter(this.input, configuration);
+    }
+
+    public boolean incrementToken() throws IOException {
+        this.clearAttributes();
+        Lexeme nextLexeme = this._IKImplement.next();
+        if (nextLexeme != null) {
+            this.termAtt.append(nextLexeme.getLexemeText());
+            this.termAtt.setLength(nextLexeme.getLength());
+            this.offsetAtt.setOffset(nextLexeme.getBeginPosition(), nextLexeme.getEndPosition());
+            this.endPosition = nextLexeme.getEndPosition();
+            this.typeAtt.setType(nextLexeme.getLexemeTypeString());
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void reset() throws IOException {
+        super.reset();
+        this._IKImplement.reset(this.input);
+    }
+
+    public final void end() {
+        int finalOffset = this.correctOffset(this.endPosition);
+        this.offsetAtt.setOffset(finalOffset, finalOffset);
+    }
+}

+ 24 - 0
mall-search/src/main/java/com/uas/search/annotation/NotEmpty.java

@@ -0,0 +1,24 @@
+package com.uas.search.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 自定义注解,监测方法的参数不为空
+ * 
+ * @author sunyj
+ * @since 2017年1月3日 上午11:31:01
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NotEmpty {
+
+	/**
+	 * 参数名称
+	 * 
+	 * @return
+	 */
+	String value() default "";
+}

+ 111 - 0
mall-search/src/main/java/com/uas/search/aop/ArgumentsCheckAspect.java

@@ -0,0 +1,111 @@
+package com.uas.search.aop;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.util.ObjectUtils;
+import com.uas.search.util.StringUtils;
+
+/**
+ * 利用AOP监测方法的参数(使用自定义注解NotEmpty)不为空
+ * 
+ * @author sunyj
+ * @since 2017年1月3日 上午11:33:27
+ */
+@Aspect
+@Component
+public class ArgumentsCheckAspect {
+
+	@Pointcut("execution(* com.uas..*.*(..))")
+	public void checkArguments() {
+
+	}
+
+	@Before("checkArguments()")
+	public void before(JoinPoint joinPoint) {
+		Method method = getMethod(joinPoint);
+		if (method == null) {
+			return;
+		}
+
+		// 参数值
+		Object[] args = joinPoint.getArgs();
+		if (args == null || args.length < 1) {
+			return;
+		}
+		Class<?>[] parameterTypes = method.getParameterTypes();
+		// 获取所有参数注解
+		Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+		// 遍历所有参数的注解
+		for (int i = 0; i < parameterAnnotations.length; i++) {
+			Object arg = args[i];
+			Annotation[] annotations = parameterAnnotations[i];
+			Class<?> parameterType = parameterTypes[i];
+			for (int j = 0; annotations != null && j < annotations.length; j++) {
+				Annotation annotation = annotations[j];
+				// 如果有注解为自定义的NotEmpty注解,则对参数值进行非空判断
+				if (annotation.annotationType() == NotEmpty.class) {
+					NotEmpty notEmptyAnnotation = (NotEmpty) annotation;
+					// 对字符串、Collection、Map进行判断
+					if (ObjectUtils.isEmpty(arg)) {
+						throw new IllegalArgumentException(joinPoint.getTarget().getClass().getSimpleName() + "."
+								+ method.getName() + ": " + "参数为空:" + notEmptyAnnotation.value() + "("
+								+ parameterType.getSimpleName() + ")");
+					}
+				}
+			}
+		}
+
+	}
+
+	/**
+	 * 获取切点所拦截的方法
+	 * 
+	 * @param joinPoint
+	 * @return
+	 */
+	private Method getMethod(JoinPoint joinPoint) {
+		Method method = null;
+		Object target = joinPoint.getTarget();
+		Class<?> clazz = target.getClass();
+		Signature signature = joinPoint.getSignature();
+		// 方法全名
+		String methodString = signature.toLongString();
+		// 取出参数类型
+		String substring = methodString.substring(methodString.indexOf("(") + 1, methodString.lastIndexOf(")"));
+		try {
+			if (!StringUtils.isEmpty(substring)) {
+				// Class.forName()不可用于java.lang.Long[]等类似的情况
+				if (!substring.contains("[]")) {
+					String[] strs = substring.split(",");
+					Class<?>[] clazzes = new Class[strs.length];
+					for (int i = 0; i < strs.length; i++) {
+						clazzes[i] = Class.forName(strs[i]);
+					}
+					// 根据签名获取方法
+					method = clazz.getMethod(signature.getName(), clazzes);
+				} else {
+					// java.lang.Long[]等类似的情况下,通过比较方法名称,获取方法(可能会获取到错误的方法)
+					for (Method m : clazz.getDeclaredMethods()) {
+						if (m.getName().equals(signature.getName())) {
+							method = m;
+						}
+					}
+				}
+			} else {
+				method = clazz.getMethod(signature.getName());
+			}
+		} catch (NoSuchMethodException | SecurityException | ClassNotFoundException e) {
+			throw new IllegalStateException("参数检查时出错", e);
+		}
+		return method;
+	}
+}

+ 70 - 0
mall-search/src/main/java/com/uas/search/aop/ExceptionHandlerAdvice.java

@@ -0,0 +1,70 @@
+/*CopyRright (c)2014: <www.usoftchina.com>
+ */
+package com.uas.search.aop;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.util.ExceptionUtils;
+
+/**
+ * 基于Application的异常处理,以AOP的形式注册到SpringMVC的处理链
+ * 
+ * @author sunyj
+ * @since 2017年7月13日 上午10:28:37
+ */
+@ControllerAdvice
+public class ExceptionHandlerAdvice {
+
+	private static Logger logger = LoggerFactory.getLogger(ExceptionHandlerAdvice.class);
+
+	/**
+	 * 处理已捕获异常,明确传达给客户端错误码、错误信息
+	 * 
+	 * @param e
+	 * @return
+	 */
+	@ExceptionHandler(Throwable.class)
+	public ResponseEntity<ModelMap> handleError(Throwable e) {
+		logger.error("", e);
+		ModelMap map = new ModelMap();
+		map.put("success", false);
+		map.put("message", e.getMessage());
+		map.put("detailedMessage", ExceptionUtils.getDetailedMessage(e));
+		HttpHeaders headers = new HttpHeaders();
+		headers.add("Content-Type", "application/json; charset=utf-8");
+		return new ResponseEntity<ModelMap>(map, headers, HttpStatus.INTERNAL_SERVER_ERROR);
+	}
+
+	/**
+	 * 处理错误
+	 * 
+	 * @param response
+	 * @param status
+	 * @param message
+	 * @param ip
+	 * @throws IOException
+	 */
+	public static void handleError(HttpServletResponse response, Integer status, String message, String ip)
+			throws IOException {
+		logger.error(message + ":" + ip);
+		response.setHeader("Content-type", "text/html;charset=UTF-8");
+		response.setStatus(status);
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.put("success", false);
+		jsonObject.put("message", message);
+		response.getWriter().println(jsonObject);
+	}
+
+}

+ 83 - 0
mall-search/src/main/java/com/uas/search/aop/WebLogAspect.java

@@ -0,0 +1,83 @@
+package com.uas.search.aop;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.stereotype.Component;
+
+import com.uas.search.util.IpHelper;
+import com.uas.search.util.ObjectUtils;
+import com.uas.search.util.StringUtils;
+
+/**
+ * 利用AOP处理Web请求日志
+ * 
+ * @author sunyj
+ * @since 2016年12月20日 下午5:46:56
+ */
+@Aspect
+@Component
+@EnableAspectJAutoProxy
+public class WebLogAspect {
+
+	private Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
+
+	@Pointcut("execution(* com.uas..controller.*.*(..))")
+	public void log() {
+
+	}
+
+	@Before("log()")
+	public void before(JoinPoint joinPoint) {
+		StringBuilder stringBuilder = new StringBuilder("request... ");
+
+		// 类名
+		Object target = joinPoint.getTarget();
+		Class<?> clazz = target.getClass();
+		stringBuilder.append(clazz.getSimpleName());
+
+		// 方法名
+		Signature signature = joinPoint.getSignature();
+		String methodName = signature.getName();
+		stringBuilder.append(".").append(methodName);
+
+		// 方法全名
+		String methodString = signature.toString();
+		// 取出参数类型
+		String substring = methodString.substring(methodString.indexOf("(") + 1, methodString.lastIndexOf(")"));
+		if (!StringUtils.isEmpty(substring)) {
+			stringBuilder.append(": ");
+			String[] strs = substring.split(",");
+			Object[] args = joinPoint.getArgs();
+			for (int i = 0; i < strs.length; i++) {
+				if (strs[i].equals("HttpServletRequest")) {
+					// 如果参数是HttpServletRequest,打印ip
+					stringBuilder.append("ip=").append(IpHelper.getIp((HttpServletRequest) args[i]));
+				} else if (strs[i].equals("HttpServletResponse")) {
+					// 如果参数是HttpServletResponse,不做处理
+					continue;
+				} else {
+					// 参数类型和参数值
+					stringBuilder.append(strs[i]).append("=").append(ObjectUtils.toString(args[i]));
+				}
+				if (i != strs.length - 1) {
+					stringBuilder.append(",");
+				}
+			}
+		}
+		logger.info(stringBuilder.toString());
+	}
+
+	@AfterReturning(value = "log()", returning = "result")
+	public void afterReturning(JoinPoint joinPoint, Object result) {
+		logger.info("result... " + result + "\n");
+	}
+}

+ 212 - 0
mall-search/src/main/java/com/uas/search/constant/SearchConstants.java

@@ -0,0 +1,212 @@
+package com.uas.search.constant;
+
+/**
+ * 索引相关的常量
+ * 
+ * @author suntg
+ * @since 2016年8月3日下午1:54:14
+ */
+public class SearchConstants {
+
+	/**
+	 * 默认搜索的最大的记录条数
+	 */
+	public static final int TOP_NUM = 1024 * 1024 * 1024;
+
+	/**
+	 * 消息队列名,该队列存放kind、brand、component等表的改动信息
+	 */
+	public static final String LUCENE_QUEUE_NAME = "lucene_queue";
+
+	/**
+	 * 消息队列对应的表
+	 */
+	public static final String LUCENE_QUEUE_TABLE_NAME = "lucene_queue_tb";
+
+	/**
+	 * 类目表名
+	 */
+	public static final String KIND_TABLE_NAME = "product$kind";
+
+	/**
+	 * 品牌表名
+	 */
+	public static final String BRAND_TABLE_NAME = "product$brand";
+
+	/**
+	 * 器件表名
+	 */
+	public static final String COMPONENT_TABLE_NAME = "product$component";
+
+	/**
+	 * 器件表名
+	 */
+	public static final String GOODS_TABLE_NAME = "v$product$cmpgoods";
+
+	/**
+	 * 商城销售订单表名
+	 */
+	public static final String ORDER_TABLE_NAME = "trade$order";
+
+	/**
+	 * 商城销售订单的发货单表名
+	 */
+	public static final String ORDER_INVOICE_TABLE_NAME = "trade$invoice_fmor";
+
+	/**
+	 * 商城采购订单表名
+	 */
+	public static final String PURCHASE_TABLE_NAME = "trade$purchase";
+
+	/**
+	 * 商城采购订单的发货单表名
+	 */
+	public static final String PURCHASE_INVOICE_TABLE_NAME = "trade$invoice_fmpu";
+
+	// 类目索引字段的key
+	public static final String KIND_ID_FIELD = "ki_id";
+	public static final String KIND_NAMECN_FIELD = "ki_name_cn";
+	public static final String KIND_LEVEL_FIELD = "ki_level";
+	public static final String KIND_ISLEAF_FIELD = "ki_isleaf";
+    public static final String KIND_VISIT_COUNT_FIELD = "ki_visit_count";
+    public static final String KIND_SEARCH_COUNT_FIELD = "ki_search_count";
+
+	// 品牌索引字段的key
+	public static final String BRAND_ID_FIELD = "br_id";
+	public static final String BRAND_NAMECN_FIELD = "br_name_cn";
+	public static final String BRAND_NAMEEN_FIELD = "br_name_en";
+    public static final String BRAND_NAMECN_UNTOKENIZED_FIELD = "br_name_cn_untokenized";
+    public static final String BRAND_NAMEEN_UNTOKENIZED_FIELD = "br_name_en_untokenized";
+	public static final String BRAND_UUID_FIELD = "br_uuid";
+    public static final String BRAND_VISIT_COUNT_FIELD = "br_visit_count";
+    public static final String BRAND_SEARCH_COUNT_FIELD = "br_search_count";
+    public static final String BRAND_WEIGHT_FIELD = "br_weight";
+
+	// 器件索引字段的key
+	public static final String COMPONENT_ID_FIELD = "cmp_id";
+	public static final String COMPONENT_UUID_FIELD = "cmp_uuid";
+	public static final String COMPONENT_CODE_FIELD = "cmp_code";
+    public static final String COMPONENT_VISIT_COUNT_FIELD = "cmp_visit_count";
+    public static final String COMPONENT_SEARCH_COUNT_FIELD = "cmp_search_count";
+	public static final String COMPONENT_RESERVE_FIELD = "cmp_reserve";
+	public static final String COMPONENT_SAMPLE_QTY_FIELD = "cmp_sample_qty";
+	public static final String COMPONENT_ORIGINAL_QTY_FIELD = "cmp_original_qty";
+	public static final String COMPONENT_INACTION_STOCK_QTY_FIELD = "cmp_inaction_stock_qty";
+	public static final String COMPONENT_KI_ID_FIELD = "ki_id";
+	public static final String COMPONENT_KI_NAME_FIELD = "ki_name_cn";
+	public static final String COMPONENT_KI_LEVEL_FIELD = "ki_level";
+    public static final String COMPONENT_KI_VISIT_COUNT_FIELD = "ki_visit_count";
+    public static final String COMPONENT_KI_SEARCH_COUNT_FIELD = "ki_search_count";
+	public static final String COMPONENT_BR_ID_FIELD = "br_id";
+	public static final String COMPONENT_BR_NAMECN_FIELD = "br_name_cn";
+	public static final String COMPONENT_BR_NAMEEN_FIELD = "br_name_en";
+	public static final String COMPONENT_BR_UUID_FIELD = "br_uuid";
+    public static final String COMPONENT_BR_VISIT_COUNT_FIELD = "br_visit_count";
+    public static final String COMPONENT_BR_SEARCH_COUNT_FIELD = "br_search_count";
+    public static final String COMPONENT_BR_WEIGHT_FIELD = "br_weight";
+	/**
+	 * 创建器件属性的索引时,在属性id的前面添加该前缀作为Field的键
+	 */
+	public static final String COMPONENT_PROPERTY_PREFIX = "pr_";
+	/**
+	 * 另外创建的经过分词的属性值,添加后缀作为Field的键
+	 */
+	public static final String COMPONENT_PROPERTY_TOKENIZED_SUFFIX = "_tokenized";
+
+	// 批次索引字段的key
+	public static final String GOODS_GO_ID_FIELD = "go_id";
+	public static final String GOODS_GO_RESERVE_FIELD = "go_reserve";
+	public static final String GOODS_GO_STATUS_FIELD = "go_status";
+	public static final String GOODS_GO_MINPRICERMB_FIELD = "go_minpricermb";
+	public static final String GOODS_GO_MINPRICEUSD_FIELD = "go_minpriceusd";
+    public static final String GOODS_GO_VISIT_COUNT_FIELD = "go_visit_count";
+	public static final String GOODS_CRNAME_FIELD = "cr_name";
+	public static final String GOODS_ST_UUID_FIELD = "st_uuid";
+	public static final String GOODS_ST_TYPE_FIELD = "st_type";
+	public static final String GOODS_CMP_ID_FIELD = "cmp_id";
+	public static final String GOODS_CMP_CODE_FIELD = "cmp_code";
+	public static final String GOODS_CMP_DESCRIPTION_FIELD = "cmp_description";
+    public static final String GOODS_CMP_VISIT_COUNT_FIELD = "cmp_visit_count";
+    public static final String GOODS_CMP_SEARCH_COUNT_FIELD = "cmp_search_count";
+	public static final String GOODS_KI_ID_FIELD = "ki_id";
+	public static final String GOODS_KI_NAME_CN_FIELD = "ki_name_cn";
+	public static final String GOODS_KI_LEVEL_FIELD = "ki_level";
+	public static final String GOODS_KI_ISLEAF_FIELD = "ki_isleaf";
+    public static final String GOODS_KI_VISIT_COUNT_FIELD = "ki_visit_count";
+    public static final String GOODS_KI_SEARCH_COUNT_FIELD = "ki_search_count";
+	public static final String GOODS_BR_ID_FIELD = "br_id";
+	public static final String GOODS_BR_NAME_CN_FIELD = "br_name_cn";
+	public static final String GOODS_BR_NAME_EN_FIELD = "br_name_en";
+	public static final String GOODS_BR_NAME_CN_UNTOKENIZED_FIELD = "br_name_cn_untokenized";
+	public static final String GOODS_BR_NAME_EN_UNTOKENIZED_FIELD = "br_name_en_untokenized";
+	public static final String GOODS_BR_UUID_FIELD = "br_uuid";
+    public static final String GOODS_BR_VISIT_COUNT_FIELD = "br_visit_count";
+    public static final String GOODS_BR_SEARCH_COUNT_FIELD = "br_search_count";
+    public static final String GOODS_BR_WEIGHT_FIELD = "br_weight";
+	public static final String GOODS_PR_ID_FIELD = "pr_id";
+	public static final String GOODS_PR_PCMPCODE_FIELD = "pr_pcmpcode";
+
+	// 商城销售订单索引字段的key
+	/**
+	 * 对应销售单id(数据库id字段)
+	 */
+	public static final String ORDER_ID_FIELD = "or_id";
+	/**
+	 * 对应销售单号(数据库or_id字段),因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	public static final String ORDER_CODE_FIELD = "or_code";
+	public static final String ORDER_BUYERUU_FIELD = "or_buyeruu";
+	public static final String ORDER_BUYERNAME_FIELD = "or_buyername";
+	public static final String ORDER_BUYERENUU_FIELD = "or_buyerenuu";
+	public static final String ORDER_BUYERENNAME_FIELD = "or_buyerenname";
+	public static final String ORDER_SELLERENUU_FIELD = "or_sellerenuu";
+	public static final String ORDER_SELLERENNAME_FIELD = "or_sellerenname";
+	public static final String ORDER_STATUS_FIELD = "or_status";
+	public static final String ORDER_DETAILS_FIELD = "or_details";
+
+	// 商城销售订单的发货单索引字段的key
+	/**
+	 * 对应商城销售订单的发货单id(数据库id字段)
+	 */
+	public static final String ORDER_INVOICE_ID_FIELD = "oi_id";
+	/**
+	 * 对应销售单发货单号(数据库in_id字段),因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	public static final String ORDER_INVOICE_CODE_FIELD = "oi_code";
+	public static final String ORDER_INVOICE_BUYERUU_FIELD = "oi_buyeruu";
+	public static final String ORDER_INVOICE_BUYERNAME_FIELD = "oi_buyername";
+	public static final String ORDER_INVOICE_BUYERENUU_FIELD = "oi_buyerenuu";
+	public static final String ORDER_INVOICE_BUYERENNAME_FIELD = "oi_buyerenname";
+	public static final String ORDER_INVOICE_STATUS_FIELD = "oi_status";
+	public static final String ORDER_INVOICE_DETAILS_FIELD = "oi_details";
+
+	// 商城采购订单索引字段的key
+	/**
+	 * 对应采购单id(数据库id字段)
+	 */
+	public static final String PURCHASE_ID_FIELD = "pu_id";
+	/**
+	 * 对应采购单号(数据库pu_id字段),因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	public static final String PURCHASE_CODE_FIELD = "pu_code";
+	public static final String PURCHASE_SELLERENUU_FIELD = "pu_sellerenuu";
+	public static final String PURCHASE_SELLERENNAME_FIELD = "pu_sellerenname";
+	public static final String PURCHASE_STATUS_FIELD = "pu_status";
+	public static final String PURCHASE_DETAILS_FIELD = "pu_details";
+
+	// 商城采购订单的发货单索引字段的key
+	/**
+	 * 对应商城采购订单的发货单id(数据库id字段)
+	 */
+	public static final String PURCHASE_INVOICE_ID_FIELD = "pi_id";
+	/**
+	 * 对应采购单发货单号(数据库in_id字段),因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	public static final String PURCHASE_INVOICE_CODE_FIELD = "pi_code";
+	public static final String PURCHASE_INVOICE_SELLERENUU_FIELD = "pi_sellerenuu";
+	public static final String PURCHASE_INVOICE_SELLERENNAME_FIELD = "pi_sellerenname";
+	public static final String PURCHASE_INVOICE_STATUS_FIELD = "pi_status";
+	public static final String PURCHASE_INVOICE_DETAILS_FIELD = "pi_details";
+
+	public static final String CREATETIME_FIELD = "createtime";
+}

+ 66 - 0
mall-search/src/main/java/com/uas/search/constant/model/BaseOrder.java

@@ -0,0 +1,66 @@
+package com.uas.search.constant.model;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 订单主表
+ * 
+ * @author suntg
+ * @since 2016年10月11日上午11:31:38
+ */
+public class BaseOrder implements Serializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	private Long id;
+
+	private String code;
+
+	private List<BaseOrderDetail> details;
+
+	public BaseOrder() {
+
+	}
+
+	public BaseOrder(Long id) {
+		this.id = id;
+	}
+
+	public BaseOrder(Long id, String code) {
+		this(id);
+		this.code = code;
+	}
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public List<BaseOrderDetail> getDetails() {
+		return details;
+	}
+
+	public void setDetails(List<BaseOrderDetail> details) {
+		this.details = details;
+	}
+
+	@Override
+	public String toString() {
+		return "BaseOrder [id=" + id + ", code=" + code + ", details=" + details + "]";
+	}
+}

+ 71 - 0
mall-search/src/main/java/com/uas/search/constant/model/BaseOrderDetail.java

@@ -0,0 +1,71 @@
+package com.uas.search.constant.model;
+
+import java.io.Serializable;
+
+/**
+ * 订单明细
+ * 
+ * @author suntg
+ * @since 2016年10月11日上午11:32:11
+ */
+public class BaseOrderDetail implements Serializable {
+
+	/**
+	 * 
+	 */
+	private static final long serialVersionUID = 1L;
+
+	private Long id;
+
+	private Short detno;
+
+	private String code;
+
+	public BaseOrderDetail() {
+
+	}
+
+	public BaseOrderDetail(Long id) {
+		this.id = id;
+	}
+
+	public BaseOrderDetail(Long id, Short detno) {
+		this(id);
+		this.detno = detno;
+	}
+
+	public BaseOrderDetail(Long id, Short detno, String code) {
+		this(id, detno);
+		this.code = code;
+	}
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	@Override
+	public String toString() {
+		return "BaseOrderDetail [id=" + id + ", detno=" + detno + ", code=" + code + "]";
+	}
+
+}

+ 31 - 0
mall-search/src/main/java/com/uas/search/constant/model/CollectField.java

@@ -0,0 +1,31 @@
+package com.uas.search.constant.model;
+
+/**
+ * 统计信息时需要的字段
+ * 
+ * @author sunyj
+ * @since 2017年7月12日 上午9:03:29
+ */
+public enum CollectField {
+
+	// 批次统计
+	/**
+	 * 类目
+	 */
+	GOODS_KIND,
+
+	/**
+	 * 品牌
+	 */
+	GOODS_BRAND,
+
+	/**
+	 * 货源
+	 */
+	GOODS_STORE_TYPE,
+
+	/**
+	 * 货币
+	 */
+	GOODS_CRNAME;
+}

+ 107 - 0
mall-search/src/main/java/com/uas/search/constant/model/PageInfo.java

@@ -0,0 +1,107 @@
+package com.uas.search.constant.model;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import com.alibaba.fastjson.JSONObject;
+
+public class PageInfo implements Pageable {
+	private int pageNumber;
+	private int pageSize;
+	private int offset;
+	private Sort sort;
+
+	public PageInfo() {
+	}
+
+	public PageInfo(String jsonPageParams) {
+		PageParams params = JSONObject.parseObject(jsonPageParams, PageParams.class);
+
+		getPageInfo(params);
+	}
+
+	public PageInfo(PageParams params) {
+		getPageInfo(params);
+	}
+
+	public PageInfo(PageParams params,Sort sort) {
+		getPageInfo(params);
+		this.sort = sort;
+	}
+
+	private void getPageInfo(PageParams params) {
+		this.pageSize = params.getSize();
+		if (this.pageSize == 0) {
+			this.pageSize = 5;
+		}
+		if (this.pageSize > 1000) {
+			throw new IllegalArgumentException("页面大小不可超过1000");
+		}
+		this.pageNumber = params.getPage();
+		if (this.pageNumber == 0) {
+			this.pageNumber = 1;
+		}
+		this.offset = (this.pageSize * (this.pageNumber - 1));
+	}
+
+	public PageInfo(int pageNumber, int pageSize) {
+		this.pageNumber = pageNumber;
+		this.pageSize = pageSize;
+		this.offset = (this.pageSize * (this.pageNumber - 1));
+	}
+
+	public PageInfo(int pageNumber, int pageSize, int offset) {
+		this.pageNumber = pageNumber;
+		this.pageSize = pageSize;
+		this.offset = offset;
+	}
+
+	public int getPageNumber() {
+		return this.pageNumber;
+	}
+
+	public int getPageSize() {
+		return this.pageSize;
+	}
+
+	public int getOffset() {
+		return this.offset;
+	}
+
+	public Sort getSort() {
+		return this.sort;
+	}
+
+	public void setPageNumber(int pageNumber) {
+		this.pageNumber = pageNumber;
+	}
+
+	public void setPageSize(int pageSize) {
+		this.pageSize = pageSize;
+	}
+
+	public void setOffset(int offset) {
+		this.offset = offset;
+	}
+
+	public void setSort(Sort sort) {
+		this.sort = sort;
+	}
+
+	public Pageable next() {
+		return null;
+	}
+
+	public Pageable previousOrFirst() {
+		return null;
+	}
+
+	public Pageable first() {
+		return null;
+	}
+
+	public boolean hasPrevious() {
+		return false;
+	}
+
+}

+ 236 - 0
mall-search/src/main/java/com/uas/search/constant/model/PageParams.java

@@ -0,0 +1,236 @@
+package com.uas.search.constant.model;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 分页参数
+ * 
+ * @author suntg
+ * @since 2016年8月3日下午9:10:47
+ */
+public class PageParams implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 页码
+	 */
+	private int page;
+
+	/**
+	 * 页大小
+	 */
+	private int size;
+
+	/**
+	 * 过滤
+	 */
+	private Map<FilterField, Object> filters;
+
+	/**
+	 * 排序
+	 */
+	private List<Sort> sort;
+
+	public PageParams() {
+	}
+
+	/**
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 */
+	public PageParams(int page, int size) {
+		this.page = page;
+		this.size = size;
+	}
+
+	/**
+	 * @return 页码
+	 */
+	public int getPage() {
+		return page;
+	}
+
+	public void setPage(int page) {
+		this.page = page;
+	}
+
+	/**
+	 * @return 页大小
+	 */
+	public int getSize() {
+		return size;
+	}
+
+	public void setSize(int size) {
+		this.size = size;
+	}
+
+	/**
+	 * @return 过滤
+	 */
+	public Map<FilterField, Object> getFilters() {
+		return filters;
+	}
+
+	public void setFilters(Map<FilterField, Object> filters) {
+		this.filters = filters;
+	}
+
+	public void filter(FilterField key, Object value) {
+		if (this.filters == null) {
+			this.filters = new HashMap<FilterField, Object>();
+		}
+		this.filters.put(key, value);
+	}
+
+	public void removeFilter(FilterField key) {
+		if (this.filters != null) {
+			this.filters.remove(key);
+		}
+	}
+
+	/**
+	 * @return 排序
+	 */
+	public List<Sort> getSort() {
+		return sort;
+	}
+
+	public void setSort(List<Sort> sort) {
+		this.sort = sort;
+	}
+
+	@Override
+	public String toString() {
+		return "PageParams [page=" + page + ", size=" + size + ", filters=" + filters + ", sort=" + sort + "]";
+	}
+
+	/**
+	 * 过滤的字段
+	 * 
+	 * @author sunyj
+	 * @since 2017年7月8日 下午5:00:55
+	 */
+	public enum FilterField {
+
+		// 器件搜索
+		/**
+		 * 类目id
+		 */
+		COMPONENT_KINDID,
+
+		/**
+		 * 品牌id
+		 */
+		COMPONENT_BRANDID,
+
+		/**
+		 * 属性信息
+		 */
+		COMPONENT_PROPERTIES,
+
+		/**
+		 * 有库存
+		 */
+		COMPONENT_HAS_RESERVE,
+
+		/**
+		 * 有样品
+		 */
+		COMPONENT_HAS_SAMPLE,
+
+		/**
+		 * 有现货
+		 */
+		COMPONENT_HAS_ORIGINAL,
+
+		/**
+		 * 有呆滞库存
+		 */
+		COMPONENT_HAS_INACTION_STOCK,
+
+		// 批次搜索
+		/**
+		 * 类目id
+		 */
+		GOODS_KINDID,
+
+		/**
+		 * 品牌id
+		 */
+		GOODS_BRANDID,
+
+		/**
+		 * 货源
+		 */
+		GOODS_STORE_TYPE,
+
+		/**
+		 * 货币
+		 */
+		GOODS_CRNAME,
+
+		/**
+		 * 状态
+		 */
+		GOODS_STATUS,
+
+		/**
+		 * 价格区间过滤:人民币最低价格
+		 */
+		GOODS_MINPRICERMB,
+
+		/**
+		 * 价格区间过滤:人民币最高价格
+		 */
+		GOODS_MAXPRICERMB,
+
+		/**
+		 * 价格区间过滤:美元最低价格
+		 */
+		GOODS_MINPRICEUSD,
+
+		/**
+		 * 价格区间过滤:美元最高价格
+		 */
+		GOODS_MAXPRICEUSD,
+
+		// 以下字段用于单据搜索
+
+		/**
+		 * 订单状态
+		 */
+		ORDER_STATUS,
+
+		/**
+		 * 买方个人uu号
+		 */
+		ORDER_BUYER_UU,
+
+		/**
+		 * 买方企业UU号
+		 */
+		ORDER_BUYER_EN_UU,
+
+		/**
+		 * 卖方企业UU号
+		 */
+		ORDER_SELLER_EN_UU,
+
+		/**
+		 * 单据搜索时,若限定时间范围,可以键值对的形式传递参数,该键代表最小(远)时间
+		 */
+		ORDER_MIN_TIME,
+
+		/**
+		 * 单据搜索时,若限定时间范围,可以键值对的形式传递参数,该键代表最大(近)时间
+		 */
+		ORDER_MAX_TIME;
+	}
+}

+ 100 - 0
mall-search/src/main/java/com/uas/search/constant/model/SPage.java

@@ -0,0 +1,100 @@
+package com.uas.search.constant.model;
+
+import java.io.Serializable;
+import java.util.List;
+
+public class SPage<T> implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	private int totalPage;
+
+	private long totalElement;
+
+	private int page;
+
+	private int size;
+
+	private boolean first;
+
+	private boolean last;
+
+	private List<T> content;
+
+	public SPage() {
+		super();
+	}
+
+	public SPage(int totalPage, long totalElement, int page, int size, boolean first, boolean last) {
+		super();
+		this.totalPage = totalPage;
+		this.totalElement = totalElement;
+		this.page = page;
+		this.size = size;
+		this.first = first;
+		this.last = last;
+	}
+
+	public int getTotalPage() {
+		return totalPage;
+	}
+
+	public void setTotalPage(int totalPage) {
+		this.totalPage = totalPage;
+	}
+
+	public long getTotalElement() {
+		return totalElement;
+	}
+
+	public void setTotalElement(long totalElement) {
+		this.totalElement = totalElement;
+	}
+
+	public int getPage() {
+		return page;
+	}
+
+	public void setPage(int page) {
+		this.page = page;
+	}
+
+	public int getSize() {
+		return size;
+	}
+
+	public void setSize(int size) {
+		this.size = size;
+	}
+
+	public boolean isFirst() {
+		return first;
+	}
+
+	public void setFirst(boolean first) {
+		this.first = first;
+	}
+
+	public boolean isLast() {
+		return last;
+	}
+
+	public void setLast(boolean last) {
+		this.last = last;
+	}
+
+	public List<T> getContent() {
+		return content;
+	}
+
+	public void setContent(List<T> content) {
+		this.content = content;
+	}
+
+	@Override
+	public String toString() {
+		return "SPage [totalPage=" + totalPage + ", totalElement=" + totalElement + ", page=" + page + ", size=" + size
+				+ ", first=" + first + ", last=" + last + ", content=" + content + "]";
+	}
+
+}

+ 105 - 0
mall-search/src/main/java/com/uas/search/constant/model/Sort.java

@@ -0,0 +1,105 @@
+package com.uas.search.constant.model;
+
+import java.io.Serializable;
+
+/**
+ * 排序
+ * 
+ * @author sunyj
+ * @since 2016年11月23日 下午1:47:44
+ */
+public class Sort implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 排序的字段
+	 */
+	private Field field;
+
+	/**
+	 * 排序默认为升序,若为true,则进行反转
+	 */
+	private boolean reverse = false;
+
+	/**
+	 * @param field
+	 *            排序的字段
+	 * @param reverse
+	 *            如果为true,则反转默认的升序
+	 */
+	public Sort(Field field, boolean reverse) {
+		this.field = field;
+		this.reverse = reverse;
+	}
+
+	/**
+	 * @param field
+	 *            排序的字段
+	 */
+	public Sort(Field field) {
+		this(field, false);
+	}
+
+	public Sort() {
+
+	}
+
+	/**
+	 * @return 排序的字段
+	 */
+	public Field getField() {
+		return field;
+	}
+
+	public void setField(Field field) {
+		this.field = field;
+	}
+
+	/**
+	 * 排序是否应该被反转
+	 * 
+	 * @return 如果为true,则反转默认的升序
+	 */
+	public boolean isReverse() {
+		return reverse;
+	}
+
+	public void setReverse(boolean reverse) {
+		this.reverse = reverse;
+	}
+
+	@Override
+	public String toString() {
+		return "Sort [field=" + field + ", reverse=" + reverse + "]";
+	}
+
+	/**
+	 * 排序的字段
+	 * 
+	 * @author sunyj
+	 * @since 2017年7月8日 下午5:12:07
+	 */
+	public enum Field {
+		/**
+		 * 批次库存
+		 */
+		GO_RESERVE,
+
+		/**
+		 * 批次价格(人民币)
+		 */
+		GO_MINPRICERMB,
+
+		/**
+		 * 批次价格(美元)
+		 */
+		GO_MINPRICEUSD,
+
+		/**
+		 * 批次搜索(由器件、品牌、类目等因素决定,不受reverse和missingValue影响)
+		 */
+		GO_SEARCH;
+	}
+
+}

+ 137 - 0
mall-search/src/main/java/com/uas/search/controller/IndexController.java

@@ -0,0 +1,137 @@
+package com.uas.search.controller;
+
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.jms.JmsListener;
+import com.uas.search.jms.LuceneMessage;
+import com.uas.search.jms.LuceneMessageDao;
+import com.uas.search.jms.LuceneMessageService;
+import com.uas.search.service.IndexService;
+import com.uas.search.support.DownloadHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import com.uas.search.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 索引创建相关请求
+ *
+ * @author sunyj
+ * @since 2016年8月5日 上午11:42:54
+ */
+@Controller
+@RequestMapping("/index")
+public class IndexController {
+
+    @Autowired
+    private LuceneMessageDao luceneMessageDao;
+
+    @Autowired
+    private IndexService indexService;
+
+    @Autowired
+    private LuceneMessageService luceneMessageService;
+
+    @Autowired
+    private JmsListener jmsListener;
+
+    @RequestMapping("/create")
+    @ResponseBody
+    public String initIndexes(String tableNames, HttpServletRequest request) {
+        return String.format("Indexes created success in %.2fs",
+                indexService.createIndexes(toList(tableNames), true) / 1000.0);
+    }
+
+    /**
+     * 根据逗号进行分隔,将字符串转为 List
+     *
+     * @param input 输入的字符串
+     * @return 转为的 List
+     */
+    private List<String> toList(String input) {
+        List<String> list = new ArrayList<>();
+        if (!StringUtils.isEmpty(input)) {
+            String[] strs = input.split(",");
+            for (String str : strs) {
+                list.add(str.toLowerCase());
+            }
+        }
+        return list;
+    }
+
+    @RequestMapping("/multiDownloadComponent")
+    @ResponseBody
+    public String multiDownloadComponent(Integer threads, Integer startFileIndex, Integer endFileIndex, String validateResult, HttpServletRequest request) {
+        long start = System.currentTimeMillis();
+        String message= "Downloaded: "+ indexService.multiDownloadComponent(threads, startFileIndex, endFileIndex,
+                StringUtils.isEmpty(validateResult) ? DownloadHelper.ValidateResult.CURRENT : DownloadHelper.ValidateResult.valueOf(validateResult.toUpperCase()));
+        message += String.format(", Time: %.2fs", (System.currentTimeMillis()-start)/1000.0);
+        return message;
+    }
+
+    @RequestMapping("/multiDownloadGoods")
+    @ResponseBody
+    public String multiDownloadGoods(Integer threads, Integer startFileIndex, Integer endFileIndex, String validateResult, HttpServletRequest request) {
+        long start = System.currentTimeMillis();
+        String message= "Downloaded: "+ indexService.multiDownloadGoods(threads, startFileIndex, endFileIndex,
+                StringUtils.isEmpty(validateResult) ? DownloadHelper.ValidateResult.CURRENT : DownloadHelper.ValidateResult.valueOf(validateResult.toUpperCase()));
+        message += String.format(", Time: %.2fs", (System.currentTimeMillis()-start)/1000.0);
+        return message;
+    }
+
+    @RequestMapping("/listen/start")
+    @ResponseBody
+    public String startListen(Long interval, HttpServletRequest request) {
+        jmsListener.start(interval);
+        return "开启成功";
+    }
+
+    @RequestMapping("/listen/stop")
+    @ResponseBody
+    public String stopListen(HttpServletRequest request) {
+        jmsListener.stop();
+        return "关闭成功";
+    }
+
+    @RequestMapping("/listen/restart")
+    @ResponseBody
+    public String restartListen(Long interval, HttpServletRequest request) {
+        if (jmsListener.isRunning()) {
+            jmsListener.stop();
+        }
+        jmsListener.start(interval);
+        return "重启成功";
+    }
+
+    @RequestMapping("/listen/details")
+    @ResponseBody
+    public SPage<LuceneMessage> listenDetails(Integer page, Integer size, HttpServletRequest request) {
+        return luceneMessageService.findAll(page, size);
+    }
+
+    @RequestMapping("/maintain")
+    @ResponseBody
+    public String maintainIndexes(@NotEmpty("tableName") String tableName, @NotEmpty("dataId") Long dataId, @NotEmpty("methodType") String methodType, String data,
+                                  HttpServletRequest request) {
+        return "已维护:" + indexService.maintainIndexes(tableName.toLowerCase(), dataId, methodType, data);
+    }
+
+    @RequestMapping("/dequeue")
+    @ResponseBody
+    public boolean dequeueLuceneQueueMessage(Long id, HttpServletRequest request) {
+        luceneMessageDao.dequeueLuceneMessage(id);
+        return true;
+    }
+
+    @RequestMapping("/updateByNewWords")
+    @ResponseBody
+    public Map<String, List<Object>> updateIndexByNewWords(String newWords, HttpServletRequest request) {
+        return indexService.updateIndexByNewWords(toList(newWords));
+    }
+}

+ 139 - 0
mall-search/src/main/java/com/uas/search/controller/OrderSearchController.java

@@ -0,0 +1,139 @@
+package com.uas.search.controller;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import com.uas.search.util.StringUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.uas.search.constant.model.BaseOrder;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.PageParams.FilterField;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.OrderInvoice;
+import com.uas.search.model.Order;
+import com.uas.search.model.PurchaseInvoice;
+import com.uas.search.model.Purchase;
+import com.uas.search.service.OrderSearchService;
+import com.uas.search.service.OrderSearchService.Table_name;
+
+/**
+ * 订单搜索请求
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午11:59:45
+ */
+@Controller
+@RequestMapping("/search")
+public class OrderSearchController {
+
+	@Autowired
+	private OrderSearchService orderSearchService;
+
+	@RequestMapping("orderIds")
+	@ResponseBody
+	public SPage<BaseOrder> getOrderIdsWithDetail(String keyword, String tableName, Integer page, Integer size,
+			String status, Long buyeruu, Long buyerenuu, Long sellerenuu, String mintime, String maxtime,
+			HttpServletRequest request) {
+		Table_name tbName = null;
+		if (!StringUtils.isEmpty(tableName)) {
+			tbName = Table_name.valueOf(tableName);
+		}
+
+		PageParams params = new PageParams();
+		if (page != null) {
+			params.setPage(page);
+		}
+		if (size != null) {
+			params.setSize(size);
+		}
+		Map<FilterField, Object> filters = new HashMap<>();
+		if (!StringUtils.isEmpty(status)) {
+			String[] strs = status.split(",");
+			List<Integer> statusList = new ArrayList<>();
+			for (String str : strs) {
+				statusList.add(Integer.valueOf(str));
+			}
+			filters.put(FilterField.ORDER_STATUS, statusList);
+		}
+		if (buyeruu != null) {
+			filters.put(FilterField.ORDER_BUYER_UU, buyeruu);
+		}
+		if (buyerenuu != null) {
+			filters.put(FilterField.ORDER_BUYER_EN_UU, buyerenuu);
+		}
+		if (sellerenuu != null) {
+			filters.put(FilterField.ORDER_SELLER_EN_UU, sellerenuu);
+		}
+		if (mintime != null) {
+			filters.put(FilterField.ORDER_MIN_TIME, parseTime(mintime));
+		}
+		if (maxtime != null) {
+			filters.put(FilterField.ORDER_MAX_TIME, parseTime(maxtime));
+		}
+		params.setFilters(filters);
+		return orderSearchService.getOrderIdsWithDetail(keyword, tbName, params);
+	}
+
+	/**
+	 * 将时间字符串转为long eg. 2016-10-10 1:12:20
+	 * 
+	 * @param str
+	 *            时间,eg. 2016-10-10 1:12:20
+	 * @return long值
+	 */
+	private Long parseTime(String str) {
+		try {
+			String[] strs = str.split("-");
+			int year = Integer.parseInt(strs[0]);
+			int month = Integer.parseInt(strs[1]) - 1;
+			String[] strs2 = strs[2].split(" ");
+			int date = Integer.parseInt(strs2[0]);
+			String[] strs3 = strs2[1].split(":");
+			int hour = Integer.parseInt(strs3[0]);
+			int minute = Integer.parseInt(strs3[1]);
+			int second = Integer.parseInt(strs3[2]);
+			Calendar calendar = Calendar.getInstance();
+			// 月份减1
+			calendar.set(year, month, date, hour, minute, second);
+			return calendar.getTimeInMillis();
+		} catch (Exception e) {
+			throw new SearchException("时间格式错误,示例:'2016-10-10 1:12:20'," + e);
+		}
+	}
+
+	@RequestMapping("/order/{id}")
+	@ResponseBody
+	public Order getOrder(@PathVariable Long id, HttpServletRequest request) {
+		return orderSearchService.getOrder(id);
+	}
+
+	@RequestMapping("/orderInvoice/{id}")
+	@ResponseBody
+	public OrderInvoice getOrderInvoice(@PathVariable Long id, HttpServletRequest request) {
+		return orderSearchService.getOrderInvoice(id);
+	}
+
+	@RequestMapping("/purchase/{id}")
+	@ResponseBody
+	public Purchase getPurchase(@PathVariable Long id, HttpServletRequest request) {
+		return orderSearchService.getPurchase(id);
+	}
+
+	@RequestMapping("/purchaseInvoice/{id}")
+	@ResponseBody
+	public PurchaseInvoice getPurchaseInvoice(@PathVariable Long id, HttpServletRequest request) {
+		return orderSearchService.getPurchaseInvoice(id);
+	}
+
+}

+ 54 - 0
mall-search/src/main/java/com/uas/search/controller/ScheduleController.java

@@ -0,0 +1,54 @@
+package com.uas.search.controller;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.uas.search.schedule.TaskInformation;
+import com.uas.search.schedule.TaskService;
+
+@Controller
+@RequestMapping("/schedule")
+public class ScheduleController {
+
+	@Autowired
+	private TaskService taskService;
+
+	@RequestMapping("/tasks")
+	@ResponseBody
+	public List<TaskInformation> allTaskInformations(HttpServletRequest request) {
+		return taskService.allTaskInformations();
+	}
+
+	@RequestMapping("/start")
+	@ResponseBody
+	public String start(HttpServletRequest request) {
+		return taskService.start();
+	}
+
+	@RequestMapping("/stop")
+	@ResponseBody
+	public String stop(HttpServletRequest request) {
+		return taskService.stop();
+	}
+
+	@RequestMapping("/restart")
+	@ResponseBody
+	public String restart(HttpServletRequest request) {
+		if (!taskService.isStopped()) {
+			taskService.stop();
+		}
+		return taskService.start();
+	}
+
+	@RequestMapping("/isStopped")
+	@ResponseBody
+	public boolean isStopped(HttpServletRequest request) {
+		return taskService.isStopped();
+	}
+}

+ 325 - 0
mall-search/src/main/java/com/uas/search/controller/SearchController.java

@@ -0,0 +1,325 @@
+package com.uas.search.controller;
+
+import com.uas.search.util.StringUtils;
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.constant.model.CollectField;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.PageParams.FilterField;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.dao.ComponentDao;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.Brand;
+import com.uas.search.model.Component;
+import com.uas.search.model.Goods;
+import com.uas.search.model.Kind;
+import com.uas.search.service.SearchService;
+import com.uas.search.service.impl.IndexServiceImpl;
+import com.uas.search.util.FileUtils;
+import com.uas.search.util.SearchUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import com.uas.search.util.CollectionUtils;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * 搜索请求
+ *
+ * @author suntg
+ * @since 2016年8月1日上午9:18:05
+ */
+@Controller
+@RequestMapping("/search")
+public class SearchController {
+
+	@Autowired
+	private SearchService searchService;
+
+	private Logger logger = LoggerFactory.getLogger(getClass());
+
+	@RequestMapping("/kindIds/{keyword}")
+	@ResponseBody
+	public SPage<Long> seachKindIds(@PathVariable String keyword, Integer page, Integer size,
+									HttpServletRequest request) {
+		return searchService.getKindIds(keyword, page, size);
+	}
+
+	@RequestMapping("/kinds/{keyword}")
+	@ResponseBody
+	public SPage<Map<String, Object>> seachKinds(@PathVariable String keyword, Integer page, Integer size,
+												 HttpServletRequest request) {
+		return searchService.getKinds(keyword, page, size);
+	}
+
+	@RequestMapping("/brandIds/{keyword}")
+	@ResponseBody
+	public SPage<Long> searchBrandIds(@PathVariable String keyword, Integer page, Integer size,
+									  HttpServletRequest request) {
+		return searchService.getBrandIds(keyword, page, size);
+	}
+
+	@RequestMapping("/brands/{keyword}")
+	@ResponseBody
+	public SPage<Map<String, Object>> searchBrand(@PathVariable String keyword, Integer page, Integer size,
+												  HttpServletRequest request) {
+		return searchService.getBrands(keyword, page, size);
+	}
+
+    @RequestMapping("/componentIds")
+    @ResponseBody
+    public Map<String, Object> searchComponentIds(String keyword, String params, HttpServletRequest request) {
+        PageParams pageParams = params==null?null:JSONObject.parseObject(params, PageParams.class);
+        return searchService.getComponentIds(keyword, pageParams);
+    }
+
+	@Autowired
+	private ComponentDao componentDao;
+
+    @RequestMapping("/components")
+    @ResponseBody
+    public List<Component> searchComponents(String keyword, String params, HttpServletRequest request) {
+        @SuppressWarnings("unchecked")
+        List<Long> ids = (List<Long>) searchComponentIds(keyword, params, request).get("componentIds");
+        return findComponents(ids);
+    }
+
+    private List<Component> findComponents(List<Long> ids){
+        if (CollectionUtils.isEmpty(ids)) {
+            return new ArrayList<Component>();
+        }
+
+        Long[] idsLong = new Long[ids.size()];
+        int i = 0;
+        for (Long id : ids) {
+            idsLong[i++] = id;
+        }
+        return componentDao.findAll(ids);
+    }
+
+	@RequestMapping("/kindIdsByComponent")
+	@ResponseBody
+	public Set<Long> searchKindIdsBySearchComponent(String keyword, String brandId, HttpServletRequest request) {
+		return searchService.getKindIdsBySearchComponent(keyword, brandId);
+	}
+
+	@RequestMapping("/kindsByComponent")
+	@ResponseBody
+	public Set<Map<String, Object>> searchKindsBySearchComponent(String keyword, String brandId,
+																  HttpServletRequest request) {
+		return searchService.getKindsBySearchComponent(keyword, brandId);
+	}
+
+	@RequestMapping("/brandIdsByComponent")
+	@ResponseBody
+	public Set<Long> searchBrandIdsBySearchComponent(String keyword, String kindId, HttpServletRequest request) {
+		return searchService.getBrandIdsBySearchComponent(keyword, kindId);
+	}
+
+	@RequestMapping("/brandsByComponent")
+	@ResponseBody
+	public Set<Map<String, Object>> searchBrandsBySearchComponent(String keyword, String kindId,
+																   HttpServletRequest request) {
+		return searchService.getBrandsBySearchComponent(keyword, kindId);
+	}
+
+	@RequestMapping("/similarKeywords/{keyword}")
+	@ResponseBody
+	public List<String> getSimilarKeywords(@PathVariable String keyword, Integer size, HttpServletRequest request) {
+		return searchService.getSimilarKeywords(keyword, size);
+	}
+
+	@RequestMapping("/similarComponents/{componentCode}")
+	@ResponseBody
+	public List<Map<String, Object>> getSimilarComponents(@PathVariable String componentCode, Integer size,
+														  HttpServletRequest request) {
+		return searchService.getSimilarComponents(componentCode, size);
+	}
+
+	@RequestMapping("/similarBrands/{brandName}")
+	@ResponseBody
+	public List<Map<String, Object>> getSimilarBrands(@PathVariable String brandName, Integer size, HttpServletRequest request) {
+		return searchService.getSimilarBrands(brandName, size);
+	}
+
+	@RequestMapping("/similarKinds/{kindName}")
+	@ResponseBody
+	public List<Map<String, Object>> getSimilarKinds(@PathVariable String kindName, Integer size, HttpServletRequest request) {
+		return searchService.getSimilarKinds(kindName, size);
+	}
+
+	@RequestMapping("/similarLeafKinds/{kindName}")
+	@ResponseBody
+	public List<Map<String, Object>> getSimilarLeafKinds(@PathVariable String kindName, Integer size, HttpServletRequest request) {
+		return searchService.getSimilarLeafKinds(kindName, size);
+	}
+
+	@RequestMapping("/similarKindsByLevel")
+	@ResponseBody
+	public List<Map<String, Object>> getSimilarKindsByLevel(String kindName, Short level, Integer size, HttpServletRequest request) {
+		return searchService.getSimilarKindsByLevel(kindName, level, size);
+	}
+
+	@RequestMapping("/similarPropertyValues")
+	@ResponseBody
+	public List<Map<String, String>> getSimilarPropertyValues(Long kindId, Long propertyId, String keyword, Long topNum,
+															  HttpServletRequest request) {
+		return searchService.getSimilarPropertyValues(kindId, propertyId, keyword, topNum);
+	}
+
+    public Map<FilterField, Object> parseFilters(JSONObject json){
+        Map<FilterField, Object> filters = new HashMap<>();
+        if(json!=null){
+            Set<Entry<String, Object>> entrySet = json.entrySet();
+            for (Entry<String, Object> entry : entrySet) {
+                FilterField field = FilterField.valueOf(entry.getKey());
+                String value = entry.getValue().toString();
+                switch (field) {
+                    case GOODS_KINDID:
+                    case GOODS_BRANDID:
+                    case GOODS_STORE_TYPE:
+                    case GOODS_CRNAME:
+                        String[] strs = value.split(",");
+                        List<Object> values = new ArrayList<>();
+                        for (String str : strs) {
+                            values.add(str);
+                        }
+                        filters.put(field, values);
+                        break;
+                    default:
+                        filters.put(field, value);
+                }
+            }
+        }
+        return filters;
+    }
+
+	@RequestMapping("/goodsIds")
+	@ResponseBody
+	public Map<String, Object> getGoodsIds(String keyword, String params, HttpServletRequest request) {
+		PageParams pageParams = params==null?null:JSONObject.parseObject(params, PageParams.class);
+		return searchService.getGoodsIds(keyword, pageParams);
+	}
+
+	@RequestMapping("/collectBySearchGoods")
+	@ResponseBody
+	public List<Map<String, Object>> collectBySearchGoods(String keyword, @RequestParam String collectedField, String filters, HttpServletRequest request) {
+        Map<FilterField, Object> filtersMap = new HashMap<>();
+        if(!StringUtils.isEmpty(filters)){
+            JSONObject json = JSONObject.parseObject(filters);
+            Set<Entry<String, Object>> entrySet = json.entrySet();
+            for (Entry<String, Object> entry : entrySet) {
+                FilterField field = FilterField.valueOf(entry.getKey());
+                switch (field) {
+                    case GOODS_KINDID:
+                    case GOODS_BRANDID:
+                    case GOODS_STORE_TYPE:
+                    case GOODS_CRNAME:
+                        filtersMap.put(field, entry.getValue());
+                        break;
+                }
+            }
+        }
+		return searchService.collectBySearchGoods(keyword, CollectField.valueOf(collectedField.toUpperCase()),
+                filtersMap);
+	}
+
+	@RequestMapping("/kind/{id}")
+	@ResponseBody
+	public Kind getKind(@PathVariable Long id, HttpServletRequest request) {
+		return searchService.getKind(id);
+	}
+
+	@RequestMapping("/brand/{id}")
+	@ResponseBody
+	public Brand getBrand(@PathVariable Long id, HttpServletRequest request) {
+		return searchService.getBrand(id);
+	}
+
+	@RequestMapping("/component/{id}")
+	@ResponseBody
+	public Component getComponent(@PathVariable Long id, HttpServletRequest request) {
+		return searchService.getComponent(id);
+	}
+
+	@RequestMapping("/goods/{id}")
+	@ResponseBody
+	public Goods getGoods(@PathVariable String id, HttpServletRequest request) {
+		return searchService.getGoods(id);
+	}
+
+	@RequestMapping("/objects")
+	@ResponseBody
+	public SPage<Object> getObjects(@RequestParam(required = true) String tableName, String keyword, String field,
+									Boolean tokenized, Integer page, Integer size, HttpServletRequest request) {
+		return searchService.getObjects(tableName.toLowerCase(), keyword, field, tokenized, page == null ? 0 : page,
+				size == null ? 0 : size);
+	}
+
+	@RequestMapping("/allObjectsToFiles")
+	@ResponseBody
+	public String writeAllObjectsToFiles(@RequestParam(required = true) String tableName, HttpServletRequest request) {
+		int page = 1;
+		int size = 1000;
+		// 不能边更新索引边分页获取索引中的数据,因为索引更新后,分页顺序可能也会变化,
+		// 所以要先把数据保存到本地,等待全部获取之后重建索引
+		Long startTime = new Date().getTime();
+		// 先删除旧的文件
+		FileUtils.deleteSubFiles(new File(SearchUtils.getDataPath(tableName)));
+
+		SPage<Object> sPage = searchService.getObjects(tableName.toLowerCase(), null, null, null, page, size);
+		// 索引中数据的总数目
+		long totalElements = sPage.getTotalElement();
+		logger.info("发现数据:" + totalElements + "条");
+		int fileIndex = 1;
+		PrintWriter printWriter = null;
+		int count = 0;
+		try {
+			File file = new File(SearchUtils.getDataPath(tableName));
+			if (!file.exists()) {
+				file.mkdirs();
+			}
+			printWriter = new PrintWriter(SearchUtils.getDataPath(tableName) + "/" + fileIndex + ".txt");
+			while (true) {
+				// 一个文件存放100000条数据,一旦超过,写入新的文件
+				if (count > IndexServiceImpl.SINGLE_FILE_MAX_SIZE) {
+					count = 1;
+					printWriter.flush();
+					printWriter.close();
+					fileIndex++;
+					printWriter = new PrintWriter(SearchUtils.getDataPath(tableName) + "/" + fileIndex + ".txt");
+				}
+				List<Object> content = sPage.getContent();
+				for (Object element : content) {
+					printWriter.println(JSONObject.toJSONString(element));
+					count++;
+				}
+				logger.info(String.format(tableName + "...................%.2f%%",
+						((page - 1) * size + content.size()) * 100.0 / totalElements));
+
+				if (++page > sPage.getTotalPage()) {
+					break;
+				}
+				sPage = searchService.getObjects(tableName.toLowerCase(), null, null, null, page, size);
+			}
+			printWriter.flush();
+			printWriter.close();
+		} catch (FileNotFoundException | SecurityException | IllegalArgumentException e) {
+			throw new SearchException(e).setDetailedMessage(e);
+		}
+		Long endTime = new Date().getTime();
+		String message = String.format("写入数据%s条,耗时%.2fs\n ", totalElements, (endTime - startTime) / 1000.0);
+		logger.info(message);
+		return message;
+	}
+}

+ 52 - 0
mall-search/src/main/java/com/uas/search/controller/UploadController.java

@@ -0,0 +1,52 @@
+package com.uas.search.controller;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
+
+@Controller
+@RequestMapping("/upload")
+public class UploadController {
+
+	private static final String UPLOAD_DIR = System.getProperty("java.io.tmpdir");
+
+	private static Logger logger = LoggerFactory.getLogger(UploadController.class);
+
+	@RequestMapping
+	@ResponseBody
+	public String upload(MultipartFile file, HttpServletRequest request) {
+		String message = "";
+		if (file == null || file.isEmpty()) {
+			message = "文件为空,无法进行上传!";
+			logger.error(message);
+			return message;
+		}
+
+		String fileName = file.getOriginalFilename();
+		String targetFilePath = (UPLOAD_DIR.endsWith(File.separator) ? UPLOAD_DIR : UPLOAD_DIR + "/") + fileName;
+		File targetFile = new File(targetFilePath);
+		if (!targetFile.getParentFile().exists()) {
+			targetFile.getParentFile().mkdirs();
+		}
+		try {
+			file.transferTo(targetFile);
+			message = "成功上传文件至 :" + targetFile.getCanonicalPath();
+			logger.info(message);
+			return message;
+		} catch (IllegalStateException | IOException e) {
+			logger.error("", e);
+		}
+
+		message = "上传文件失败: " + fileName;
+		logger.error(message);
+		return message;
+	}
+}

+ 24 - 0
mall-search/src/main/java/com/uas/search/dao/BrandDao.java

@@ -0,0 +1,24 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.Brand;
+
+/**
+ * @author sunyj
+ * @since 2016年7月7日 下午5:52:52
+ */
+@Repository
+public interface BrandDao
+		extends JpaSpecificationExecutor<Brand>, JpaRepository<Brand, Long> {
+
+	/**
+	 * 根据id获取品牌
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Brand findById(Long id);
+}

+ 18 - 0
mall-search/src/main/java/com/uas/search/dao/ComponentDao.java

@@ -0,0 +1,18 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.Component;
+
+/**
+ * @author sunyj
+ * @since 2016年7月7日 下午5:59:24
+ */
+@Repository
+public interface ComponentDao
+		extends JpaSpecificationExecutor<Component>, JpaRepository<Component, Long> {
+
+	public Component findByUuid(String uuid);
+}

+ 125 - 0
mall-search/src/main/java/com/uas/search/dao/GoodsDao.java

@@ -0,0 +1,125 @@
+package com.uas.search.dao;
+
+import com.uas.search.model.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import com.uas.search.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author sunyj
+ * @since 2017年7月8日 下午7:45:01
+ */
+@Repository
+public class GoodsDao {
+
+    @Autowired
+    private TradeGoodsDao tradeGoodsDao;
+
+    @Autowired
+    private ComponentDao componentDao;
+
+    @Autowired
+    private StoreDao storeDao;
+
+    @Autowired
+    private ProductsDao productsDao;
+
+    /**
+     * 根据批次 id 或者器件 id 获取批次
+     *
+     * @param goods 批次 id 或者器件 id
+     * @return 批次
+     */
+    public List<Goods> find(Goods goods) {
+        if (goods == null) {
+            return null;
+        }
+        List<Goods> goodsesList = new ArrayList<>();
+        if (goods.getTradeGoods() != null && goods.getTradeGoods().getId() != null) {
+            goodsesList.add(findByGoId(goods.getTradeGoods().getId()));
+        } else if (goods.getComponent() != null && goods.getComponent().getId() != null) {
+            goodsesList.addAll(findByCmpId(goods.getComponent().getId()));
+        }
+        return goodsesList;
+    }
+
+    /**
+     * 根据批次 id 获取批次信息
+     *
+     * @param goId 批次 id
+     * @return 批次信息
+     */
+    private Goods findByGoId(Long goId) {
+        TradeGoods tradeGoods = tradeGoodsDao.findOne(goId);
+        if (tradeGoods == null) {
+            throw new IllegalStateException("批次不存在:" + goId);
+        }
+        return findByTradeGoods(tradeGoods);
+    }
+
+    /**
+     * 获取批次信息
+     *
+     * @param tradeGoods 批次
+     * @return 批次信息
+     */
+    public Goods findByTradeGoods(TradeGoods tradeGoods) {
+        Component component = componentDao.findByUuid(tradeGoods.getCmpUuid());
+        Store store = null;
+        Products products = null;
+        if (tradeGoods.getStoreId() != null) {
+            store = storeDao.findByUuid(tradeGoods.getStoreId());
+        }
+        if (tradeGoods.getProductId() != null) {
+            products = productsDao.findOne(tradeGoods.getProductId());
+        }
+        return new Goods(tradeGoods, store, component, products);
+    }
+
+    /**
+     * 根据器件 id 获取批次信息
+     *
+     * @param cmpId 器件id
+     * @return 批次信息
+     */
+    public List<Goods> findByCmpId(Long cmpId) {
+        Component component = componentDao.findOne(cmpId);
+        if (component == null) {
+            throw new IllegalStateException("器件不存在:" + cmpId);
+        }
+        return findByComponent(component);
+    }
+
+    /**
+     * 根据器件获取批次信息
+     *
+     * @param component 器件
+     * @return 批次信息
+     */
+    public List<Goods> findByComponent(Component component) {
+        if (component == null) {
+            return null;
+        }
+        List<TradeGoods> tradeGoodsesList = tradeGoodsDao.findByCmpUuid(component.getUuid());
+        List<Goods> goodsesList = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(tradeGoodsesList)) {
+            for (TradeGoods tradeGoods : tradeGoodsesList) {
+                Store store = null;
+                Products products = null;
+                if (tradeGoods.getStoreId() != null) {
+                    store = storeDao.findByUuid(tradeGoods.getStoreId());
+                }
+                if (tradeGoods.getProductId() != null) {
+                    products = productsDao.findOne(tradeGoods.getProductId());
+                }
+                goodsesList.add(new Goods(tradeGoods, store, component, products));
+            }
+        } else {
+            goodsesList.add(new Goods(null, null, component, null));
+        }
+        return goodsesList;
+    }
+}

+ 24 - 0
mall-search/src/main/java/com/uas/search/dao/KindDao.java

@@ -0,0 +1,24 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.Kind;
+
+/**
+ * @author sunyj
+ * @since 2016年7月7日 下午5:46:35
+ */
+@Repository
+public interface KindDao
+		extends JpaSpecificationExecutor<Kind>, JpaRepository<Kind, Long> {
+
+	/**
+	 * 根据id获取类目
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Kind findById(Long id);
+}

+ 23 - 0
mall-search/src/main/java/com/uas/search/dao/OrderDao.java

@@ -0,0 +1,23 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.Order;
+
+/**
+ * @author sunyj
+ * @since 2016年10月14日 上午11:58:50
+ */
+@Repository
+public interface OrderDao
+		extends JpaSpecificationExecutor<Order>, JpaRepository<Order, Long> {
+	/**
+	 * 根据id获取销售单
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Order findById(Long id);
+}

+ 23 - 0
mall-search/src/main/java/com/uas/search/dao/OrderInvoiceDao.java

@@ -0,0 +1,23 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.OrderInvoice;
+
+/**
+ * @author sunyj
+ * @since 2016年10月14日 上午11:58:50
+ */
+@Repository
+public interface OrderInvoiceDao
+		extends JpaSpecificationExecutor<OrderInvoice>, JpaRepository<OrderInvoice, Long> {
+	/**
+	 * 根据id获取销售发货单
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public OrderInvoice findById(Long id);
+}

+ 14 - 0
mall-search/src/main/java/com/uas/search/dao/ProductsDao.java

@@ -0,0 +1,14 @@
+package com.uas.search.dao;
+
+import com.uas.search.model.Products;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author sunyj
+ * @since 2017/11/24 17:02
+ */
+@Repository
+public interface ProductsDao extends JpaSpecificationExecutor<Products>, JpaRepository<Products, Long> {
+}

+ 23 - 0
mall-search/src/main/java/com/uas/search/dao/PurchaseDao.java

@@ -0,0 +1,23 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.Purchase;
+
+/**
+ * @author sunyj
+ * @since 2016年10月14日 上午11:58:50
+ */
+@Repository
+public interface PurchaseDao
+		extends JpaSpecificationExecutor<Purchase>, JpaRepository<Purchase, Long> {
+	/**
+	 * 根据id获取销售单
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Purchase findById(Long id);
+}

+ 23 - 0
mall-search/src/main/java/com/uas/search/dao/PurchaseInvoiceDao.java

@@ -0,0 +1,23 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.PurchaseInvoice;
+
+/**
+ * @author sunyj
+ * @since 2016年10月14日 上午11:58:50
+ */
+@Repository
+public interface PurchaseInvoiceDao
+		extends JpaSpecificationExecutor<PurchaseInvoice>, JpaRepository<PurchaseInvoice, Long> {
+	/**
+	 * 根据id获取销售发货单
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public PurchaseInvoice findById(Long id);
+}

+ 18 - 0
mall-search/src/main/java/com/uas/search/dao/StoreDao.java

@@ -0,0 +1,18 @@
+package com.uas.search.dao;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.Store;
+
+/**
+ * @author sunyj
+ * @since 2017年7月22日 下午8:16:07
+ */
+@Repository
+public interface StoreDao
+		extends JpaSpecificationExecutor<Store>, JpaRepository<Store, Long> {
+
+	public Store findByUuid(String uuid);
+}

+ 20 - 0
mall-search/src/main/java/com/uas/search/dao/TradeGoodsDao.java

@@ -0,0 +1,20 @@
+package com.uas.search.dao;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.stereotype.Repository;
+
+import com.uas.search.model.TradeGoods;
+
+/**
+ * @author sunyj
+ * @since 2017年7月22日 下午8:16:07
+ */
+@Repository
+public interface TradeGoodsDao
+		extends JpaSpecificationExecutor<TradeGoods>, JpaRepository<TradeGoods, Long> {
+
+	public List<TradeGoods> findByCmpUuid(String cmpUuid);
+}

+ 90 - 0
mall-search/src/main/java/com/uas/search/exception/SearchException.java

@@ -0,0 +1,90 @@
+package com.uas.search.exception;
+
+/**
+ * 搜索异常
+ * 
+ * @author sunyj
+ * @since 2016年9月30日 下午3:32:48
+ */
+public class SearchException extends RuntimeException {
+
+	private static final long serialVersionUID = 1L;
+
+	private String message;
+
+	/**
+	 * 详细信息
+	 */
+	private String detailedMessage;
+
+	public SearchException(String message) {
+		this.message = message;
+	}
+
+	public SearchException(Throwable e) {
+		this.message = getMessage(e);
+	}
+
+	/**
+	 * 获取异常及其Cause拼接成的字符串
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 拼接后的结果
+	 */
+	public String getMessage(Throwable e) {
+		StringBuilder sb = new StringBuilder(e.toString());
+		if (e.getCause() != null) {
+			sb.append("\nCaused by: ").append(getMessage(e.getCause()));
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * 获取异常及其Cause的StackTrace拼接成的字符串
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 拼接后的结果
+	 */
+	public String getDetailedMessage(Throwable e) {
+		StringBuilder sb = new StringBuilder(e.toString());
+		StackTraceElement[] stackTraceElements = e.getStackTrace();
+		for (StackTraceElement stackTraceElement : stackTraceElements) {
+			sb.append("\n\t").append(stackTraceElement.toString());
+		}
+		if (e.getCause() != null) {
+			sb.append("\nCaused by: ").append(getDetailedMessage(e.getCause()));
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * 获取异常及其Cause的StackTrace用以设置详细信息
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 本SearchException对象
+	 */
+	public SearchException setDetailedMessage(Throwable e) {
+		this.detailedMessage = getDetailedMessage(e);
+		return this;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public void setMessage(String message) {
+		this.message = message;
+	}
+
+	public String getDetailedMessage() {
+		return detailedMessage;
+	}
+
+	public void setDetailedMessage(String detailedMessage) {
+		this.detailedMessage = detailedMessage;
+	}
+
+}

+ 101 - 0
mall-search/src/main/java/com/uas/search/grouping/DistinctGroupCollector.java

@@ -0,0 +1,101 @@
+package com.uas.search.grouping;
+
+import com.uas.search.util.ArrayUtils;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.SimpleCollector;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class DistinctGroupCollector extends SimpleCollector {
+
+	private final String groupField;
+
+	private final CollectField[] collectFields;
+
+	private final Set<String> fieldsToLoad = new HashSet<>();
+
+	private LeafReader reader;
+
+	private Set<Long> values = new HashSet<>();
+
+	private Set<Map<String, Object>> collectValues = new HashSet<>();
+
+	public DistinctGroupCollector(String groupField, CollectField... collectFields) {
+		this.groupField = groupField;
+		this.collectFields = collectFields;
+		this.fieldsToLoad.add(groupField);
+		if(collectFields!=null){
+			for(CollectField collectField : collectFields){
+				fieldsToLoad.add(collectField.getFieldName());
+			}
+		}
+	}
+
+	@Override
+	public boolean needsScores() {
+		return false;
+	}
+
+	@Override
+	protected void doSetNextReader(LeafReaderContext context) throws IOException {
+		super.doSetNextReader(context);
+		reader = context.reader();
+	}
+
+	@Override
+	public void collect(int doc) throws IOException {
+		if (reader != null) {
+			Document document = reader.document(doc, fieldsToLoad);
+			String value = document.get(groupField);
+			if (value != null) {
+				values.add(Long.parseLong(value));
+				// 统计指定信息
+				if(!ArrayUtils.isEmpty(collectFields)){
+					Map<String, Object> collectValue = new HashMap<>();
+					for(CollectField collectField : collectFields){
+						String v = document.get(collectField.getFieldName());
+						if(v != null){
+							collectValue.put(collectField.getDisplayName(), v);
+						}
+					}
+					collectValues.add(collectValue);
+				}
+			}
+		}
+	}
+
+	public Set<Long> getValues() {
+		return values;
+	}
+
+	public Set<Map<String, Object>> getCollectValues(){
+		return collectValues;
+	}
+
+	/**
+	 * 用于统计的字段
+	 */
+	public static class CollectField {
+		private String fieldName;
+		private String displayName;
+
+		public CollectField(String fieldName, String displayName) {
+			this.fieldName = fieldName;
+			this.displayName = displayName;
+		}
+
+		public String getFieldName() {
+			return fieldName;
+		}
+
+		public String getDisplayName() {
+			return displayName;
+		}
+	}
+}

+ 77 - 0
mall-search/src/main/java/com/uas/search/grouping/GoodsGroupCollector.java

@@ -0,0 +1,77 @@
+package com.uas.search.grouping;
+
+import com.uas.search.util.StringUtils;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.LeafReader;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.SimpleCollector;
+
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * 批次统计
+ *
+ * @author sunyj
+ * @since 2017年7月12日 下午2:12:56
+ */
+public class GoodsGroupCollector extends SimpleCollector {
+
+    private Set<String> fieldsToLoad = new HashSet<>();
+
+    private List<String> uniqueValues = new ArrayList<>();
+
+    private String uniqueField;
+
+    private LeafReader reader;
+
+    private List<Map<String, Object>> values = new ArrayList<>();
+
+    public GoodsGroupCollector(String uniqueField, Set<String> fieldsToLoad) {
+        this.uniqueField = uniqueField;
+        this.fieldsToLoad.addAll(fieldsToLoad);
+    }
+
+    @Override
+    public boolean needsScores() {
+        return false;
+    }
+
+    @Override
+    protected void doSetNextReader(LeafReaderContext context) throws IOException {
+        super.doSetNextReader(context);
+        reader = context.reader();
+    }
+
+    @Override
+    public void collect(int doc) throws IOException {
+        if (null != reader) {
+            Document document = reader.document(doc);
+            String uniqueValue = document.get(uniqueField);
+            // 排除重复值
+            if (!StringUtils.isEmpty(uniqueValue)) {
+                if (!uniqueValues.contains(uniqueValue)) {
+                    Map<String, Object> map = new HashMap<>();
+                    uniqueValues.add(uniqueValue);
+                    for (String field : fieldsToLoad) {
+                        String value = document.get(field);
+                        map.put(field, value);
+                    }
+                    map.put("count", 1L);
+                    values.add(map);
+                } else {
+                    // 统计数目
+                    int index = uniqueValues.indexOf(uniqueValue);
+                    Map<String, Object> map = values.get(index);
+                    Long count = (Long) map.get("count");
+                    map.put("count", count + 1);
+                }
+            }
+        }
+    }
+
+    public List<Map<String, Object>> getValues() {
+        return values;
+    }
+
+}

+ 152 - 0
mall-search/src/main/java/com/uas/search/jms/JmsListener.java

@@ -0,0 +1,152 @@
+package com.uas.search.jms;
+
+import com.uas.search.constant.model.SPage;
+import com.uas.search.model.ParsedQueueMessage;
+import com.uas.search.schedule.TaskInformation;
+import com.uas.search.schedule.Executable;
+import com.uas.search.schedule.TaskService;
+import com.uas.search.service.IndexService;
+import com.uas.search.util.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**对数据库的消息队列进行实时监听
+ *
+ * @author sunyj
+ * @since 2017/10/11 16:20
+ */
+@Service
+public class JmsListener {
+
+    /**
+     * 第一次执行的延迟时间间隔为 3 秒,以防止日志交杂错乱,不好查看
+     */
+    private static final long INITIAL_DELAY = 3000;
+    /**
+     * 两次任务之间的等待时间间隔为 1 秒钟
+     */
+    private static final long INTERVAL = 1000;
+
+    @Autowired
+    private LuceneMessageDao luceneMessageDao;
+
+    @Autowired
+    private IndexService indexService;
+
+    @Autowired
+    private TaskService taskService;
+
+    @Autowired
+    private LuceneMessageService luceneMessageService;
+
+    @Autowired
+    private QueueMessageParser queueMessageParser;
+
+    private TaskInformation taskInformation;
+
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * 开启实时更新索引
+     *
+     * @param interval
+     *            每次接收到jms消息后等待的时间(秒)
+     * @return 开启成功与否的提示信息
+     */
+    public void start(Long interval) {
+        if(interval == null){
+            interval = INTERVAL;
+        }else{
+            interval *= 1000;
+        }
+        if(interval <= 0){
+            throw new IllegalArgumentException("interval 不合法:" + interval);
+        }
+
+        if (isRunning()) {
+            throw new IllegalStateException("已存在运行的索引实时更新服务");
+        }
+
+        try {
+            String title = "实时更新";
+            Executable command = new Executable() {
+                @Override
+                public String execute() {
+                    SPage<LuceneMessage> sPage = luceneMessageService.findAll(1, 100);
+                    List<LuceneMessage> luceneMessages = sPage.getContent();
+                    if (CollectionUtils.isEmpty(luceneMessages)) {
+                        return "无消息";
+                    }
+                    for(LuceneMessage luceneMessage : luceneMessages){
+                        try {
+                            process(luceneMessage);
+                        } catch (Throwable e) {
+                            logger.error("", e);
+                        }
+                    }
+                    // 如果消息不止一页,立即消费下一页
+                    if(sPage.getTotalPage() > 1){
+                        execute();
+                    }
+                    return "正常";
+                }
+            };
+            taskInformation = new TaskInformation(title, command, INITIAL_DELAY, interval, TaskInformation.ScheduleType.FixedDelay);
+            taskService.newTask(taskInformation);
+            if (!taskService.isStopped()) {
+                taskService.stop();
+            }
+            taskService.start();
+        } catch (Throwable e) {
+            if (taskInformation != null) {
+                taskInformation = null;
+            }
+            throw new IllegalStateException("开启失败", e);
+        }
+    }
+
+    /**
+     * 关闭实时更新索引服务
+     *
+     * @return 关闭成功与否的提示信息
+     */
+    public void stop() {
+        if (!isRunning()) {
+            throw new IllegalStateException("索引实时更新服务未开启或已关闭");
+        } else {
+            taskService.remove(taskInformation.getCode());
+            taskInformation = null;
+            if (!taskService.isStopped()) {
+                taskService.stop();
+            }
+            taskService.start();
+        }
+    }
+
+    /**
+     * @return 索引实时更新服务是否正在运行
+     */
+    public boolean isRunning() {
+        return taskInformation != null && taskService.exist(taskInformation.getCode());
+    }
+
+    /**
+     * 对得到的队列消息进行解析,之后根据解析出来的对象,对lucene索引进行添加、更新或删除操作
+     *
+     * @param luceneMessage 消息
+     */
+    private void process(LuceneMessage luceneMessage) {
+        logger.info(luceneMessage.toString());
+        Long id = luceneMessage.getId();
+        luceneMessage.setRetryCount(luceneMessage.getRetryCount() + 1);
+        luceneMessageDao.save(luceneMessage);
+        ParsedQueueMessage parsedQueueMessage = queueMessageParser.parse(luceneMessage.getTableName(), luceneMessage.getDataId(), luceneMessage.getMethodType(), luceneMessage.getData());
+        List<Object> maintainedObjects = indexService.maintainIndexes(parsedQueueMessage);
+        logger.info("Maintained... " + maintainedObjects);
+        luceneMessageDao.dequeueLuceneMessage(id);
+    }
+}

+ 153 - 0
mall-search/src/main/java/com/uas/search/jms/LuceneMessage.java

@@ -0,0 +1,153 @@
+package com.uas.search.jms;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * 用于实时更新的消息
+ *
+ * @author sunyj
+ * @since 2017/10/11 16:12
+ */
+@Entity
+@Table(name = "lucene$message")
+public class LuceneMessage implements Serializable {
+
+    /**
+     * 序列号
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * id
+     */
+    @Id
+    @Column(name = "me_id")
+    private Long id;
+
+    /**
+     * 表名
+     */
+    @Column(name = "me_table_name")
+    private String tableName;
+
+    /**
+     * 更改类型
+     */
+    @Column(name = "me_method_type")
+    private String methodType;
+
+    /**
+     * 数据 id
+     */
+    @Column(name = "me_data_id")
+    private Long dataId;
+
+    /**
+     * 数据,可为空
+     */
+    @Column(name = "me_data")
+    private String data;
+
+    /**
+     * 优先级
+     */
+    @Column(name = "me_priority")
+    private Long priority;
+
+    /**
+     * 尝试次数
+     */
+    @Column(name = "me_retry_count")
+    private Long retryCount;
+
+    /**
+     * 时间
+     */
+    @Column(name = "me_create_time")
+    private Date createTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getTableName() {
+        return tableName;
+    }
+
+    public void setTableName(String tableName) {
+        this.tableName = tableName;
+    }
+
+    public String getMethodType() {
+        return methodType;
+    }
+
+    public void setMethodType(String methodType) {
+        this.methodType = methodType;
+    }
+
+    public Long getDataId() {
+        return dataId;
+    }
+
+    public void setDataId(Long dataId) {
+        this.dataId = dataId;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+
+    public Long getPriority() {
+        return priority;
+    }
+
+    public void setPriority(Long priority) {
+        this.priority = priority;
+    }
+
+    public Long getRetryCount() {
+        return retryCount;
+    }
+
+    public void setRetryCount(Long retryCount) {
+        this.retryCount = retryCount;
+    }
+
+    public String getCreateTime() {
+        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(createTime);
+    }
+
+    public void setCreateTime(Date createTime) {
+        this.createTime = createTime;
+    }
+
+    @Override
+    public String toString() {
+        return "LuceneMessage{" +
+                "id=" + id +
+                ", tableName='" + tableName + '\'' +
+                ", methodType='" + methodType + '\'' +
+                ", dataId=" + dataId +
+                ", data='" + data + '\'' +
+                ", priority=" + priority +
+                ", retryCount=" + retryCount +
+                ", createTime=" + createTime +
+                '}';
+    }
+}
+

+ 46 - 0
mall-search/src/main/java/com/uas/search/jms/LuceneMessageDao.java

@@ -0,0 +1,46 @@
+package com.uas.search.jms;
+
+import com.uas.search.annotation.NotEmpty;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.query.Procedure;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+/**
+ * @author sunyj
+ * @since 2016年7月7日 下午5:52:52
+ */
+@Repository
+public interface LuceneMessageDao
+        extends JpaSpecificationExecutor<LuceneMessage>, JpaRepository<LuceneMessage, Long> {
+
+    /**
+     * 获取指定数目的数据
+     *
+     * @param start 开始的记录
+     * @param size 指定数目
+     * @return 数据
+     */
+    @Query(value = "select * from lucene$message where me_retry_count < 5 order by me_priority desc, me_create_time limit ?1, ?2", nativeQuery = true)
+    List<LuceneMessage> findList(@NotEmpty("start") Integer start, @NotEmpty("size") Integer size);
+
+    /**
+     * 统计行数
+     * @return 行数
+     */
+    @Query(value = "select count(1) from lucene$message where me_retry_count < 5", nativeQuery = true)
+    long count();
+
+    /**
+     * 出队消息
+     * @param id 消息 id
+     */
+    @Transactional
+    @Procedure(procedureName = "dequeue_lucene_message")
+    void dequeueLuceneMessage(@NotEmpty("id") Long id);
+}

+ 23 - 0
mall-search/src/main/java/com/uas/search/jms/LuceneMessageService.java

@@ -0,0 +1,23 @@
+package com.uas.search.jms;
+
+import com.uas.search.constant.model.SPage;
+
+/**
+ * 消息
+ *
+ * @author sunyj
+ * @since 2017/10/11 17:31
+ */
+public interface LuceneMessageService {
+
+    /**
+     * 获取消息
+     *
+     * @param page
+     *            页码
+     * @param size
+     *            页大小
+     * @return 消息(实时更新情况的详细信息)
+     */
+    SPage<LuceneMessage> findAll(Integer page, Integer size);
+}

+ 68 - 0
mall-search/src/main/java/com/uas/search/jms/LuceneMessageServiceImpl.java

@@ -0,0 +1,68 @@
+package com.uas.search.jms;
+
+import com.uas.search.constant.model.SPage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 消息
+ *
+ * @author sunyj
+ * @since 2017/10/11 17:33
+ */
+@Service
+public class LuceneMessageServiceImpl implements LuceneMessageService {
+
+    /**
+     * 默认的页码
+     */
+    private static final int PAGE_INDEX = 1;
+
+    /**
+     * 默认每页的大小
+     */
+    private static final int PAGE_SIZE = 20;
+
+    @Autowired
+    private LuceneMessageDao luceneMessageDao;
+
+    @Override
+    public SPage<LuceneMessage> findAll(Integer page, Integer size) {
+        // 处理分页信息
+        if (page == null || page <= 0) {
+            page = PAGE_INDEX;
+        }
+        if (size == null || size <= 0) {
+            size = PAGE_SIZE;
+        }
+
+        SPage<LuceneMessage> sPage = new SPage<>();
+        long totalElement = luceneMessageDao.count();
+        sPage.setTotalElement(totalElement);
+        // 总数目为0,返回
+        if (totalElement == 0) {
+            return sPage;
+        }
+
+        int totalPage = (int) Math.ceil(totalElement / (1.0 * size));
+        sPage.setTotalPage(totalPage);
+        // 如果页码过大
+        if (page > totalPage) {
+            page = totalPage;
+        }
+        int start = (page - 1) * size;
+        sPage.setPage(page);
+        sPage.setSize(size);
+        if (page == 1) {
+            sPage.setFirst(true);
+        }
+        if (page == totalPage) {
+            sPage.setLast(true);
+        }
+        List<LuceneMessage> messages = luceneMessageDao.findList(start, size);
+        sPage.setContent(messages);
+        return sPage;
+    }
+}

+ 253 - 0
mall-search/src/main/java/com/uas/search/jms/QueueMessageParser.java

@@ -0,0 +1,253 @@
+package com.uas.search.jms;
+
+import com.alibaba.fastjson.JSONException;
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.dao.*;
+import com.uas.search.model.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 对得到的队列消息进行解析的工具
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午6:14:03
+ */
+@Service
+public class QueueMessageParser {
+
+	@Autowired
+	private KindDao kindDao;
+
+	@Autowired
+	private BrandDao brandDao;
+
+	@Autowired
+	private ComponentDao componentDao;
+
+	@Autowired
+	private OrderDao orderDao;
+
+	@Autowired
+	private OrderInvoiceDao orderInvoiceDao;
+
+	@Autowired
+	private PurchaseDao purchaseDao;
+
+	@Autowired
+	private PurchaseInvoiceDao purchaseInvoiceDao;
+
+	/**
+	 * 对得到的json消息进行解析
+	 * 
+	 * @param tableName 表名
+	 * @param dataId 数据 id
+	 * @param methodType 更改类型
+	 * @param data 数据
+	 * @return ParsedQueueMessage对象
+	 * @throws JSONException
+	 */
+	public ParsedQueueMessage parse(@NotEmpty("tableName") String tableName, @NotEmpty("dataId") Long dataId, @NotEmpty("methodType") String methodType, String data) throws JSONException {
+		ParsedQueueMessage parsedQueueMessage = new ParsedQueueMessage();
+		Object object;
+		try {
+			// 解析数据库表的更改类型
+			if (methodType.equals("insert")) {
+				parsedQueueMessage.setMethodType(ParsedQueueMessage.INSERT);
+			} else if (methodType.equals("update")) {
+				parsedQueueMessage.setMethodType(ParsedQueueMessage.UPDATE);
+			} else if (methodType.equals("delete")) {
+				parsedQueueMessage.setMethodType(ParsedQueueMessage.DELETE);
+			} else {
+				throw new IllegalStateException("unsupported method type: " + methodType);
+			}
+
+			// 解析哪个表有更改
+			if (tableName.equals(SearchConstants.KIND_TABLE_NAME)) {
+				object = parseKind(dataId, methodType);
+			} else if (tableName.equals(SearchConstants.BRAND_TABLE_NAME)) {
+				object = parseBrand(dataId, methodType);
+			} else if (tableName.equals(SearchConstants.COMPONENT_TABLE_NAME)) {
+				object = parseComponent(dataId, methodType);
+			} else if (tableName.equals(SearchConstants.GOODS_TABLE_NAME)) {
+				object = parseGoods(dataId, data);
+			} else if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+				object = parseOrder(dataId, methodType);
+			} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+				object = parseOrderInvoice(dataId, methodType);
+			} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+				object = parsePurchase(dataId, methodType);
+			} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+				object = parsePurchaseInvoice(dataId, methodType);
+			} else {
+				throw new IllegalStateException("unsupported table name: " + tableName);
+			}
+		} catch (Throwable e) {
+			// 防止SQLRecoverableException导致应用终止
+			throw new IllegalStateException("message parsing failed!", e);
+		}
+
+		parsedQueueMessage.setObject(object);
+		return parsedQueueMessage;
+	}
+
+	/**
+	 * 对kind类目进行解析
+	 * 
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return kind类目对象
+	 * @throws JSONException
+	 */
+	private Kind parseKind(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		Kind kind;
+		// delete操作
+		// 删除后数据库中可能已经没有相应数据了,无法通过dao获取,需要手动创建对象
+		if (methodType.equalsIgnoreCase("delete")) {
+			kind = new Kind();
+			kind.setId(id);
+		} else {
+			kind = kindDao.findOne(id);
+		}
+		return kind;
+	}
+
+	/**
+	 * 对brand品牌进行解析
+	 *
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return brand品牌对象
+	 * @throws JSONException
+	 */
+	private Brand parseBrand(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		Brand brand;
+		if (methodType.equalsIgnoreCase("delete")) {
+			brand = new Brand();
+			brand.setId(id);
+		} else {
+			brand = brandDao.findOne(id);
+		}
+		return brand;
+	}
+
+	/**
+	 * 对component器件进行解析
+	 *
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return component器件对象
+	 * @throws JSONException
+	 */
+	private Component parseComponent(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		Component component;
+		if (methodType.equalsIgnoreCase("delete")) {
+			component = new Component();
+			component.setId(id);
+		} else {
+			component = componentDao.findOne(id);
+		}
+		return component;
+	}
+
+	/**
+	 * 对goods进行解析
+	 *
+	 * @param id id
+	 * @param data 数据
+	 * @return 批次对象
+	 * @throws JSONException
+	 */
+	private Goods parseGoods(@NotEmpty("id") Long id, @NotEmpty("data") String data) throws JSONException {
+		Goods goods = new Goods();
+		if (data == null || data.equalsIgnoreCase("cmpId")) {
+            Component component = new Component();
+            component.setId(id);
+            goods.setComponent(component);
+		} else if (data.equalsIgnoreCase("goId")){
+            TradeGoods tradeGoods = new TradeGoods();
+            tradeGoods.setId(id);
+			goods.setTradeGoods(tradeGoods);
+		} else {
+            throw new IllegalArgumentException("未指定是 cmpId 还是 goId");
+        }
+		return goods;
+	}
+
+	/**
+	 * 对销售单进行解析
+	 *
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return 销售单对象
+	 * @throws JSONException
+	 */
+	private Order parseOrder(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		Order order;
+		if (methodType.equalsIgnoreCase("delete")) {
+			order = new Order();
+			order.setId(id);
+		} else {
+			order = orderDao.findOne(id);
+		}
+		return order;
+	}
+
+	/**
+	 * 对销售发货单进行解析
+	 *
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return 销售发货单对象
+	 * @throws JSONException
+	 */
+	private OrderInvoice parseOrderInvoice(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		OrderInvoice orderInvoice;
+		if (methodType.equalsIgnoreCase("delete")) {
+			orderInvoice = new OrderInvoice();
+			orderInvoice.setId(id);
+		} else {
+			orderInvoice = orderInvoiceDao.findOne(id);
+		}
+		return orderInvoice;
+	}
+
+	/**
+	 * 对采购单进行解析
+	 *
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return 采购单对象
+	 * @throws JSONException
+	 */
+	private Purchase parsePurchase(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		Purchase purchase;
+		if (methodType.equalsIgnoreCase("delete")) {
+			purchase = new Purchase();
+			purchase.setId(id);
+		} else {
+			purchase = purchaseDao.findOne(id);
+		}
+		return purchase;
+	}
+
+	/**
+	 * 对采购发货单进行解析
+	 *
+	 * @param id id
+	 * @param methodType 改动类型
+	 * @return 采购发货单对象
+	 * @throws JSONException
+	 */
+	private PurchaseInvoice parsePurchaseInvoice(@NotEmpty("id") Long id, @NotEmpty("methodType") String methodType) throws JSONException {
+		PurchaseInvoice purchaseInvoice;
+		if (methodType.equalsIgnoreCase("delete")) {
+			purchaseInvoice = new PurchaseInvoice();
+			purchaseInvoice.setId(id);
+		} else {
+			purchaseInvoice = purchaseInvoiceDao.findOne(id);
+		}
+		return purchaseInvoice;
+	}
+}

+ 73 - 0
mall-search/src/main/java/com/uas/search/model/BaseOrderDetail.java

@@ -0,0 +1,73 @@
+package com.uas.search.model;
+
+/**
+ * 单据明细基类
+ * 
+ * @author sunyj
+ * @since 2016年10月31日 上午11:30:35
+ */
+public class BaseOrderDetail {
+	private Long id;
+	private String code;
+	private Short detno;
+	private String cmpCode;
+	private String kiName;
+	private String brName;
+	private Integer status;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getCmpCode() {
+		return cmpCode;
+	}
+
+	public void setCmpCode(String cmpCode) {
+		this.cmpCode = cmpCode;
+	}
+
+	public String getKiName() {
+		return kiName;
+	}
+
+	public void setKiName(String kiName) {
+		this.kiName = kiName;
+	}
+
+	public String getBrName() {
+		return brName;
+	}
+
+	public void setBrName(String brName) {
+		this.brName = brName;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+}

+ 135 - 0
mall-search/src/main/java/com/uas/search/model/Brand.java

@@ -0,0 +1,135 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ * 品牌简要信息,只用于索引的创建与查询
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午5:48:32
+ */
+@Entity
+@Table(name = "product$brand")
+public class Brand implements Serializable {
+
+	/**
+	 * 序列号
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "br_id")
+	private Long id;
+
+	/**
+	 * uuid
+	 */
+	@Column(name = "br_uuid", unique = true)
+	private String uuid;
+
+	/**
+	 * 品牌中文名称
+	 */
+	@Column(name = "br_name_cn")
+	private String nameCn;
+
+	/**
+	 * 品牌英文名称
+	 */
+	@Column(name = "br_name_en")
+	private String nameEn;
+
+	/**
+	 * 访问量
+	 */
+	@Column(name = "br_visit_count")
+	private Long visitCount;
+
+	/**
+	 * 搜索次数
+	 */
+	@Column(name = "br_search_count")
+	private Long searchCount;
+
+	/**
+	 * 权重
+	 */
+	@Column(name = "br_weight")
+	private Double weight;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getUuid() {
+		return uuid;
+	}
+
+	public void setUuid(String uuid) {
+		this.uuid = uuid;
+	}
+
+	public String getNameCn() {
+		return nameCn;
+	}
+
+	public void setNameCn(String nameCn) {
+		this.nameCn = nameCn;
+	}
+
+	public String getNameEn() {
+		return nameEn;
+	}
+
+	public void setNameEn(String nameEn) {
+		this.nameEn = nameEn;
+	}
+
+	public Long getVisitCount() {
+		return visitCount;
+	}
+
+	public void setVisitCount(Long visitCount) {
+		this.visitCount = visitCount;
+	}
+
+	public Long getSearchCount() {
+		return searchCount;
+	}
+
+	public void setSearchCount(Long searchCount) {
+		this.searchCount = searchCount;
+	}
+
+	public Double getWeight() {
+		return weight;
+	}
+
+	public void setWeight(Double weight) {
+		this.weight = weight;
+	}
+
+	@Override
+	public String toString() {
+		return "Brand{" +
+				"id=" + id +
+				", uuid='" + uuid + '\'' +
+				", nameCn='" + nameCn + '\'' +
+				", nameEn='" + nameEn + '\'' +
+				", visitCount=" + visitCount +
+				", searchCount=" + searchCount +
+				", weight=" + weight +
+				'}';
+	}
+}

+ 231 - 0
mall-search/src/main/java/com/uas/search/model/Component.java

@@ -0,0 +1,231 @@
+package com.uas.search.model;
+
+import javax.persistence.*;
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * 器件简要信息,只用于索引的创建与查询
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午5:55:26
+ */
+@Entity
+@Table(name = "product$component")
+public class Component implements Serializable {
+
+	/**
+	 * 序列号
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "cmp_id")
+	private Long id;
+
+	/**
+	 * 器件的uuid
+	 */
+	@Column(name = "cmp_uuid", unique = true)
+	private String uuid;
+
+	/**
+	 * 原厂型号
+	 */
+	@Column(name = "cmp_code")
+	private String code;
+
+	/**
+	 * 类目
+	 */
+	@ManyToOne
+	@JoinColumn(name = "cmp_kiid")
+	private Kind kind;
+
+	/**
+	 * 品牌
+	 */
+	@ManyToOne
+	@JoinColumn(name = "cmp_brid")
+	private Brand brand;
+
+	/**
+	 * 以下为器件的库存交易属性,由器件对应的上架商品发生变化时,更新反应到器件
+	 */
+
+	/**
+	 * 器件的库存
+	 */
+	@Column(name = "cmp_reserve")
+	private Double reserve;
+
+	/**
+	 * 样品数量汇总
+	 */
+	@Column(name = "cmp_sampleqty")
+	private Double sampleQty;
+
+	/**
+	 * 现货数量
+	 */
+	@Column(name = "cmp_originalqty")
+	private Double originalQty;
+
+	/**
+	 * 呆滞库存数量
+	 */
+	@Column(name = "cmp_inastockqty")
+	private Double inactionStockQty;
+
+	/**
+	 * 描述
+	 */
+	@Column(name = "cmp_description")
+	private String description;
+
+	/**
+	 * 访问量
+	 */
+	@Column(name = "cmp_visit_count")
+	private Long visitCount;
+
+	/**
+	 * 搜索次数
+	 */
+	@Column(name = "cmp_search_count")
+	private Long searchCount;
+
+	/**
+	 * 带的属性及属性的值
+	 */
+	@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER)
+	@JoinColumn(name = "pv_componentid", updatable = false, insertable = false)
+	@OrderBy("detno")
+	private Set<PropertyValue> properties;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getUuid() {
+		return uuid;
+	}
+
+	public void setUuid(String uuid) {
+		this.uuid = uuid;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Kind getKind() {
+		return kind;
+	}
+
+	public void setKind(Kind kind) {
+		this.kind = kind;
+	}
+
+	public Brand getBrand() {
+		return brand;
+	}
+
+	public void setBrand(Brand brand) {
+		this.brand = brand;
+	}
+
+	public Double getReserve() {
+		return reserve;
+	}
+
+	public void setReserve(Double reserve) {
+		this.reserve = reserve;
+	}
+
+	public Double getSampleQty() {
+		return sampleQty;
+	}
+
+	public void setSampleQty(Double sampleQty) {
+		this.sampleQty = sampleQty;
+	}
+
+	public Double getOriginalQty() {
+		return originalQty;
+	}
+
+	public void setOriginalQty(Double originalQty) {
+		this.originalQty = originalQty;
+	}
+
+	public Double getInactionStockQty() {
+		return inactionStockQty;
+	}
+
+	public void setInactionStockQty(Double inactionStockQty) {
+		this.inactionStockQty = inactionStockQty;
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public Set<PropertyValue> getProperties() {
+		return properties;
+	}
+
+	public void setProperties(Set<PropertyValue> properties) {
+		this.properties = properties;
+	}
+
+	public Long getVisitCount() {
+		return visitCount;
+	}
+
+	public void setVisitCount(Long visitCount) {
+		this.visitCount = visitCount;
+	}
+
+	public Long getSearchCount() {
+		return searchCount;
+	}
+
+	public void setSearchCount(Long searchCount) {
+		this.searchCount = searchCount;
+	}
+
+	@Override
+	public String toString() {
+		return "Component{" +
+				"id=" + id +
+				", uuid='" + uuid + '\'' +
+				", code='" + code + '\'' +
+				", kind=" + kind +
+				", brand=" + brand +
+				", reserve=" + reserve +
+				", sampleQty=" + sampleQty +
+				", originalQty=" + originalQty +
+				", inactionStockQty=" + inactionStockQty +
+				", description='" + description + '\'' +
+				", visitCount=" + visitCount +
+				", searchCount=" + searchCount +
+				", properties=" + properties +
+				'}';
+	}
+}

+ 58 - 0
mall-search/src/main/java/com/uas/search/model/Enterprise.java

@@ -0,0 +1,58 @@
+/*CopyRright (c)2014: <www.usoftchina.com>
+ */
+package com.uas.search.model;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 企业信息
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:55:23
+ */
+@Entity
+@Table(name = "sec$enterprises")
+public class Enterprise implements Serializable {
+
+	/**
+	 * 序列号
+	 */
+	private static final long serialVersionUID = 1L;
+
+	@Id
+	@Column(name = "en_uu")
+	private Long uu;
+
+	/**
+	 * 公司名称
+	 */
+	@Column(name = "en_name")
+	private String enName;
+
+	public Long getUu() {
+		return uu;
+	}
+
+	public void setUu(Long uu) {
+		this.uu = uu;
+	}
+
+	public String getEnName() {
+		return enName;
+	}
+
+	public void setEnName(String enName) {
+		this.enName = enName;
+	}
+
+	@Override
+	public String toString() {
+		return "Enterprise [uu=" + uu + ", enName=" + enName + "]";
+	}
+
+}

+ 86 - 0
mall-search/src/main/java/com/uas/search/model/Goods.java

@@ -0,0 +1,86 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+
+/**
+ * 器件批次
+ * 
+ * @author sunyj
+ * @since 2017年7月8日 下午5:14:08
+ */
+public class Goods implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * 批次
+	 */
+	private TradeGoods tradeGoods;
+
+	/**
+	 * 货源
+	 */
+	private Store store;
+
+	/**
+	 * 器件
+	 */
+	private Component component;
+
+	/**
+	 * 物料
+	 */
+	private Products products;
+
+	public Goods() {
+	}
+
+	public Goods(TradeGoods tradeGoods, Store store, Component component, Products products) {
+		this.tradeGoods = tradeGoods;
+		this.store = store;
+		this.component = component;
+		this.products = products;
+	}
+
+	public TradeGoods getTradeGoods() {
+		return tradeGoods;
+	}
+
+	public void setTradeGoods(TradeGoods tradeGoods) {
+		this.tradeGoods = tradeGoods;
+	}
+
+	public Store getStore() {
+		return store;
+	}
+
+	public void setStore(Store store) {
+		this.store = store;
+	}
+
+	public Component getComponent() {
+		return component;
+	}
+
+	public void setComponent(Component component) {
+		this.component = component;
+	}
+
+	public Products getProducts() {
+		return products;
+	}
+
+	public void setProducts(Products products) {
+		this.products = products;
+	}
+
+	@Override
+	public String toString() {
+		return "Goods{" +
+				"tradeGoods=" + tradeGoods +
+				", store=" + store +
+				", component=" + component +
+				", products=" + products +
+				'}';
+	}
+}

+ 120 - 0
mall-search/src/main/java/com/uas/search/model/Kind.java

@@ -0,0 +1,120 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ * 类目简要信息,只用于索引的创建与查询
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午5:38:44
+ */
+@Entity
+@Table(name = "product$kind")
+public class Kind implements Serializable {
+
+	/**
+	 * 序列号
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "ki_id")
+	private Long id;
+
+	/**
+	 * 类目名称
+	 */
+	@Column(name = "ki_name")
+	private String nameCn;
+
+	/**
+	 * 类目的层级,从1开始,1、2、3
+	 */
+	@Column(name = "ki_level")
+	private Short level;
+
+	/**
+	 * 是否为叶子类目 1是 0否
+	 */
+	@Column(name = "ki_isleaf")
+	private Short isLeaf;
+
+	/**
+	 * 访问量
+	 */
+	@Column(name = "ki_visit_count")
+	private Long visitCount;
+
+	/**
+	 * 搜索次数
+	 */
+	@Column(name = "ki_search_count")
+	private Long searchCount;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getNameCn() {
+		return nameCn;
+	}
+
+	public void setNameCn(String nameCn) {
+		this.nameCn = nameCn;
+	}
+
+	public Short getLevel() {
+		return level;
+	}
+
+	public void setLevel(Short level) {
+		this.level = level;
+	}
+
+	public Short getIsLeaf() {
+		return isLeaf;
+	}
+
+	public void setIsLeaf(Short isLeaf) {
+		this.isLeaf = isLeaf;
+	}
+
+	public Long getVisitCount() {
+		return visitCount;
+	}
+
+	public void setVisitCount(Long visitCount) {
+		this.visitCount = visitCount;
+	}
+
+	public Long getSearchCount() {
+		return searchCount;
+	}
+
+	public void setSearchCount(Long searchCount) {
+		this.searchCount = searchCount;
+	}
+
+	@Override
+	public String toString() {
+		return "Kind{" +
+				"id=" + id +
+				", nameCn='" + nameCn + '\'' +
+				", level=" + level +
+				", isLeaf=" + isLeaf +
+				", visitCount=" + visitCount +
+				", searchCount=" + searchCount +
+				'}';
+	}
+}

+ 176 - 0
mall-search/src/main/java/com/uas/search/model/Order.java

@@ -0,0 +1,176 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+
+/**
+ * 商城销售订单
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:09:53
+ */
+@Entity(name = "trade.Order")
+@Table(name = "trade$order")
+public class Order implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 订单号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "or_id", unique = true)
+	private String code;
+
+	/**
+	 * 买方uu
+	 * 
+	 * @Tip 这里因为平台作为中间商,下达订单都看做给平台下达订单
+	 */
+	@Column(name = "or_buyeruu")
+	private Long buyeruu;
+
+	/**
+	 * 买方姓名
+	 */
+	@Column(name = "or_buyername")
+	private String buyername;
+
+	/**
+	 * 买方企业
+	 * 
+	 * @Tip 这里因为平台作为中间商,下达订单都看做给平台下达订单
+	 */
+	@OneToOne
+	@JoinColumn(name = "or_buyerenuu", updatable = false, insertable = false)
+	private Enterprise buyerEnterprise;
+
+	/**
+	 * 卖方企业
+	 * 
+	 * @Tip 这里因为平台作为中间商,下达订单都看做给平台下达订单
+	 */
+	@OneToOne
+	@JoinColumn(name = "or_sellerenuu", updatable = false, insertable = false)
+	private Enterprise sellerEnterprise;
+
+	/**
+	 * 订单生成时间
+	 */
+	@Column(name = "or_creattime")
+	private Date createtime;
+
+	/**
+	 * 订单状态(1->2->3->4->5->6->7->8->9)
+	 * 
+	 * @Tip 必须严格按照顺序流转 1、TOBECONFIRMED(501, "待确认"), 2、TOBEPAID(503, "待付款"),
+	 *      3、PAID(505, "已付款"), 4、TOBESHIPPED(406, "待出货"), 5、SHIPPINGIN(403,
+	 *      "出货中"), 6、SHIPPED(407, "已出货"), 7、SENDING(408, "发货中"), 8、INBOUND(404,
+	 *      "待收货") 9、RECEIVED(405, "已收货"), 10、UNAVAILABLE(602, "无效的");
+	 */
+	@Column(name = "or_status")
+	private Integer status;
+
+	/**
+	 * 订单明细
+	 */
+	@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER)
+	@JoinColumn(name = "order_id", updatable = false, insertable = false)
+	@OrderBy("detno")
+	private Set<OrderDetail> details;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Long getBuyeruu() {
+		return buyeruu;
+	}
+
+	public void setBuyeruu(Long buyeruu) {
+		this.buyeruu = buyeruu;
+	}
+
+	public String getBuyername() {
+		return buyername;
+	}
+
+	public void setBuyername(String buyername) {
+		this.buyername = buyername;
+	}
+
+	public Enterprise getBuyerEnterprise() {
+		return buyerEnterprise;
+	}
+
+	public void setBuyerEnterprise(Enterprise buyerEnterprise) {
+		this.buyerEnterprise = buyerEnterprise;
+	}
+
+	public Enterprise getSellerEnterprise() {
+		return sellerEnterprise;
+	}
+
+	public void setSellerEnterprise(Enterprise sellerEnterprise) {
+		this.sellerEnterprise = sellerEnterprise;
+	}
+
+	public Date getCreatetime() {
+		return createtime;
+	}
+
+	public void setCreatetime(Date createtime) {
+		this.createtime = createtime;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	public Set<OrderDetail> getDetails() {
+		return details;
+	}
+
+	public void setDetails(Set<OrderDetail> details) {
+		this.details = details;
+	}
+
+	@Override
+	public String toString() {
+		return "Order [id=" + id + ", code=" + code + ", buyeruu=" + buyeruu + ", buyername=" + buyername
+				+ ", buyerEnterprise=" + buyerEnterprise + ", sellerEnterprise=" + sellerEnterprise + ", createtime="
+				+ createtime + ", status=" + status + ", details=" + details + "]";
+	}
+
+}

+ 119 - 0
mall-search/src/main/java/com/uas/search/model/OrderDetail.java

@@ -0,0 +1,119 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 商城销售订单明细
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:19:23
+ */
+@Entity
+@Table(name = "trade$order_detail")
+public class OrderDetail {
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 明细序号
+	 */
+	@Column(name = "detno")
+	private Short detno;
+
+	/**
+	 * 订单明细编号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "detail_id", unique = true)
+	private String code;
+
+	/**
+	 * 原厂型号
+	 */
+	@Column(name = "cmp_code")
+	private String cmpCode;
+
+	/**
+	 * 器件所属类目
+	 */
+	@Column(name = "ki_name")
+	private String kiName;
+
+	/**
+	 * 器件所属品牌
+	 */
+	@Column(name = "br_name_cn")
+	private String brName;
+
+	/**
+	 * 订单明细状态
+	 */
+	@Column(name = "detail_status")
+	private Integer status;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getCmpCode() {
+		return cmpCode;
+	}
+
+	public void setCmpCode(String cmpCode) {
+		this.cmpCode = cmpCode;
+	}
+
+	public String getKiName() {
+		return kiName;
+	}
+
+	public void setKiName(String kiName) {
+		this.kiName = kiName;
+	}
+
+	public String getBrName() {
+		return brName;
+	}
+
+	public void setBrName(String brName) {
+		this.brName = brName;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	@Override
+	public String toString() {
+		return "OrderDetail [id=" + id + ", detno=" + detno + ", code=" + code + ", cmpCode=" + cmpCode
+				+ ", kiName=" + kiName + ", brName=" + brName + ", status=" + status + "]";
+	}
+}

+ 156 - 0
mall-search/src/main/java/com/uas/search/model/OrderInvoice.java

@@ -0,0 +1,156 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
+import javax.persistence.OneToOne;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+
+/**
+ * 商城销售订单的发货单
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:05:02
+ */
+@Entity
+@Table(name = "trade$invoice_fmor")
+public class OrderInvoice implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 发货单号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "in_id", unique = true)
+	private String code;
+
+	/**
+	 * 买方uu
+	 * 
+	 * @Tip 这里因为平台作为中间商,所以发货单都由平台下达
+	 */
+	@Column(name = "in_buyeruu")
+	private Long buyeruu;
+
+	/**
+	 * 买方姓名
+	 */
+	@Column(name = "in_buyername")
+	private String buyername;
+
+	/**
+	 * 买方企业
+	 * 
+	 * @Tip 这里因为平台作为中间商,下达订单都看做给平台下达订单
+	 */
+	@OneToOne
+	@JoinColumn(name = "in_buyerenuu", updatable = false, insertable = false)
+	private Enterprise buyerEnterprise;
+
+	/**
+	 * 单生成时间
+	 */
+	@Column(name = "in_creattime")
+	private Date createtime;
+
+	/**
+	 * 订单状态(1->2)
+	 * 
+	 * @Tip 必须严格按照顺序流转 1、INBOUND(404, "待收货") 2、RECEIVED(405, "已收货"),
+	 */
+	@Column(name = "in_status")
+	private Integer status;
+
+	/**
+	 * 发货单明细
+	 */
+	@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER)
+	@JoinColumn(name = "invoice_id", updatable = false, insertable = false)
+	@OrderBy("detno")
+	private Set<OrderInvoiceDetail> details;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Long getBuyeruu() {
+		return buyeruu;
+	}
+
+	public void setBuyeruu(Long buyeruu) {
+		this.buyeruu = buyeruu;
+	}
+
+	public String getBuyername() {
+		return buyername;
+	}
+
+	public void setBuyername(String buyername) {
+		this.buyername = buyername;
+	}
+
+	public Date getCreatetime() {
+		return createtime;
+	}
+
+	public void setCreatetime(Date createtime) {
+		this.createtime = createtime;
+	}
+
+	public Enterprise getBuyerEnterprise() {
+		return buyerEnterprise;
+	}
+
+	public void setBuyerEnterprise(Enterprise buyerEnterprise) {
+		this.buyerEnterprise = buyerEnterprise;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	public Set<OrderInvoiceDetail> getDetails() {
+		return details;
+	}
+
+	public void setDetails(Set<OrderInvoiceDetail> details) {
+		this.details = details;
+	}
+
+	@Override
+	public String toString() {
+		return "OrderInvoice [id=" + id + ", code=" + code + ", buyeruu=" + buyeruu + ", buyername="
+				+ buyername + ", buyerEnterprise=" + buyerEnterprise + ", createtime=" + createtime + ", status="
+				+ status + ", details=" + details + "]";
+	}
+
+}

+ 119 - 0
mall-search/src/main/java/com/uas/search/model/OrderInvoiceDetail.java

@@ -0,0 +1,119 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 商城销售订单的发货单明细
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:29:22
+ */
+@Entity
+@Table(name = "trade$invoice_fmor_dt")
+public class OrderInvoiceDetail {
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 明细序号 detail NO.
+	 */
+	@Column(name = "detno")
+	private Short detno;
+
+	/**
+	 * 发货单明细编号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "detail_id", unique = true)
+	private String code;
+
+	/**
+	 * 原厂型号
+	 */
+	@Column(name = "cmp_code")
+	private String cmpCode;
+
+	/**
+	 * 器件所属类目
+	 */
+	@Column(name = "ki_name")
+	private String kiName;
+
+	/**
+	 * 器件所属品牌
+	 */
+	@Column(name = "br_name_cn")
+	private String brName;
+
+	/**
+	 * 发货单明细状态
+	 */
+	@Column(name = "detail_status")
+	private Integer status;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getCmpCode() {
+		return cmpCode;
+	}
+
+	public void setCmpCode(String cmpCode) {
+		this.cmpCode = cmpCode;
+	}
+
+	public String getKiName() {
+		return kiName;
+	}
+
+	public void setKiName(String kiName) {
+		this.kiName = kiName;
+	}
+
+	public String getBrName() {
+		return brName;
+	}
+
+	public void setBrName(String brName) {
+		this.brName = brName;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	@Override
+	public String toString() {
+		return "OrderInvoiceDetail [id=" + id + ", detno=" + detno + ", code=" + code + ", cmpCode=" + cmpCode
+				+ ", kiName=" + kiName + ", brName=" + brName + ", status=" + status + "]";
+	}
+}

+ 86 - 0
mall-search/src/main/java/com/uas/search/model/ParsedQueueMessage.java

@@ -0,0 +1,86 @@
+package com.uas.search.model;
+
+import java.util.Arrays;
+
+/**
+ * 对数据库队列里的消息进行解析后所得到的数据
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午8:50:13
+ */
+public class ParsedQueueMessage {
+
+	/**
+	 * 数据库中表的改动为insert类型
+	 */
+	public static final int INSERT = 1;
+
+	/**
+	 * 改动为update类型
+	 */
+	public static final int UPDATE = 2;
+
+	/**
+	 * 改动为delete类型
+	 */
+	public static final int DELETE = 3;
+
+	/**
+	 * 表改动后,解析消息时,更改的类型
+	 */
+	private int methodType;
+
+	/**
+	 * 存放解析出来的对象:kind、brand或component、order、purchase等对象
+	 */
+	private Object object;
+
+	/**
+	 * 是否为insert类型
+	 * 
+	 * @return
+	 */
+	public boolean isInsert() {
+		return methodType == INSERT;
+	}
+
+	/**
+	 * 是否为update类型
+	 * 
+	 * @return
+	 */
+	public boolean isUpdate() {
+		return methodType == UPDATE;
+	}
+
+	/**
+	 * 是否为delete类型
+	 * 
+	 * @return
+	 */
+	public boolean isDelete() {
+		return methodType == DELETE;
+	}
+
+	public int getMethodType() {
+		return methodType;
+	}
+
+	public void setMethodType(int methodType) {
+		this.methodType = methodType;
+	}
+
+	public Object getObject() {
+		return object;
+	}
+
+	public void setObject(Object object) {
+		this.object = object;
+	}
+
+	@Override
+	public String toString() {
+		return "ParsedQueueMessage [methodType=" + methodType + ", object=" + object + "]";
+	}
+
+}

+ 57 - 0
mall-search/src/main/java/com/uas/search/model/Products.java

@@ -0,0 +1,57 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ * 物料(用于非标搜索)
+ *
+ * @author sunyj
+ * @since 2017/11/24 16:46
+ */
+@Entity
+@Table(name = "products")
+public class Products implements Serializable {
+
+    /**
+     * 序列号
+     */
+    private static final long serialVersionUID = 1L;
+
+    @Id
+    @Column(name = "pr_id")
+    private Long id;
+
+    /**
+     * 型号
+     */
+    @Column(name = "pr_pcmpcode")
+    private String pcmpCode;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getPcmpCode() {
+        return pcmpCode;
+    }
+
+    public void setPcmpCode(String pcmpCode) {
+        this.pcmpCode = pcmpCode;
+    }
+
+    @Override
+    public String toString() {
+        return "Products{" +
+                "id=" + id +
+                ", pcmpCode='" + pcmpCode + '\'' +
+                '}';
+    }
+}

+ 68 - 0
mall-search/src/main/java/com/uas/search/model/Property.java

@@ -0,0 +1,68 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 标准属性
+ * 
+ * @author suntg
+ * @since 2016年3月11日下午5:02:18
+ */
+@Entity
+@Table(name = "product$property")
+public class Property implements Serializable {
+
+	/**
+	 * 序列号
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "pt_id")
+	private Long id;
+
+	/**
+	 * 属性中文名
+	 */
+	@Column(name = "pt_label")
+	private String labelCn;
+
+	/**
+	 * 属性名 英文
+	 */
+	@Column(name = "pt_label_en")
+	private String labelEn;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getLabelCn() {
+		return labelCn;
+	}
+
+	public void setLabelCn(String labelCn) {
+		this.labelCn = labelCn;
+	}
+
+	public String getLabelEn() {
+		return labelEn;
+	}
+
+	public void setLabelEn(String labelEn) {
+		this.labelEn = labelEn;
+	}
+
+}

+ 120 - 0
mall-search/src/main/java/com/uas/search/model/PropertyValue.java

@@ -0,0 +1,120 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+
+/**
+ * 器件对应的属性的值
+ * 
+ * @author suntg
+ * @since 2016年3月11日下午2:24:29
+ */
+@Entity
+@Table(name = "product$propertyvalue")
+public class PropertyValue implements Serializable {
+
+	/**
+	 * 序列号
+	 */
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "pv_id")
+	private Long id;
+
+	/**
+	 * 器件Id TODO 仅用于合并导出的器件和属性数据,jpa中不需要,后期可能会删除
+	 */
+	@Column(name = "pv_componentid")
+	private Long componentid;
+
+	/**
+	 * 属性Id
+	 */
+	@Column(name = "pv_propertyid")
+	private Long propertyid;
+
+	/**
+	 * 属性
+	 */
+	// TODO 索引未用到,确认无用后删除
+	@OneToOne(cascade = { CascadeType.REFRESH })
+	@JoinColumn(name = "pv_propertyid", insertable = false, updatable = false)
+	private Property property;
+
+	/**
+	 * 排序 TODO jpa使用,之后换为jdbc后,需要删除
+	 */
+	@Column(name = "pv_detno")
+	private Short detno;
+
+	/**
+	 * 属性的值
+	 */
+	@Column(name = "pv_value")
+	private String value;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Long getComponentid() {
+		return componentid;
+	}
+
+	public void setComponentid(Long componentid) {
+		this.componentid = componentid;
+	}
+
+	public Property getProperty() {
+		return property;
+	}
+
+	public void setProperty(Property property) {
+		this.property = property;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getValue() {
+		return value;
+	}
+
+	public void setValue(String value) {
+		this.value = value;
+	}
+
+	public Long getPropertyid() {
+		return propertyid;
+	}
+
+	public void setPropertyid(Long propertyid) {
+		this.propertyid = propertyid;
+	}
+
+	@Override
+	public String toString() {
+		return "PropertyValue [propertyid=" + propertyid + ", value=" + value + "]";
+	}
+
+}

+ 138 - 0
mall-search/src/main/java/com/uas/search/model/Purchase.java

@@ -0,0 +1,138 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+
+/**
+ * 商城采购订单
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:24:00
+ */
+@Entity(name = "trade.Purchase")
+@Table(name = "trade$purchase")
+public class Purchase implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 采购单号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "pu_id", unique = true)
+	private String code;
+
+	/**
+	 * 企业卖方uu
+	 * 
+	 * @Tip 这里因为平台作为中间商,所以采购单都由平台下达
+	 */
+	@Column(name = "pu_sellerenuu")
+	private Long sellerenuu;
+
+	/**
+	 * 卖方企业名称
+	 */
+	@Column(name = "pu_sellername")
+	private String sellerenname;
+
+	/**
+	 * 单生成时间
+	 */
+	@Column(name = "pu_createtime")
+	private Date createtime;
+
+	/**
+	 * 订单状态(1->2->3)
+	 * 
+	 * @Tip 必须严格按照顺序流转 1、TOBESHIPPED(406, "待出货"), 2、INBOUND(404, "待收货"),
+	 *      3、RECEIVED(405, "已收货"), 4、TOBEPAID(503, "待付款"), 5、PAID(504, "已付款"),
+	 */
+	@Column(name = "pu_status")
+	private Integer status;
+
+	/**
+	 * 采购单明细
+	 */
+	@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER)
+	@JoinColumn(name = "purchase_id", updatable = false, insertable = false)
+	@OrderBy("detno")
+	private Set<PurchaseDetail> details;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Long getSellerenuu() {
+		return sellerenuu;
+	}
+
+	public void setSellerenuu(Long sellerenuu) {
+		this.sellerenuu = sellerenuu;
+	}
+
+	public String getSellerenname() {
+		return sellerenname;
+	}
+
+	public void setSellerenname(String sellerenname) {
+		this.sellerenname = sellerenname;
+	}
+
+	public Date getCreatetime() {
+		return createtime;
+	}
+
+	public void setCreatetime(Date createtime) {
+		this.createtime = createtime;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	public Set<PurchaseDetail> getDetails() {
+		return details;
+	}
+
+	public void setDetails(Set<PurchaseDetail> details) {
+		this.details = details;
+	}
+
+	@Override
+	public String toString() {
+		return "Purchase [id=" + id + ", code=" + code + ", sellerenuu=" + sellerenuu + ", sellerenname="
+				+ sellerenname + ", createtime=" + createtime + ", status=" + status + ", details=" + details + "]";
+	}
+
+}

+ 119 - 0
mall-search/src/main/java/com/uas/search/model/PurchaseDetail.java

@@ -0,0 +1,119 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 商城采购订单明细
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:27:53
+ */
+@Entity
+@Table(name = "trade$purchase_detail")
+public class PurchaseDetail {
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 明细序号 detailNO
+	 */
+	@Column(name = "detno")
+	private Short detno;
+
+	/**
+	 * 采购单明细编号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "detail_id", unique = true)
+	private String code;
+
+	/**
+	 * 原厂型号
+	 */
+	@Column(name = "cmp_code")
+	private String cmpCode;
+
+	/**
+	 * 器件所属类目
+	 */
+	@Column(name = "ki_name")
+	private String kiName;
+
+	/**
+	 * 器件所属品牌
+	 */
+	@Column(name = "br_name_cn")
+	private String brName;
+
+	/**
+	 * 采购单明细状态
+	 */
+	@Column(name = "detail_status")
+	private Integer status;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getCmpCode() {
+		return cmpCode;
+	}
+
+	public void setCmpCode(String cmpCode) {
+		this.cmpCode = cmpCode;
+	}
+
+	public String getKiName() {
+		return kiName;
+	}
+
+	public void setKiName(String kiName) {
+		this.kiName = kiName;
+	}
+
+	public String getBrName() {
+		return brName;
+	}
+
+	public void setBrName(String brName) {
+		this.brName = brName;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	@Override
+	public String toString() {
+		return "PurchaseDetail [id=" + id + ", detno=" + detno + ", code=" + code + ", cmpCode=" + cmpCode
+				+ ", kiName=" + kiName + ", brName=" + brName + ", status=" + status + "]";
+	}
+}

+ 140 - 0
mall-search/src/main/java/com/uas/search/model/PurchaseInvoice.java

@@ -0,0 +1,140 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Set;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.OneToMany;
+import javax.persistence.OrderBy;
+import javax.persistence.Table;
+
+/**
+ * 商城采购订单的发货单
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:32:37
+ */
+@Entity
+@Table(name = "trade$invoice_fmpu")
+public class PurchaseInvoice implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 发货单号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "in_id", unique = true)
+	private String code;
+
+	/**
+	 * 卖方企业uu
+	 * 
+	 * @Tip 这里因为平台作为中间商,所以发货单都由平台下达
+	 */
+	@Column(name = "in_sellerenuu")
+	private Long sellerenuu;
+
+	/**
+	 * 卖方企业名称
+	 */
+	@Column(name = "in_sellername")
+	private String sellerenname;
+
+	/**
+	 * 单生成时间
+	 */
+	@Column(name = "in_creattime")
+	private Date createtime;
+
+	/**
+	 * 单据状态(1->2->3)
+	 * 
+	 * @Tip 必须严格按照顺序流转 1、TOBESHIPPED(406, "待出货"), 2、INBOUND(404, "待收货")
+	 *      3、RECEIVED(405, "已收货"), 4、TORECEIVEMONEY(506, "待收款"),
+	 *      5、MONEYRECEIVED(505, "已收款"),
+	 */
+	@Column(name = "in_status")
+	private Integer status;
+
+	/**
+	 * 发货单明细
+	 */
+	@OneToMany(cascade = { CascadeType.ALL }, fetch = FetchType.EAGER)
+	@JoinColumn(name = "invoice_id", updatable = false, insertable = false)
+	@OrderBy("detno")
+	private Set<PurchaseInvoiceDetail> details;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Long getSellerenuu() {
+		return sellerenuu;
+	}
+
+	public void setSellerenuu(Long sellerenuu) {
+		this.sellerenuu = sellerenuu;
+	}
+
+	public String getSellerenname() {
+		return sellerenname;
+	}
+
+	public void setSellerenname(String sellerenname) {
+		this.sellerenname = sellerenname;
+	}
+
+	public Date getCreatetime() {
+		return createtime;
+	}
+
+	public void setCreatetime(Date createtime) {
+		this.createtime = createtime;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	public Set<PurchaseInvoiceDetail> getDetails() {
+		return details;
+	}
+
+	public void setDetails(Set<PurchaseInvoiceDetail> details) {
+		this.details = details;
+	}
+
+	@Override
+	public String toString() {
+		return "PurchaseInvoice [id=" + id + ", code=" + code + ", sellerenuu=" + sellerenuu
+				+ ", sellerenname=" + sellerenname + ", createtime=" + createtime + ", status=" + status + ", details="
+				+ details + "]";
+	}
+
+}

+ 119 - 0
mall-search/src/main/java/com/uas/search/model/PurchaseInvoiceDetail.java

@@ -0,0 +1,119 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 商城采购订单的发货单明细
+ * 
+ * @author sunyj
+ * @since 2016年10月14日 上午10:31:06
+ */
+@Entity
+@Table(name = "trade$invoice_fmpu_dt")
+public class PurchaseInvoiceDetail {
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * 明细序号 detail NO.(保存ERP的序号)
+	 */
+	@Column(name = "detno")
+	private Short detno;
+
+	/**
+	 * 发货单明细编号,因易与id命名混淆,其他类、lucenne建索引时该字段难以区分,特以此命名
+	 */
+	@Column(name = "detail_id", unique = true)
+	private String code;
+
+	/**
+	 * 原厂型号
+	 */
+	@Column(name = "cmp_code")
+	private String cmpCode;
+
+	/**
+	 * 器件所属类目
+	 */
+	@Column(name = "ki_name")
+	private String kiName;
+
+	/**
+	 * 器件所属品牌
+	 */
+	@Column(name = "br_name_cn")
+	private String brName;
+
+	/**
+	 * 发货单明细状态
+	 */
+	@Column(name = "detail_status")
+	private Integer status;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public Short getDetno() {
+		return detno;
+	}
+
+	public void setDetno(Short detno) {
+		this.detno = detno;
+	}
+
+	public String getCmpCode() {
+		return cmpCode;
+	}
+
+	public void setCmpCode(String cmpCode) {
+		this.cmpCode = cmpCode;
+	}
+
+	public String getKiName() {
+		return kiName;
+	}
+
+	public void setKiName(String kiName) {
+		this.kiName = kiName;
+	}
+
+	public String getBrName() {
+		return brName;
+	}
+
+	public void setBrName(String brName) {
+		this.brName = brName;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	@Override
+	public String toString() {
+		return "PurchaseInvoiceDetail [id=" + id + ", detno=" + detno + ", code=" + code + ", cmpCode="
+				+ cmpCode + ", kiName=" + kiName + ", brName=" + brName + ", status=" + status + "]";
+	}
+}

+ 70 - 0
mall-search/src/main/java/com/uas/search/model/Store.java

@@ -0,0 +1,70 @@
+package com.uas.search.model;
+
+import java.io.Serializable;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+/**
+ * 店铺
+ * 
+ * @author sunyj
+ * @since 2017年7月8日 下午6:14:26
+ */
+@Entity
+@Table(name = "store$info")
+public class Store implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	/**
+	 * uuid
+	 */
+	@Column(name = "ST_UUID")
+	private String uuid;
+
+	/**
+	 * 类型
+	 */
+	@Column(name = "ST_TYPE")
+	private String type;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getUuid() {
+		return uuid;
+	}
+
+	public void setUuid(String uuid) {
+		this.uuid = uuid;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	@Override
+	public String toString() {
+		return "Store [id=" + id + ", uuid=" + uuid + ", type=" + type + "]";
+	}
+
+}

+ 179 - 0
mall-search/src/main/java/com/uas/search/model/TradeGoods.java

@@ -0,0 +1,179 @@
+package com.uas.search.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.io.Serializable;
+
+/**
+ * 批次简要信息,只用于索引的创建与查询
+ * 
+ * @author sunyj
+ * @since 2017年7月22日 下午8:06:10
+ */
+@Entity
+@Table(name = "trade$goods")
+public class TradeGoods implements Serializable {
+
+	private static final long serialVersionUID = 1L;
+
+	public static final Long[] VALID_STATUS = {601L, 602L};
+
+	/**
+	 * id
+	 */
+	@Id
+	@Column(name = "go_id")
+	private Long id;
+
+	/**
+	 * 批次的库存
+	 */
+	@Column(name = "go_reserve")
+	private Double reserve;
+
+	/**
+	 * 状态
+	 */
+	@Column(name = "go_status")
+	private Long status;
+
+	/**
+	 * 批次的人民币价格
+	 */
+	@Column(name = "go_minpricermb")
+	private Double minPriceRMB;
+
+	/**
+	 * 批次的美元价格
+	 */
+	@Column(name = "go_minpriceusd")
+	private Double minPriceUSD;
+
+	/**
+	 * 货币
+	 */
+	@Column(name = "cr_name")
+	private String crName;
+
+	/**
+	 * 货源uuid
+	 */
+	@Column(name = "go_storeid")
+	private String storeId;
+
+	/**
+	 * 器件的uuid
+	 */
+	@Column(name = "cmp_uuid")
+	private String cmpUuid;
+
+	/**
+	 * 物料 id
+	 */
+	@Column(name = "go_productid")
+	private Long productId;
+
+	/**
+	 * 访问量
+	 */
+	@Column(name = "go_visit_count")
+	private Long visitCount;
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public Double getReserve() {
+		return reserve;
+	}
+
+	public void setReserve(Double reserve) {
+		this.reserve = reserve;
+	}
+
+	public Long getStatus() {
+		return status;
+	}
+
+	public void setStatus(Long status) {
+		this.status = status;
+	}
+
+	public Double getMinPriceRMB() {
+		return minPriceRMB;
+	}
+
+	public void setMinPriceRMB(Double minPriceRMB) {
+		this.minPriceRMB = minPriceRMB;
+	}
+
+	public Double getMinPriceUSD() {
+		return minPriceUSD;
+	}
+
+	public void setMinPriceUSD(Double minPriceUSD) {
+		this.minPriceUSD = minPriceUSD;
+	}
+
+	public String getCrName() {
+		return crName;
+	}
+
+	public void setCrName(String crName) {
+		this.crName = crName;
+	}
+
+	public String getStoreId() {
+		return storeId;
+	}
+
+	public void setStoreId(String storeId) {
+		this.storeId = storeId;
+	}
+
+	public String getCmpUuid() {
+		return cmpUuid;
+	}
+
+	public void setCmpUuid(String cmpUuid) {
+		this.cmpUuid = cmpUuid;
+	}
+
+	public Long getProductId() {
+		return productId;
+	}
+
+	public void setProductId(Long productId) {
+		this.productId = productId;
+	}
+
+	public Long getVisitCount() {
+		return visitCount;
+	}
+
+	public void setVisitCount(Long visitCount) {
+		this.visitCount = visitCount;
+	}
+
+	@Override
+	public String toString() {
+		return "TradeGoods{" +
+				"id=" + id +
+				", reserve=" + reserve +
+				", status=" + status +
+				", minPriceRMB=" + minPriceRMB +
+				", minPriceUSD=" + minPriceUSD +
+				", crName='" + crName + '\'' +
+				", storeId='" + storeId + '\'' +
+				", cmpUuid='" + cmpUuid + '\'' +
+				", productId=" + productId +
+				", visitCount=" + visitCount +
+				'}';
+	}
+}

+ 17 - 0
mall-search/src/main/java/com/uas/search/schedule/Executable.java

@@ -0,0 +1,17 @@
+package com.uas.search.schedule;
+
+/**
+ * 可执行的,用于执行定时任务
+ * 
+ * @author sunyj
+ * @since 2016年12月19日 上午11:31:39
+ */
+public interface Executable {
+
+	/**
+	 * 执行定时任务
+	 * 
+	 * @return 任务执行结果
+	 */
+	public String execute();
+}

+ 156 - 0
mall-search/src/main/java/com/uas/search/schedule/TaskInformation.java

@@ -0,0 +1,156 @@
+package com.uas.search.schedule;
+
+import com.uas.search.util.NumberGenerator;
+
+import java.util.Objects;
+
+/**
+ * 定时任务信息
+ * 
+ * @author sunyj
+ * @since 2016年12月19日 上午10:21:28
+ */
+public class TaskInformation {
+
+	/**
+	 * 任务 code
+	 */
+	private String code;
+
+	/**
+	 * 任务标题
+	 */
+	private String title;
+
+	/**
+	 * 所执行的任务
+	 */
+	private Executable command;
+
+	/**
+	 * 第一次执行的延迟时间间隔(毫秒)
+	 */
+	private long initialDelay;
+
+	/**
+	 * 两次任务之间的等待时间间隔(毫秒)
+	 */
+	private long interval;
+
+	private ScheduleType scheduleType;
+
+	public TaskInformation() {
+		super();
+	}
+
+	public TaskInformation(String title, Executable command, long initialDelay, long interval, ScheduleType scheduleType) {
+		init();
+		this.title = title;
+		this.command = command;
+		this.initialDelay = initialDelay;
+		this.interval = interval;
+		this.scheduleType = scheduleType;
+	}
+
+	public void init(){
+		code = NumberGenerator.generateId();
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public void setCode(String code) {
+		this.code = code;
+	}
+
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	public Executable getCommand() {
+		return command;
+	}
+
+	public void setCommand(Executable command) {
+		this.command = command;
+	}
+
+	public long getInitialDelay() {
+		return initialDelay;
+	}
+
+	public void setInitialDelay(long initialDelay) {
+		this.initialDelay = initialDelay;
+	}
+
+	public long getInterval() {
+		return interval;
+	}
+
+	public void setInterval(long interval) {
+		this.interval = interval;
+	}
+
+	public ScheduleType getScheduleType() {
+		return scheduleType;
+	}
+
+	public void setScheduleType(ScheduleType scheduleType) {
+		this.scheduleType = scheduleType;
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
+		}
+		if (obj == null || getClass() != obj.getClass() || !(obj instanceof TaskInformation)) {
+			return false;
+		}
+		TaskInformation other = (TaskInformation) obj;
+		// command不好比较,不进行比较,也不比较 code
+		return Objects.equals(title, other.getTitle()) && initialDelay == other.getInitialDelay()
+				&& interval == other.getInterval() && scheduleType == other.getScheduleType();
+	}
+
+	@Override
+	public String toString() {
+		return "TaskInformation [code=" + code + ", title=" + title + ", initialDelay=" + initialDelay + ", interval=" + interval + ", scheduleType=" + scheduleType + "]";
+	}
+
+	/**
+	 * 定时的间隔类型
+	 */
+	public enum ScheduleType {
+		/**
+		 * Creates and executes a periodic action that becomes enabled first
+		 * after the given initial delay, and subsequently with the given
+		 * period; that is executions will commence after
+		 * <tt>initialDelay</tt> then <tt>initialDelay+period</tt>, then
+		 * <tt>initialDelay + 2 * period</tt>, and so on.
+		 * If any execution of the task
+		 * encounters an exception, subsequent executions are suppressed.
+		 * Otherwise, the task will only terminate via cancellation or
+		 * termination of the executor.  If any execution of this task
+		 * takes longer than its period, then subsequent executions
+		 * may start late, but will not concurrently execute.
+		 */
+		FixedRate,
+
+		/**
+		 * Creates and executes a periodic action that becomes enabled first
+		 * after the given initial delay, and subsequently with the
+		 * given delay between the termination of one execution and the
+		 * commencement of the next.  If any execution of the task
+		 * encounters an exception, subsequent executions are suppressed.
+		 * Otherwise, the task will only terminate via cancellation or
+		 * termination of the executor.
+		 */
+		FixedDelay
+	}
+}

+ 74 - 0
mall-search/src/main/java/com/uas/search/schedule/TaskLog.java

@@ -0,0 +1,74 @@
+package com.uas.search.schedule;
+
+import com.alibaba.fastjson.JSON;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * 定时任务的执行日志
+ * 
+ * @author sunyj
+ * @since 2016年12月19日 上午10:25:50
+ */
+public class TaskLog {
+
+	/**
+	 * 定时任务的信息
+	 */
+	private TaskInformation taskInformation;
+
+	/**
+	 * 任务执行时间
+	 */
+	private Date executeTime;
+
+	/**
+	 * 任务执行结果
+	 */
+	private String result;
+
+	public TaskLog(TaskInformation taskInformation, Date executeTime, String result) {
+		super();
+		this.taskInformation = taskInformation;
+		this.executeTime = executeTime;
+		this.result = result;
+	}
+
+	public TaskInformation getTaskInformation() {
+		return taskInformation;
+	}
+
+	public void setTaskInformation(TaskInformation taskInformation) {
+		this.taskInformation = taskInformation;
+	}
+
+	public Date getExecuteTime() {
+		return executeTime;
+	}
+
+	public void setExecuteTime(Date executeTime) {
+		this.executeTime = executeTime;
+	}
+
+	public String getResult() {
+		return result;
+	}
+
+	public void setResult(String result) {
+		this.result = result;
+	}
+
+	public String toJSONString() {
+		return "{\"taskInformation\": " + JSON.toJSONString(taskInformation) + ", \"executeTime\": \""
+				+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(executeTime) + "\", \"result\": \"" + result
+				+ "\"}";
+	}
+
+	@Override
+	public String toString() {
+		return "TaskLog [taskInformation=" + taskInformation + ", executeTime=" + executeTime + ", result=" + result
+				+ "]";
+	}
+
+}

+ 65 - 0
mall-search/src/main/java/com/uas/search/schedule/TaskService.java

@@ -0,0 +1,65 @@
+package com.uas.search.schedule;
+
+import com.uas.search.annotation.NotEmpty;
+
+import java.util.List;
+
+/**
+ * 管理定时任务
+ * 
+ * @author sunyj
+ * @since 2016年12月19日 上午10:52:29
+ */
+public interface TaskService {
+	/**
+	 * 建立定时任务
+	 * 
+	 * @param taskInformation
+	 *            定时任务的信息
+	 */
+	public void newTask(TaskInformation taskInformation);
+
+	/**
+	 * 根据指定的 code,移除定时任务
+	 *
+	 * @param code 任务的 code
+	 */
+	void remove(@NotEmpty("code") String code);
+
+	/**
+	 * 是否存在定时任务
+	 *
+	 * @param code 任务的 code
+	 * @return true 则存在
+	 */
+	boolean exist(@NotEmpty("code") String code);
+
+	/**
+	 * 获取定时任务信息
+	 * 
+	 * @return
+	 */
+	public List<TaskInformation> allTaskInformations();
+
+	/**
+	 * 开启定时任务
+	 * 
+	 * @return 返回的结果
+	 */
+	public String start();
+
+	/**
+	 * 关闭定时任务
+	 * 
+	 * @return 返回的结果
+	 */
+	public String stop();
+
+	/**
+	 * 定时任务是否停止
+	 * 
+	 * @return
+	 */
+	public boolean isStopped();
+
+}

+ 210 - 0
mall-search/src/main/java/com/uas/search/schedule/TaskServiceImpl.java

@@ -0,0 +1,210 @@
+package com.uas.search.schedule;
+
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.util.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.StringUtils;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 管理定时任务
+ * 
+ * @author sunyj
+ * @since 2016年12月19日 上午10:54:43
+ */
+@Service
+public class TaskServiceImpl implements TaskService {
+
+	/**
+	 * 存储定时任务信息
+	 */
+	private List<TaskInformation> taskInformations = new ArrayList<>();
+
+	/**
+	 * 定时任务调度
+	 */
+	private ScheduledExecutorService scheduledExecutorService;
+
+	/**
+	 * 任务执行日志文件的路径
+	 */
+	private static final String LOG_DIR = System.getProperty("java.io.tmpdir");
+
+	private Logger logger = LoggerFactory.getLogger(TaskServiceImpl.class);
+
+	@Override
+	public void newTask(TaskInformation taskInformation) {
+		if (taskInformation == null || StringUtils.isEmpty(taskInformation.getTitle())
+				|| taskInformation.getCommand() == null) {
+			throw new IllegalArgumentException("定时任务的 title 和 command 不可为空");
+		}
+		if (contain(taskInformation)) {
+			throw new IllegalStateException("任务已存在:" + taskInformation);
+		}
+		taskInformations.add(taskInformation);
+	}
+
+	@Override
+	public void remove(@NotEmpty("code") String code) {
+		for (TaskInformation d : taskInformations) {
+			if (Objects.equals(code, d.getCode())) {
+				taskInformations.remove(d);
+				return;
+			}
+		}
+		throw new IllegalArgumentException("定时任务不存在:" + code);
+	}
+
+	@Override
+	public boolean exist(@NotEmpty("code") String code) {
+		for (TaskInformation d : taskInformations) {
+			if (Objects.equals(code, d.getCode())) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 判断定时任务是否已经存在
+	 * 
+	 * @param taskInformation
+	 * @return
+	 */
+	private boolean contain(TaskInformation taskInformation) {
+		if (CollectionUtils.isEmpty(taskInformations)) {
+			return false;
+		}
+		for (TaskInformation d : taskInformations) {
+			if (taskInformation.equals(d)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 保存日志到本地文件
+	 * 
+	 * @param log
+	 */
+	private void saveLog(TaskLog log) {
+		if (log == null) {
+			throw new NullPointerException();
+		}
+		FileWriter fileWriter = null;
+		try {
+			String logFilePath = (LOG_DIR.endsWith(File.separator) ? LOG_DIR : LOG_DIR + "/") + "mall-search-mysql-task-log.log";
+			fileWriter = new FileWriter(logFilePath, true);
+			fileWriter.write(log.toJSONString() + "\n");
+			fileWriter.flush();
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			if (fileWriter != null) {
+				try {
+					fileWriter.close();
+				} catch (IOException e) {
+					logger.error("", e);
+				}
+			}
+		}
+	}
+
+	@Override
+	public List<TaskInformation> allTaskInformations() {
+		return taskInformations;
+	}
+
+	@Override
+	public String start() {
+		String message = "";
+		if (!isStopped()) {
+			message = "已存在运行的定时任务";
+			logger.error(message);
+			return message;
+		}
+		if (!CollectionUtils.isEmpty(taskInformations)) {
+			// 线程数为1,以免多个创建索引的任务互相影响,导致索引创建失败
+			scheduledExecutorService = Executors.newScheduledThreadPool(1);
+			for (TaskInformation taskInformation : taskInformations) {
+				logger.info("New task: " + taskInformation);
+				switch (taskInformation.getScheduleType()){
+                    case FixedRate:
+                        scheduledExecutorService.scheduleAtFixedRate(getCommand(taskInformation),
+                                taskInformation.getInitialDelay(), taskInformation.getInterval(), TimeUnit.MILLISECONDS);
+                        break;
+                    case FixedDelay:
+                        scheduledExecutorService.scheduleWithFixedDelay(getCommand(taskInformation),
+                                taskInformation.getInitialDelay(), taskInformation.getInterval(), TimeUnit.MILLISECONDS);
+                        break;
+                }
+			}
+			message = "已开启定时任务:" + taskInformations;
+			logger.info(message + "\n");
+			return message;
+		} else {
+			message = "定时任务为空";
+			logger.error(message + "\n");
+			return message;
+		}
+	}
+
+	/**
+	 * 获取任务需执行的命令
+	 * 
+	 * @param taskInformation
+	 * @return
+	 */
+	private Runnable getCommand(final TaskInformation taskInformation) {
+		return new Runnable() {
+			@Override
+			public void run() {
+				try {
+					Executable command = taskInformation.getCommand();
+					String result = command.execute();
+					saveLog(new TaskLog(taskInformation, new Date(), result));
+				} catch (Throwable e) {
+					saveLog(new TaskLog(taskInformation, new Date(),
+							"定时任务出错" + ExceptionUtils.getDetailedMessage(e)));
+				}
+			}
+		};
+	}
+
+	@Override
+	public String stop() {
+		String message = "";
+		if (isStopped()) {
+			message = "定时任务已经停止或者未开启过";
+			logger.error(message);
+			return message;
+		}
+		logger.info("Remove old tasks...");
+		scheduledExecutorService.shutdownNow();
+		message = "已关闭定时任务";
+		logger.info(message + "\n");
+		return message;
+	}
+
+	@Override
+	public boolean isStopped() {
+		if (scheduledExecutorService == null) {
+			return true;
+		}
+		return scheduledExecutorService.isShutdown() || scheduledExecutorService.isTerminated();
+	}
+}

+ 99 - 0
mall-search/src/main/java/com/uas/search/service/IndexService.java

@@ -0,0 +1,99 @@
+package com.uas.search.service;
+
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.model.ParsedQueueMessage;
+import com.uas.search.support.DownloadHelper;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 索引处理
+ *
+ * @author sunyj
+ * @since 2016年8月5日 下午2:33:58
+ */
+public interface IndexService {
+
+    /**
+     * 创建指定表的索引
+     *
+     * @param tableNames         可为空,指定的表,默认创建所有表的索引
+     * @param componentFromFiles 可为空,器件索引数据是否来自文件,默认来自文件
+     * @return 创建的索引花费总时间 ms
+     */
+    public Long createIndexes(List<String> tableNames, Boolean componentFromFiles);
+
+    /**
+     * 多线程下载器件的数据至本地文件中,以供建索引用
+     *
+     * @param threads        线程数量,默认为 1
+     * @param startFileIndex 开始的文件,默认为 1
+     * @param endFileIndex   结束的文件,默认为 1024 * 1024 * 1024
+     * @param validateResult 下载完成后,是否对结果进行校验
+     * @return 下载的数据条数
+     */
+    public long multiDownloadComponent(Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult);
+
+    /**
+     * 多线程下载批次的数据至本地文件中,以供建索引用
+     *
+     * @param threads        线程数量,默认为 1
+     * @param startFileIndex 开始的文件,默认为 1
+     * @param endFileIndex   结束的文件,默认为 1024 * 1024 * 1024
+     * @param validateResult 下载完成后,是否对结果进行校验
+     * @return 下载的数据条数
+     */
+    public long multiDownloadGoods(Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult);
+
+    /**
+     * 将新对象添加在lucene索引中
+     *
+     * @param obj 将添加的对象
+     * @return 添加的对象
+     */
+    public Object save(Object obj);
+
+    /**
+     * 将新对象添加在lucene索引中
+     *
+     * @param obj 将更新的对象
+     * @return 更新的对象
+     */
+    public Object update(Object obj);
+
+    /**
+     * 将对象从lucene索引中删除
+     *
+     * @param obj 将删除的对象
+     * @return 删除的对象
+     */
+    public Object delete(Object obj);
+
+    /**
+     * 根据自队列消息解析出来的对象,对lucene索引进行添加、更新或删除操作
+     *
+     * @param parsedQueueMessage 自队列消息解析出来的对象
+     * @return 维护的对象
+     */
+    public List<Object> maintainIndexes(@NotEmpty("parsedQueueMessage") ParsedQueueMessage parsedQueueMessage);
+
+    /**
+     * 根据实体类型名维护出问题的表的索引
+     *
+     * @param tableName  需维护的实体类型名
+     * @param dataId     需维护的id
+     * @param methodType 需对索引做何种更改
+     * @param data       数据
+     * @return 维护的对象
+     */
+    public List<Object> maintainIndexes(@NotEmpty("tableName") String tableName, @NotEmpty("dataId") Long dataId, @NotEmpty("methodType") String methodType, String data);
+
+    /**
+     * 根据(录入的词典)新词更新索引
+     *
+     * @param newWords 词典新词
+     * @return 更新的对象
+     */
+    Map<String, List<Object>> updateIndexByNewWords(List<String> newWords);
+}

+ 208 - 0
mall-search/src/main/java/com/uas/search/service/OrderSearchService.java

@@ -0,0 +1,208 @@
+package com.uas.search.service;
+
+import com.uas.search.constant.model.BaseOrder;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.OrderInvoice;
+import com.uas.search.model.Order;
+import com.uas.search.model.PurchaseInvoice;
+import com.uas.search.model.Purchase;
+
+/**
+ * 商城订单搜索服务
+ * 
+ * @author suntg
+ * @since 2016年10月11日上午11:36:27
+ */
+public interface OrderSearchService {
+
+	/**
+	 * 订单的类型名称
+	 * 
+	 * @author suntg
+	 * @since 2016年10月11日上午11:36:44
+	 */
+	public enum Table_name {
+		/**
+		 * 商城销售订单
+		 */
+		ORDER("order"),
+		/**
+		 * 商城采购订单
+		 */
+		PURCHASE("purchase"),
+		/**
+		 * 商城销售订单的发货单
+		 */
+		ORDER_INVOICE("order_invoice"),
+		/**
+		 * 商城采购订单的发货单
+		 */
+		PURCHASE_INVOICE("purchase_invocie");
+
+		private Table_name(String phrase) {
+			this.phrase = phrase;
+		}
+
+		private String phrase;
+
+		public String value() {
+			return phrase;
+		}
+
+		@Override
+		public String toString() {
+			return phrase;
+		}
+	}
+
+	/**
+	 * 根据订单关键词、订单类型、额外条件搜索订单id信息
+	 * 
+	 * @param keyword
+	 *            模糊搜索关键词,可以是:订单主表单号、订单买方个人姓名、订单买方企业名称、订单卖方企业名称、订单明细中的单号、器件型号、
+	 *            器件品牌名称、 器件类目名称
+	 * @param tableName
+	 *            订单类型名称
+	 * @param params
+	 *            翻页、过滤等信息
+	 *            <p>
+	 *            关于过滤,通过键值对指定过滤条件,键为
+	 *            {@link com.uas.search.constant.model.PageParams.FilterField}
+	 *            ,值的类型由键决定:
+	 *            </p>
+	 * 
+	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *            "Fields and types">
+	 *            <tr>
+	 *            <th>Field</th>
+	 *            <th>Type</th>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>ORDER_STATUS</td>
+	 *            <td>List(Integer),多个状态之间取"或"的关系</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>ORDER_BUYER_UU</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>ORDER_BUYER_EN_UU</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>ORDER_SELLER_EN_UU</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>ORDER_MIN_TIME</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>ORDER_MAX_TIME</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            </table>
+	 * 
+	 * @return 符合条件的单据。<br>
+	 *         如果关键词命中主表中的信息,结果有主表信息不带从表;<br>
+	 *         如果关键词没命中主表信息,命中了从表中的信息,结果有主表带被命中的从表信息;<br>
+	 *         如果都没有命中,则不命中。
+	 */
+	public SPage<BaseOrder> getOrderIdsWithDetail(String keyword, Table_name tableName, PageParams params)
+			throws SearchException;
+
+	/**
+	 * 根据关键词和其他信息搜索销售单id信息
+	 * 
+	 * @param keyword
+	 *            关键词,可以是:销售单号、买方个人姓名、买方企业名称、卖方企业名称、明细中的单号、器件型号、 器件品牌名称、器件类目名称
+	 * @param params
+	 *            其他信息,可能带有翻页信息,filter中可以有:订单状态码(List
+	 *            <Integer>,多个状态之间取"或"的关系)、买方个人UU号、买方企业UU号、卖方企业UU号、订单日期范围最大值、
+	 *            订单日期最小值
+	 * @return 符合条件的单据。<br>
+	 *         如果关键词命中主表中的信息,结果有主表信息不带从表;<br>
+	 *         如果关键词没命中主表信息,命中了从表中的信息,结果有主表带被命中的从表信息;<br>
+	 *         如果都没有命中,则不命中。
+	 */
+	public SPage<BaseOrder> searchOrderIds(String keyword, PageParams params);
+
+	/**
+	 * 根据关键词和其他信息搜索销售发货单id信息
+	 * 
+	 * @param keyword
+	 *            关键词,可以是:发货单号、买方个人姓名、买方企业名称、明细中的单号、器件型号、 器件品牌名称、器件类目名称
+	 * @param params
+	 *            其他信息,可能带有翻页信息,filter中可以有:订单状态码(List
+	 *            <Integer>,多个状态之间取"或"的关系)、买方个人UU号、买方企业UU号、订单日期范围最大值、订单日期最小值
+	 * @return 符合条件的单据。<br>
+	 *         如果关键词命中主表中的信息,结果有主表信息不带从表;<br>
+	 *         如果关键词没命中主表信息,命中了从表中的信息,结果有主表带被命中的从表信息;<br>
+	 *         如果都没有命中,则不命中。
+	 */
+	public SPage<BaseOrder> searchOrderInvoiceIds(String keyword, PageParams params);
+
+	/**
+	 * 根据关键词和其他信息搜索采购单id信息
+	 * 
+	 * @param keyword
+	 *            模糊搜索关键词,可以是:采购单号、卖方企业名称、明细中的单号、器件型号、器件品牌名称、 器件类目名称
+	 * @param params
+	 *            关键词,可能带有翻页信息,filter中可以有:订单状态码(List
+	 *            <Integer>,多个状态之间取"或"的关系)、卖方企业UU号、订单日期范围最大值、订单日期最小值
+	 * @return 符合条件的单据。<br>
+	 *         如果关键词命中主表中的信息,结果有主表信息不带从表;<br>
+	 *         如果关键词没命中主表信息,命中了从表中的信息,结果有主表带被命中的从表信息;<br>
+	 *         如果都没有命中,则不命中。
+	 */
+	public SPage<BaseOrder> searchPurchaseIds(String keyword, PageParams params);
+
+	/**
+	 * 根据关键词和其他信息搜索采购发货单id信息
+	 * 
+	 * @param keyword
+	 *            关键词,可以是:发货单号、卖方企业名称、明细中的单号、器件型号、 器件品牌名称、器件类目名称
+	 * @param params
+	 *            其他信息,可能带有翻页信息,filter中可以有:订单状态码(List
+	 *            <Integer>,多个状态之间取"或"的关系)、卖方企业UU号、订单日期范围最大值、订单日期最小值
+	 * @return 符合条件的单据。<br>
+	 *         如果关键词命中主表中的信息,结果有主表信息不带从表;<br>
+	 *         如果关键词没命中主表信息,命中了从表中的信息,结果有主表带被命中的从表信息;<br>
+	 *         如果都没有命中,则不命中。
+	 */
+	public SPage<BaseOrder> searchPurchaseInvoiceIds(String keyword, PageParams params);
+
+	/**
+	 * 根据id获取索引中的销售单数据
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Order getOrder(Long id);
+
+	/**
+	 * 根据id获取索引中的销售发货单数据
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public OrderInvoice getOrderInvoice(Long id);
+
+	/**
+	 * 根据id获取索引中的采购单数据
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Purchase getPurchase(Long id);
+
+	/**
+	 * 根据id获取索引中的采购发货单数据
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public PurchaseInvoice getPurchaseInvoice(Long id);
+}

+ 429 - 0
mall-search/src/main/java/com/uas/search/service/SearchService.java

@@ -0,0 +1,429 @@
+package com.uas.search.service;
+
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.constant.model.CollectField;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.PageParams.FilterField;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.Brand;
+import com.uas.search.model.Component;
+import com.uas.search.model.Goods;
+import com.uas.search.model.Kind;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 搜索服务的接口
+ * 
+ * @author suntg
+ * @since 2016年7月29日下午4:58:45
+ */
+public interface SearchService {
+
+	/**
+	 * 根据关键字搜索产品类目id
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 * @return 符合条件的类目id
+	 * @throws SearchException
+	 */
+	public SPage<Long> getKindIds(String keyword, Integer page, Integer size) throws SearchException;
+
+	/**
+	 * 根据关键词搜索产品类目
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 * @return
+	 * @throws SearchException
+	 */
+	public SPage<Map<String, Object>> getKinds(String keyword, Integer page, Integer size) throws SearchException;
+
+	/**
+	 * 根据关键词搜索产品品牌id
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 * @return 符合条件的品牌id
+	 * @throws SearchException
+	 */
+	public SPage<Long> getBrandIds(String keyword, Integer page, Integer size) throws SearchException;
+
+	/**
+	 * 根据关键词搜索产品品牌
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 * @return 符合条件的品牌
+	 * @throws SearchException
+	 */
+	public SPage<Map<String, Object>> getBrands(String keyword, Integer page, Integer size) throws SearchException;
+
+	/**
+	 * 根据关键词搜索产品(关键词可能是器件、类目、品牌,甚至可能是类目、品牌的混合)
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param params
+	 *            翻页、过滤等信息
+	 *            <p>
+	 *            关于过滤,通过键值对指定过滤条件,键为
+	 *            {@link com.uas.search.constant.model.PageParams.FilterField}
+	 *            ,值的类型由键决定:
+	 *            </p>
+	 * 
+	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *            "Fields and types">
+	 *            <tr>
+	 *            <th>Field</th>
+	 *            <th>Type</th>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_KINDID</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_BRANDID</td>
+	 *            <td>Long</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_PROPERTIES</td>
+	 *            <td>键值对,键值分别为属性id、属性值</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_HAS_RESERVE</td>
+	 *            <td>Boolean</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_HAS_SAMPLE</td>
+	 *            <td>Boolean</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_HAS_ORIGINAL</td>
+	 *            <td>Boolean</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>COMPONENT_HAS_INACTION_STOCK</td>
+	 *            <td>Boolean</td>
+	 *            </tr>
+	 *            </table>
+	 * 
+	 * @return
+	 * @throws SearchException
+	 */
+	public Map<String, Object> getComponentIds(String keyword, PageParams params) throws SearchException;
+
+	/**
+	 * 根据产品搜索获取产品类目id的统计
+	 * 
+	 * @param keyword
+	 * @param brandId
+	 *            (可选)
+	 * @return
+	 * @throws SearchException
+	 */
+	public Set<Long> getKindIdsBySearchComponent(String keyword, String brandId) throws SearchException;
+
+	/**
+	 * 根据产品搜索获取产品类目的统计
+	 * 
+	 * @param keyword
+	 * @param brandId
+	 *            (可选)
+	 * @return
+	 * @throws SearchException
+	 */
+	public Set<Map<String, Object>> getKindsBySearchComponent(String keyword, String brandId) throws SearchException;
+
+	/**
+	 * 根据产品搜索获取产品品牌id的统计
+	 * 
+	 * @param keyword
+	 * @param kindId
+	 *            (可选)
+	 * @return
+	 * @throws SearchException
+	 */
+	public Set<Long> getBrandIdsBySearchComponent(String keyword, String kindId) throws SearchException;
+
+	/**
+	 * 根据产品搜索获取产品品牌的统计
+	 * 
+	 * @param keyword
+	 * @param kindId
+	 *            (可选)
+	 * @return
+	 * @throws SearchException
+	 */
+	public Set<Map<String, Object>> getBrandsBySearchComponent(String keyword, String kindId) throws SearchException;
+
+	/**
+	 * 根据输入获取联想词(包括器件、类目、品牌,按顺序获取,数量不足,才会获取下一个)
+	 * 
+	 * @param keyword
+	 * @param size 指定的联想词数目
+	 * @return
+	 * @throws SearchException
+	 */
+	public List<String> getSimilarKeywords(String keyword, Integer size) throws SearchException;
+
+	/**
+	 * 根据输入的原厂型号获取联想词
+	 * 
+	 * @param componentCode
+	 * @param size 指定的联想词数目
+	 * @return 包括id、uuid、code
+	 * @throws SearchException
+	 */
+	public List<Map<String, Object>> getSimilarComponents(String componentCode, Integer size) throws SearchException;
+
+	/**
+	 * 根据输入的品牌获取联想词
+	 * 
+	 * @param brandName
+	 * @param size 指定的联想词数目
+	 * @return 包括id、uuid、nameCn、nameEn
+	 * @throws SearchException
+	 */
+	public List<Map<String, Object>> getSimilarBrands(String brandName, Integer size) throws SearchException;
+
+	/**
+	 * 根据输入的类目名获取联想词
+	 * 
+	 * @param kindName
+	 * @param size 指定的联想词数目
+	 * @return 包括id、nameCn、level、isLeaf
+	 * @throws SearchException
+	 */
+	public List<Map<String, Object>> getSimilarKinds(String kindName, Integer size) throws SearchException;
+
+	/**
+	 * 根据输入的类目名获取末级类目联想词
+	 * 
+	 * @param kindName
+	 * @param size 指定的联想词数目
+	 * @return 包括id、nameCn、level、isLeaf
+	 * @throws SearchException
+	 */
+	public List<Map<String, Object>> getSimilarLeafKinds(String kindName, Integer size) throws SearchException;
+
+	/**
+	 * 根据输入的类目名和指定的类目级别获取联想词
+	 * 
+	 * @param kindName
+	 * @param level
+	 * @param size 指定的联想词数目
+	 * @return 包括id、nameCn、level、isLeaf
+	 * @throws SearchException
+	 */
+	public List<Map<String, Object>> getSimilarKindsByLevel(String kindName, Short level, Integer size) throws SearchException;
+
+	/**
+	 * 根据类目id、属性id、属性值获取联想词
+	 * 
+	 * @param kindId
+	 *            类目id
+	 * @param propertyId
+	 *            属性id
+	 * @param keyword
+	 *            (可选) 属性值(部分字符)
+	 * @param topNum
+	 *            (可选) 获取的最大数目
+	 * @return 相似的属性值,包括propertyValue
+	 * @throws SearchException
+	 */
+	public List<Map<String, String>> getSimilarPropertyValues(Long kindId, Long propertyId, String keyword, Long topNum)
+			throws SearchException;
+
+	/**
+	 * 根据关键词搜索批次(关键词可能是器件、类目、品牌)
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param pageParams
+	 *            翻页、过滤、排序等信息
+	 *            <p>
+	 *            关于过滤,通过键值对指定过滤条件,键为
+	 *            {@link com.uas.search.constant.model.PageParams.FilterField}
+	 *            ,值的类型由键决定:
+	 *            </p>
+	 * 
+	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *            "Fields and types">
+	 *            <tr>
+	 *            <th>Field</th>
+	 *            <th>Type</th>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_KINDID</td>
+	 *            <td>List(Long)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_BRANDID</td>
+	 *            <td>List(Long)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_STORE_TYPE</td>
+	 *            <td>List(String)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_CRNAME</td>
+	 *            <td>List(String)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_MINPRICERMB</td>
+	 *            <td>Double</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_MAXPRICERMB</td>
+	 *            <td>Double</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_MINPRICEUSD</td>
+	 *            <td>Double</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_MAXPRICEUSD</td>
+	 *            <td>Double</td>
+	 *            </tr>
+	 *            </table>
+	 * 
+	 * @return 器件id、批次id和分页信息
+	 */
+	public Map<String, Object> getGoodsIds(String keyword, PageParams pageParams) throws SearchException;
+
+	/**
+	 * 搜索批次时,统计指定信息
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param collectedField
+	 *            需要统计的信息
+	 * @param filters
+	 *            过滤条件,值的类型由键决定:
+	 * 
+	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *            "Fields and types">
+	 *            <tr>
+	 *            <th>Field</th>
+	 *            <th>Type</th>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_KINDID</td>
+	 *            <td>List(Long)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_BRANDID</td>
+	 *            <td>List(Long)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_STORE_TYPE</td>
+	 *            <td>List(String)</td>
+	 *            </tr>
+	 *            <tr>
+	 *            <td>GOODS_CRNAME</td>
+	 *            <td>List(String)</td>
+	 *            </tr>
+	 *            </table>
+	 * 
+	 * @return 统计的信息(由collectedField决定)
+	 * 
+	 *         <table border=1 cellpadding=5 cellspacing=0 summary=
+	 *         "Collected fields and messages">
+	 *         <tr>
+	 *         <th>Collected field</th>
+	 *         <th>Message</th>
+	 *         </tr>
+	 *         <tr>
+	 *         <td>GOODS_KIND</td>
+	 *         <td>ki_id、ki_name_cn</td>
+	 *         </tr>
+	 *         <tr>
+	 *         <td>GOODS_BRAND</td>
+	 *         <td>br_id、br_uuid、br_name_cn、br_name_en</td>
+	 *         </tr>
+	 *         <tr>
+	 *         <td>GOODS_STORE_TYPE</td>
+	 *         <td>store_type</td>
+	 *         </tr>
+	 *         <tr>
+	 *         <td>GOODS_CRNAME</td>
+	 *         <td>cr_name</td>
+	 *         </tr>
+	 *         </table>
+	 */
+	public List<Map<String, Object>> collectBySearchGoods(String keyword, CollectField collectedField,
+			Map<FilterField, Object> filters);
+
+	/**
+	 * 根据id获取类目
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Kind getKind(Long id);
+
+	/**
+	 * 根据id获取品牌
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Brand getBrand(Long id);
+
+	/**
+	 * 根据id获取器件
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Component getComponent(Long id);
+
+	/**
+	 * 根据id获取批次
+	 * 
+	 * @param id
+	 * @return
+	 */
+	public Goods getGoods(String id);
+
+	/**
+	 * 分页获取本地指定表的索引中的数据
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param keyword
+	 *            关键词,可为空,默认正则搜索空串
+	 * @param field
+	 *            搜索字段,可为空,默认取各表索引中的id字段
+	 * @param tokenized
+	 *            是否分词,可为空,默认不分词
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            分页大小
+	 * @return
+	 */
+	public SPage<Object> getObjects(String tableName, String keyword, String field, Boolean tokenized, @NotEmpty("page") Integer page, @NotEmpty("size") Integer size);
+
+}

+ 687 - 0
mall-search/src/main/java/com/uas/search/service/impl/IndexServiceImpl.java

@@ -0,0 +1,687 @@
+package com.uas.search.service.impl;
+
+import com.alibaba.fastjson.JSONException;
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.DruidDBConfiguration;
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.constant.model.PageInfo;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.dao.*;
+import com.uas.search.exception.SearchException;
+import com.uas.search.jms.JmsListener;
+import com.uas.search.jms.QueueMessageParser;
+import com.uas.search.model.*;
+import com.uas.search.service.IndexService;
+import com.uas.search.support.DownloadHelper;
+import com.uas.search.support.DownloadService;
+import com.uas.search.support.IndexSearcherManager;
+import com.uas.search.support.IndexWriterManager;
+import com.uas.search.util.ObjectToDocumentUtils;
+import com.uas.search.util.SearchUtils;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Service;
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.StringUtils;
+
+import java.io.*;
+import java.util.*;
+
+import static com.uas.search.constant.SearchConstants.COMPONENT_TABLE_NAME;
+import static com.uas.search.constant.SearchConstants.GOODS_TABLE_NAME;
+
+/**
+ * 创建索引
+ *
+ * @author sunyj
+ * @since 2016年8月5日 下午2:23:22
+ */
+@Service
+public class IndexServiceImpl implements IndexService {
+
+    @Autowired
+    private DruidDBConfiguration druidDBConfiguration;
+
+	@Autowired
+	private QueueMessageParser queueMessageParser;
+
+	@Autowired
+	private KindDao kindDao;
+
+	@Autowired
+	private BrandDao brandDao;
+
+	@Autowired
+	private ComponentDao componentDao;
+
+    @Autowired
+    private TradeGoodsDao tradeGoodsDao;
+
+	@Autowired
+	private GoodsDao goodsDao;
+
+	@Autowired
+	private OrderDao orderDao;
+
+	@Autowired
+	private OrderInvoiceDao orderInvoiceDao;
+
+	@Autowired
+	private PurchaseDao purchaseDao;
+
+	@Autowired
+	private PurchaseInvoiceDao purchaseInvoiceDao;
+
+	private IndexWriter indexWriter;
+
+	private static IndexWriterManager indexWriterManager = new IndexWriterManager();
+
+	@Autowired
+	private JmsListener jmsListener;
+
+	private IndexSearcherManager indexSearcherManager = IndexSearcherManager.getInstance();
+
+	/**
+	 * 是否正在创建索引
+	 */
+	private boolean creatingIndex = false;
+
+	/**
+	 * 从数据库获取数据时的分页大小
+	 */
+	public static final int PAGE_SIZE = 1000;
+
+	/**
+	 * 单个文件存储的最大数据数目,需是PAGE_SIZE的整数倍
+	 */
+	public static final int SINGLE_FILE_MAX_SIZE = PAGE_SIZE;
+
+	private static Logger logger = LoggerFactory.getLogger(IndexServiceImpl.class);
+
+	@Override
+	public Long createIndexes(List<String> tableNames, Boolean componentFromFiles) {
+		if (creatingIndex) {
+			throw new SearchException("已存在线程在创建索引,不可重复请求");
+		}
+		creatingIndex = true;
+		// 如果索引实时更新处于开启状态,需要关闭(以免两者同时操作索引出现问题)
+		if (jmsListener.isRunning()) {
+			logger.info("索引实时更新服务正在运行,尝试关闭索引实时更新服务...");
+			jmsListener.stop();
+		}
+
+		if (CollectionUtils.isEmpty(tableNames)) {
+			tableNames = SearchUtils.getTableNames();
+		}
+
+		try {
+			Long startTime = new Date().getTime();
+
+			for (String tableName : tableNames) {
+                try {
+                    Long start = new Date().getTime();
+                    Long size = 0L;
+                    deleteIndexes(tableName);
+                    if (tableName.equals(SearchConstants.KIND_TABLE_NAME)) {
+                        size = createKindIndexes();
+                    } else if (tableName.equals(SearchConstants.BRAND_TABLE_NAME)) {
+                        size = createBrandIndexes();
+                    } else if (tableName.equals(COMPONENT_TABLE_NAME)) {
+                        // 只有明确指定不从本地文件读取数据时,才从数据库获取数据建立索引
+                        if (componentFromFiles != null && !componentFromFiles) {
+                            size = createComponentIndexes();
+                        } else {
+                            size = createComponentIndexesFromFiles();
+                        }
+                    } else if (tableName.equals(SearchConstants.GOODS_TABLE_NAME)) {
+                        size = createGoodsIndexesFromFiles();
+                    } else if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+                        size = createOrderIndexes();
+                    } else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+                        size = createOrderInvoiceIndexes();
+                    } else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+                        size = createPurchaseIndexes();
+                    } else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+                        size = createPurchaseInvoiceIndexes();
+                    } else {
+                        logger.error("不支持创建该表索引:" + tableName);
+                        continue;
+                    }
+                    indexSearcherManager.flushCache(tableName, indexWriter, null);
+                    Long end = new Date().getTime();
+                    logger.info(String.format("创建%s索引: %s条,耗时%.2fs\n ", tableName, size, (end - start) / 1000.0));
+                }finally {
+                    indexWriterManager.release(tableName);
+                }
+            }
+
+			Long endTime = new Date().getTime();
+			logger.info(String.format("索引创建成功, 共用时间%.2fs\n", (endTime - startTime) / 1000.0));
+			return endTime - startTime;
+		} catch (Exception e) {
+			// 防止SQLRecoverableException导致应用终止
+			throw new IllegalStateException("索引创建失败", e);
+		} finally {
+			creatingIndex = false;
+		}
+	}
+
+	/**
+	 * 删除指定表的索引
+	 *
+	 * @param tableName
+	 *            表名
+	 * @throws InterruptedException
+	 * @throws IOException
+	 */
+	private void deleteIndexes(String tableName) throws InterruptedException, IOException {
+		indexWriter = indexWriterManager.get(tableName);
+		logger.info("正在清理旧索引..." + tableName);
+		indexWriter.deleteAll();
+		logger.info("旧索引清理完毕..." + tableName);
+	}
+
+	/**
+	 * 创建类目索引
+	 *
+	 * @return 写入的类目索引数
+	 * @throws IOException
+	 */
+	private Long createKindIndexes() throws IOException {
+		logger.info("正在创建类目索引...");
+		List<Kind> kinds = kindDao.findAll();
+		logger.info("发现数据:" + kinds.size() + "条");
+		return createIndexesWithObjects(kinds.toArray());
+	}
+
+	/**
+	 * 创建品牌索引
+	 *
+	 * @return 写入的品牌索引数
+	 * @throws IOException
+	 */
+	private Long createBrandIndexes() throws IOException {
+		logger.info("正在创建品牌索引...");
+		List<Brand> brands = brandDao.findAll();
+		logger.info("发现数据:" + brands.size() + "条");
+		return createIndexesWithObjects(brands.toArray());
+	}
+
+    private Long createComponentIndexesFromFiles() throws IOException {
+        logger.info("正在创建器件索引...");
+        Long size = 0L;
+        BufferedReader bufferedReader = null;
+        try {
+            // 从本地路径读取器件数据
+            String componentDataPath = SearchUtils.getDataPath(COMPONENT_TABLE_NAME);
+            File[] files = new File(componentDataPath).listFiles();
+            if (files == null || files.length == 0) {
+                logger.info("创建器件索引失败,原因:器件数据文件不存在!");
+                return 0L;
+            }
+            for (File file : files) {
+                logger.info("读取文件: " + file.getName());
+                bufferedReader = new BufferedReader(new FileReader(file));
+                String line;
+                while (!StringUtils.isEmpty(line = bufferedReader.readLine())) {
+                    Component component;
+                    try {
+                        component = JSONObject.parseObject(line, Component.class);
+                    } catch (JSONException e) {
+                        throw new IllegalArgumentException(line, e);
+                    }
+                    Document document = ObjectToDocumentUtils.toDocument(component);
+                    if (document != null) {
+                        size++;
+                        // 每创建10000条,打印一次进度
+                        if (size % 10000 == 0) {
+                            logger.info("Component indexed..................." + size);
+                        }
+                        indexWriter.addDocument(document);
+                    }
+                }
+                indexWriter.commit();
+            }
+        } catch (FileNotFoundException e) {
+            logger.error("创建器件索引失败,原因:器件数据文件不存在!");
+            return 0L;
+        }  finally {
+            if (bufferedReader != null) {
+                bufferedReader.close();
+            }
+        }
+        return size;
+    }
+
+	/**
+	 * 创建器件索引,从数据库取数据
+	 *
+	 * @return 写入的器件索引数
+	 * @throws IOException
+	 */
+	public Long createComponentIndexes() throws IOException {
+		logger.info("正在创建器件索引...");
+		Long size = 0L;
+		PageParams params = new PageParams();
+		int page = 1;
+		params.setSize(PAGE_SIZE);
+		params.setPage(page);
+
+		PageInfo info = new PageInfo(params);
+		Page<Component> pageResult = componentDao.findAll(info);
+		long totalElements = pageResult.getTotalElements();
+		logger.info("Number of components: " + totalElements);
+		// 用于记录上次提交索引时的创建进度
+		double recordProgress = 0;
+		while (totalElements > size) {
+			List<Component> components = pageResult.getContent();
+			for (Component component : components) {
+				Document document = ObjectToDocumentUtils.toDocument(component);
+				if (document != null) {
+					indexWriter.addDocument(document);
+				}
+			}
+			size += components.size();
+
+			// 器件索引的创建进度(百分比)
+			double indexProgress = size * 100.0 / totalElements;
+			logger.info(String.format("Component indexed...................%.2f%%", indexProgress));
+			// 每创建5%,提交一次,避免内存耗尽,发生OutOfMemoryError
+			if (indexProgress - recordProgress > 5) {
+				indexWriter.commit();
+				recordProgress = indexProgress;
+			}
+
+			page++;
+			params.setPage(page);
+			info = new PageInfo(params);
+			pageResult = componentDao.findAll(info);
+		}
+		indexWriter.commit();
+		return size;
+	}
+
+	private Long createGoodsIndexesFromFiles() throws IOException {
+		logger.info("正在创建批次索引...");
+		Long size = 0L;
+        BufferedReader bufferedReader=null;
+		try {
+			// 从本地路径读取批次数据
+			File[] files = new File(SearchUtils.getDataPath(SearchConstants.GOODS_TABLE_NAME)).listFiles();
+			if (files == null || files.length == 0) {
+				logger.info("创建批次索引失败,原因:批次数据文件不存在!");
+				return 0L;
+			}
+			for (File file : files) {
+				logger.info("读取文件: " + file.getName());
+				bufferedReader = new BufferedReader(new FileReader(file));
+				String line;
+				while (!StringUtils.isEmpty(line = bufferedReader.readLine())) {
+                    Goods goods;
+                    try {
+                        goods = JSONObject.parseObject(line, Goods.class);
+                    } catch (JSONException e) {
+                        throw new IllegalArgumentException(line, e);
+                    }
+                    Document document = ObjectToDocumentUtils.toDocument(goods);
+					if (document != null) {
+						size++;
+						// 每创建10000条,打印一次进度
+						if (size % 10000 == 0) {
+                            logger.info("Goods indexed..................." + size);
+						}
+						indexWriter.addDocument(document);
+					}
+				}
+				indexWriter.commit();
+                bufferedReader.close();
+			}
+		} catch (FileNotFoundException e) {
+			logger.error("创建批次索引失败,原因:批次数据文件不存在!");
+			return 0L;
+		} finally {
+            indexWriter.commit();
+            if (bufferedReader != null) {
+                bufferedReader.close();
+            }
+        }
+		return size;
+	}
+
+	private Long createOrderIndexes() {
+		logger.info("正在创建销售单索引...");
+		List<Order> orders = orderDao.findAll();
+		logger.info("发现数据:" + orders.size() + "条");
+		return createIndexesWithObjects(orders.toArray());
+	}
+
+	private Long createOrderInvoiceIndexes() {
+		logger.info("正在创建销售发货单索引...");
+		List<OrderInvoice> orderInvoices = orderInvoiceDao.findAll();
+		logger.info("发现数据:" + orderInvoices.size() + "条");
+		return createIndexesWithObjects(orderInvoices.toArray());
+	}
+
+	private Long createPurchaseIndexes() {
+		logger.info("正在创建采购单索引...");
+		List<Purchase> purchases = purchaseDao.findAll();
+		logger.info("发现数据:" + purchases.size() + "条");
+		return createIndexesWithObjects(purchases.toArray());
+	}
+
+	private Long createPurchaseInvoiceIndexes() {
+		logger.info("正在创建采购发货单索引...");
+		List<PurchaseInvoice> purchaseInvoices = purchaseInvoiceDao.findAll();
+		logger.info("发现数据:" + purchaseInvoices.size() + "条");
+		return createIndexesWithObjects(purchaseInvoices.toArray());
+	}
+
+	/**
+	 * 利用对象数组创建索引
+	 *
+	 * @param objects
+	 *            对象数组,可为Kind、Brand、Order、
+	 *            OrderInvoice、Purchase、
+	 *            PurchaseInvoice
+	 * @return 对象数组的数量
+	 */
+	private Long createIndexesWithObjects(Object[] objects) {
+		if (objects == null || objects.length < 1)
+			return 0L;
+
+		long count = 0;
+		try {
+			for (Object object : objects) {
+				Document document = ObjectToDocumentUtils.toDocument(object);
+				if (document != null) {
+					indexWriter.addDocument(document);
+					count++;
+				}
+			}
+			indexWriter.commit();
+		} catch (IOException e) {
+			logger.error("", e);
+		}
+		return count;
+	}
+
+    @Override
+    public long multiDownloadComponent(Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult) {
+        return multiDownloadData(COMPONENT_TABLE_NAME,threads,startFileIndex,endFileIndex, validateResult);
+    }
+
+    @Override
+    public long multiDownloadGoods(Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult) {
+        return multiDownloadData(GOODS_TABLE_NAME,threads,startFileIndex,endFileIndex, validateResult);
+    }
+
+    /**
+     * 多线程下载数据
+     *
+     * @param tableName      表名
+     * @param threads        线程数量,默认为 1
+     * @param startFileIndex 开始的文件,默认为 1
+     * @param endFileIndex   结束的文件,默认为 1024 * 1024 * 1024
+     * @param validateResult 下载完成后,是否对结果进行校验
+     * @return 下载的数据条数
+     */
+    private long multiDownloadData(String tableName, Integer threads, Integer startFileIndex, Integer endFileIndex, DownloadHelper.ValidateResult validateResult) {
+        threads = threads == null || threads < 1 ? 1 : threads;
+        if (threads > druidDBConfiguration.getMaxActive()) {
+            throw new IllegalArgumentException("线程数量不可超过 " + druidDBConfiguration.getMaxActive());
+        }
+
+        if (tableName.equals(COMPONENT_TABLE_NAME)) {
+            DownloadHelper<Component> downloadHelper = new DownloadHelper<>(threads, startFileIndex, endFileIndex, tableName, "id",componentDao, new DownloadService<Component>(), validateResult);
+            return downloadHelper.getResult();
+
+        } else if (tableName.equals(GOODS_TABLE_NAME)) {
+            DownloadHelper<TradeGoods> downloadHelper = new DownloadHelper<>(threads, startFileIndex, endFileIndex, tableName, "id",tradeGoodsDao, new DownloadService<TradeGoods>(){
+                @Override
+                protected void println(PrintWriter printWriter, TradeGoods element) {
+                    Goods goods = goodsDao.findByTradeGoods(element);
+                    printWriter.println(JSONObject.toJSONString(goods));
+                }
+            }, validateResult);
+            return downloadHelper.getResult();
+        } else {
+            throw new IllegalArgumentException("多线程下载不支持该表:" + tableName);
+        }
+    }
+
+
+	@Override
+	public Object save(Object obj) {
+		if (obj != null) {
+			String tableName = SearchUtils.getTableName(obj.getClass());
+			Document document = ObjectToDocumentUtils.toDocument(obj);
+			if (document != null) {
+				try {
+					indexWriter = indexWriterManager.get(tableName);
+					indexWriter.addDocument(document);
+					indexSearcherManager.flushCache(tableName, indexWriter, null);
+					return obj;
+				} catch (IOException | InterruptedException e) {
+					logger.error("", e);
+				} finally {
+					indexWriterManager.release(tableName);
+				}
+			} else {
+				logger.info("对象转为Document时为null:" + obj);
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public Object update(Object obj) {
+		if (obj != null) {
+			String tableName = SearchUtils.getTableName(obj.getClass());
+			Document document = ObjectToDocumentUtils.toDocument(obj);
+			if (document != null) {
+				try {
+					indexWriter = indexWriterManager.get(tableName);
+					if (obj instanceof Kind) {
+						indexWriter.updateDocument(
+								new Term(SearchConstants.KIND_ID_FIELD, String.valueOf(((Kind) obj).getId())),
+								document);
+					} else if (obj instanceof Brand) {
+						indexWriter.updateDocument(new Term(SearchConstants.BRAND_ID_FIELD,
+								String.valueOf(((Brand) obj).getId())), document);
+					} else if (obj instanceof Component) {
+						indexWriter.updateDocument(new Term(SearchConstants.COMPONENT_ID_FIELD,
+								String.valueOf(((Component) obj).getId())), document);
+					} else if (obj instanceof Order) {
+						indexWriter.updateDocument(new Term(SearchConstants.ORDER_ID_FIELD,
+								String.valueOf(((Order) obj).getId())), document);
+					} else if (obj instanceof OrderInvoice) {
+						indexWriter.updateDocument(new Term(SearchConstants.ORDER_INVOICE_ID_FIELD,
+								String.valueOf(((OrderInvoice) obj).getId())), document);
+					} else if (obj instanceof Purchase) {
+						indexWriter.updateDocument(new Term(SearchConstants.PURCHASE_ID_FIELD,
+								String.valueOf(((Purchase) obj).getId())), document);
+					} else if (obj instanceof PurchaseInvoice) {
+						indexWriter.updateDocument(new Term(SearchConstants.PURCHASE_INVOICE_ID_FIELD,
+								String.valueOf(((PurchaseInvoice) obj).getId())), document);
+					} else {
+						throw new SearchException("Message parsing failed!");
+					}
+					indexSearcherManager.flushCache(tableName, indexWriter, null);
+					return obj;
+				} catch (IOException | InterruptedException e) {
+					logger.error("", e);
+				} finally {
+					indexWriterManager.release(tableName);
+				}
+			} else {
+				logger.info("对象转为Document时为null:" + obj);
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public Object delete(Object obj) {
+		if (obj != null) {
+			String tableName = SearchUtils.getTableName(obj.getClass());
+			try {
+				indexWriter = indexWriterManager.get(tableName);
+				if (obj instanceof Kind) {
+					indexWriter.deleteDocuments(
+							new Term(SearchConstants.KIND_ID_FIELD, String.valueOf(((Kind) obj).getId())));
+				} else if (obj instanceof Brand) {
+					indexWriter.deleteDocuments(
+							new Term(SearchConstants.BRAND_ID_FIELD, String.valueOf(((Brand) obj).getId())));
+				} else if (obj instanceof Component) {
+					indexWriter.deleteDocuments(new Term(SearchConstants.COMPONENT_ID_FIELD,
+							String.valueOf(((Component) obj).getId())));
+				} else if (obj instanceof Goods) {
+					indexWriter.deleteDocuments(toTerm((Goods) obj));
+				} else if (obj instanceof Order) {
+					indexWriter.deleteDocuments(
+							new Term(SearchConstants.ORDER_ID_FIELD, String.valueOf(((Order) obj).getId())));
+				} else if (obj instanceof OrderInvoice) {
+					indexWriter.deleteDocuments(new Term(SearchConstants.ORDER_INVOICE_ID_FIELD,
+							String.valueOf(((OrderInvoice) obj).getId())));
+				} else if (obj instanceof Purchase) {
+					indexWriter.deleteDocuments(new Term(SearchConstants.PURCHASE_ID_FIELD,
+							String.valueOf(((Purchase) obj).getId())));
+				} else if (obj instanceof PurchaseInvoice) {
+					indexWriter.deleteDocuments(new Term(SearchConstants.PURCHASE_INVOICE_ID_FIELD,
+							String.valueOf(((PurchaseInvoice) obj).getId())));
+				} else {
+					throw new SearchException("Message parsing failed!");
+				}
+				indexSearcherManager.flushCache(tableName, indexWriter, null);
+				return obj;
+			} catch (IOException e) {
+				logger.error("", e);
+			} catch (InterruptedException e) {
+				logger.error("", e);
+			} finally {
+				indexWriterManager.release(tableName);
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 根据goods构造Term
+	 *
+	 * @param goods
+	 * @return
+	 */
+	private Term toTerm(Goods goods) {
+		if (goods.getTradeGoods() != null) {
+			return new Term(SearchConstants.GOODS_GO_ID_FIELD, String.valueOf(goods.getTradeGoods().getId()));
+		} else if (goods.getComponent() != null) {
+			return new Term(SearchConstants.GOODS_CMP_ID_FIELD, String.valueOf(goods.getComponent().getId()));
+		}
+		return null;
+	}
+
+	@Override
+	public List<Object> maintainIndexes(@NotEmpty("parsedQueueMessage") ParsedQueueMessage parsedQueueMessage) {
+		Object object = parsedQueueMessage.getObject();
+		if (object == null) {
+			return null;
+		}
+		List<Object> maintainedObjects = new ArrayList<>();
+        // 新增、更新索引
+        if (parsedQueueMessage.isInsert() || parsedQueueMessage.isUpdate()) {
+            if (object instanceof Goods) {
+                List<Goods> goodsesList = goodsDao.find((Goods) object);
+                delete(object);
+                for (Goods goods : goodsesList) {
+                    Object maintainedObject = save(goods);
+                    if (maintainedObject != null) {
+                        maintainedObjects.add(maintainedObject);
+                    }
+                }
+            } else {
+                Object maintainedObject = update(object);
+                if (maintainedObject != null) {
+                    maintainedObjects.add(maintainedObject);
+                }
+            }
+        }
+        // 删除索引
+        else if (parsedQueueMessage.isDelete()) {
+            Object maintainedObject = delete(object);
+            if (maintainedObject != null) {
+                maintainedObjects.add(maintainedObject);
+            }
+        } else {
+            throw new IllegalStateException("message parsing failed!");
+        }
+		return maintainedObjects;
+	}
+
+	@Override
+	public List<Object> maintainIndexes(@NotEmpty("tableName") String tableName, @NotEmpty("dataId") Long dataId, @NotEmpty("methodType") String methodType, String data) {
+		ParsedQueueMessage parsedQueueMessage = queueMessageParser.parse(tableName, dataId, methodType, data);
+		return maintainIndexes(parsedQueueMessage);
+	}
+
+	@Override
+	public Map<String, List<Object>> updateIndexByNewWords(List<String> newWords) {
+		Map<String, List<Object>> updatedObjects = new HashMap<>();
+		// 类目
+		updatedObjects.put(SearchConstants.KIND_TABLE_NAME, updateIndexByNewWords(newWords,
+				SearchConstants.KIND_TABLE_NAME, SearchConstants.KIND_ID_FIELD,
+				SearchConstants.KIND_NAMECN_FIELD));
+		// 品牌
+		updatedObjects.put(SearchConstants.BRAND_TABLE_NAME, updateIndexByNewWords(newWords,
+				SearchConstants.BRAND_TABLE_NAME, SearchConstants.BRAND_ID_FIELD,
+				SearchConstants.BRAND_NAMECN_FIELD, SearchConstants.BRAND_NAMEEN_FIELD));
+		// 器件
+		updatedObjects.put(SearchConstants.COMPONENT_TABLE_NAME, updateIndexByNewWords(newWords,
+				SearchConstants.COMPONENT_TABLE_NAME, SearchConstants.COMPONENT_ID_FIELD,
+				SearchConstants.COMPONENT_CODE_FIELD, SearchConstants.COMPONENT_KI_NAME_FIELD,
+				SearchConstants.COMPONENT_BR_NAMECN_FIELD, SearchConstants.COMPONENT_BR_NAMEEN_FIELD));
+		// 批次
+		updatedObjects.put(SearchConstants.GOODS_TABLE_NAME, updateIndexByNewWords(newWords,
+				SearchConstants.GOODS_TABLE_NAME, SearchConstants.GOODS_CMP_ID_FIELD,
+				SearchConstants.GOODS_CMP_CODE_FIELD, SearchConstants.GOODS_KI_NAME_CN_FIELD,
+				SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD, SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD));
+		return updatedObjects;
+	}
+
+    /**
+     * 根据(录入的词典)新词更新指定表的索引
+     *
+     * @param newWords  词典新词
+     * @param tableName 表名
+     * @param idField   id 字段名称
+     * @param fields    新词可能牵涉到的字段名称
+     * @return 更新的对象
+     */
+    private List<Object> updateIndexByNewWords(List<String> newWords, String tableName, String idField, String... fields) {
+        List<Object> updatedObjects = new ArrayList<>();
+        if (!CollectionUtils.isEmpty(newWords)) {
+            BooleanQuery booleanQuery = new BooleanQuery();
+            for (String newWord : newWords) {
+                for (String field : fields) {
+                    booleanQuery.add(SearchUtils.getBooleanQuery(field, newWord, false, false, BooleanClause.Occur.MUST), BooleanClause.Occur.SHOULD);
+                }
+            }
+            logger.info(booleanQuery.toString());
+            SPage<Document> documents = SearchUtils.getDocuments(tableName, booleanQuery, null,
+                    SearchConstants.TOP_NUM);
+            for (Document document : documents.getContent()) {
+                updatedObjects.addAll(maintainIndexes(tableName, Long.parseLong(document.get(idField)), "update", null));
+            }
+        }
+        return updatedObjects;
+    }
+
+}

+ 411 - 0
mall-search/src/main/java/com/uas/search/service/impl/OrderSearchServiceImpl.java

@@ -0,0 +1,411 @@
+package com.uas.search.service.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.StringUtils;
+
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.constant.model.BaseOrder;
+import com.uas.search.constant.model.BaseOrderDetail;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.PageParams.FilterField;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.OrderInvoice;
+import com.uas.search.model.Order;
+import com.uas.search.model.PurchaseInvoice;
+import com.uas.search.model.Purchase;
+import com.uas.search.service.OrderSearchService;
+import com.uas.search.util.DocumentToObjectUtils;
+import com.uas.search.util.OrderSearchUtils;
+import com.uas.search.util.SearchUtils;
+
+/**
+ * 单据搜索实现
+ * 
+ * @author sunyj
+ * @since 2016年10月18日 下午5:59:12
+ */
+@Service
+public class OrderSearchServiceImpl implements OrderSearchService {
+
+	/**
+	 * 默认的页码
+	 */
+	private static final int PAGE_INDEX = 1;
+
+	/**
+	 * 默认每页的大小
+	 */
+	private static final int PAGE_SIZE = 20;
+
+	private static Logger logger = LoggerFactory.getLogger(OrderSearchServiceImpl.class);
+
+	@Override
+	public SPage<BaseOrder> getOrderIdsWithDetail(String keyword, Table_name tableName, PageParams params) {
+		if (tableName == Table_name.ORDER) {
+			return searchOrderIds(keyword, params);
+		} else if (tableName == Table_name.ORDER_INVOICE) {
+			return searchOrderInvoiceIds(keyword, params);
+		} else if (tableName == Table_name.PURCHASE) {
+			return searchPurchaseIds(keyword, params);
+		} else if (tableName == Table_name.PURCHASE_INVOICE) {
+			return searchPurchaseInvoiceIds(keyword, params);
+		}
+		throw new SearchException("tableName不合法");
+	}
+
+	@Override
+	public SPage<BaseOrder> searchOrderIds(String keyword, PageParams params) {
+		return searchMainOrder(keyword, params, SearchConstants.ORDER_TABLE_NAME);
+	}
+
+	@Override
+	public SPage<BaseOrder> searchOrderInvoiceIds(String keyword, PageParams params) {
+		return searchMainOrder(keyword, params, SearchConstants.ORDER_INVOICE_TABLE_NAME);
+	}
+
+	@Override
+	public SPage<BaseOrder> searchPurchaseIds(String keyword, PageParams params) {
+		return searchMainOrder(keyword, params, SearchConstants.PURCHASE_TABLE_NAME);
+	}
+
+	@Override
+	public SPage<BaseOrder> searchPurchaseInvoiceIds(String keyword, PageParams params) {
+		return searchMainOrder(keyword, params, SearchConstants.PURCHASE_INVOICE_TABLE_NAME);
+	}
+
+	/**
+	 * 搜索主索引(相对于明细来说)
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param params
+	 *            其他信息,可能有分页信息,filter中可能有可以过滤的信息
+	 * @param tableName
+	 *            主表名
+	 * @return 搜索结果
+	 */
+	private SPage<BaseOrder> searchMainOrder(String keyword, PageParams params, String tableName) {
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
+		// 获取该表keyword可以搜索的域
+		List<String> keywordFields = OrderSearchUtils.getKeywordFields(tableName);
+
+		SPage<BaseOrder> sPage = new SPage<>();
+		BooleanQuery booleanQuery = new BooleanQuery();
+
+		if (keyword == null) {
+			keyword = "";
+		}
+		// 关键词带空格,进行与操作
+		String[] strs = keyword.split(" ");
+		for (String str : strs) {
+			// keyword可能是哪些域
+			BooleanQuery booleanQuery2 = new BooleanQuery();
+			for (String keywordField : keywordFields) {
+				booleanQuery2.add(SearchUtils.getRegexpQuery(keywordField, str), BooleanClause.Occur.SHOULD);
+			}
+			booleanQuery.add(booleanQuery2, BooleanClause.Occur.MUST);
+		}
+
+		// 分页信息
+		if (params == null) {
+			sPage.setPage(PAGE_INDEX);
+			sPage.setSize(PAGE_SIZE);
+			sPage.setFirst(true);
+		} else {
+			int page = params.getPage();
+			if (page > 0) {
+				sPage.setPage(page);
+			} else {
+				sPage.setPage(PAGE_INDEX);
+				sPage.setFirst(true);
+			}
+			int size = params.getSize();
+			if (size > 0) {
+				sPage.setSize(size);
+			} else {
+				sPage.setSize(PAGE_SIZE);
+			}
+
+			Map<FilterField, Object> filters = params.getFilters();
+			if (!CollectionUtils.isEmpty(filters)) {
+				// 如果filter中有订单状态
+				if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_STATUS))) {
+					@SuppressWarnings("unchecked")
+					List<Integer> statusList = (List<Integer>) filters.get(FilterField.ORDER_STATUS);
+					BooleanQuery booleanQuery2 = new BooleanQuery();
+					for (Integer status : statusList) {
+						booleanQuery2.add(
+								new TermQuery(
+										new Term(OrderSearchUtils.getStatusField(tableName), String.valueOf(status))),
+								BooleanClause.Occur.SHOULD);
+					}
+					booleanQuery.add(booleanQuery2, BooleanClause.Occur.MUST);
+				}
+				// 如果filter中有买方个人uu
+				if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_BUYER_UU))) {
+					booleanQuery.add(new TermQuery(new Term(OrderSearchUtils.getBuyeruuField(tableName),
+							filters.get(FilterField.ORDER_BUYER_UU).toString())), BooleanClause.Occur.MUST);
+				}
+				// 如果filter中有买方企业uu
+				if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_BUYER_EN_UU))) {
+					booleanQuery.add(new TermQuery(new Term(OrderSearchUtils.getBuyerenuuField(tableName),
+							filters.get(FilterField.ORDER_BUYER_EN_UU).toString())), BooleanClause.Occur.MUST);
+				}
+				// 如果filter中有卖方企业uu
+				if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_SELLER_EN_UU))) {
+					booleanQuery.add(new TermQuery(new Term(OrderSearchUtils.getSellerenuuField(tableName),
+							filters.get(FilterField.ORDER_SELLER_EN_UU).toString())), BooleanClause.Occur.MUST);
+				}
+				// 如果最小时间和最大时间至少传了一个参数,说明需要对时间范围进行限定
+				if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_MIN_TIME))
+						|| !StringUtils.isEmpty(filters.get(FilterField.ORDER_MAX_TIME))) {
+					// 最小日期初始化为2016.1.1
+					Calendar calendar = Calendar.getInstance();
+					calendar.set(2016, 0, 1);
+					// 最大日期初始化为现在
+					long minTime = calendar.getTimeInMillis();
+					long maxTime = new Date().getTime();
+					if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_MIN_TIME))) {
+						minTime = Long.parseLong(filters.get(FilterField.ORDER_MIN_TIME).toString());
+					}
+					if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_MAX_TIME))) {
+						maxTime = Long.parseLong(filters.get(FilterField.ORDER_MAX_TIME).toString());
+					}
+					booleanQuery.add(NumericRangeQuery.newLongRange(SearchConstants.CREATETIME_FIELD, minTime, maxTime,
+							true, true), BooleanClause.Occur.MUST);
+				}
+			}
+		}
+		logger.info(booleanQuery.toString());
+
+		try {
+			TopDocs topDocs;
+			// 如果页码不为1
+			if (sPage.getPage() > 1) {
+				TopDocs previousTopDocs = indexSearcher.search(booleanQuery, (sPage.getPage() - 1) * sPage.getSize());
+				int totalHits = previousTopDocs.totalHits;
+				ScoreDoc[] previousScoreDocs = previousTopDocs.scoreDocs;
+				// 如果主表中未匹配到结果,再搜索其明细表
+				if (totalHits == 0) {
+					return searchDetails(keyword, params, tableName);
+				} else {
+					if ((sPage.getPage() - 1) * sPage.getSize() >= totalHits) {
+						throw new SearchException("页码过大:元素总数量为" + totalHits);
+					}
+					topDocs = indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], booleanQuery,
+							sPage.getSize());
+				}
+			} else {
+				sPage.setFirst(true);
+				topDocs = indexSearcher.search(booleanQuery, sPage.getSize());
+			}
+			// 如果主表中未匹配到结果,再搜索其明细表
+			int totalHits = topDocs.totalHits;
+			if (totalHits < 1) {
+				return searchDetails(keyword, params, tableName);
+			}
+
+			// 设置总元素个数、页数等信息
+			sPage.setTotalElement(totalHits);
+			int totalPage = (int) Math.ceil(totalHits / (1.0 * sPage.getSize()));
+			sPage.setTotalPage(totalPage);
+			if (totalPage == sPage.getPage()) {
+				sPage.setLast(true);
+			}
+
+			ScoreDoc[] scoreDocs = topDocs.scoreDocs;
+			List<BaseOrder> baseOrders = new ArrayList<>();
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				Document document = indexSearcher.doc(scoreDoc.doc);
+				Long id = Long.valueOf(document.get(OrderSearchUtils.getIdField(tableName)));
+				String code = document.get(OrderSearchUtils.getCodeField(tableName));
+				BaseOrder baseOrder = new BaseOrder(id, code);
+				List<BaseOrderDetail> details = DocumentToObjectUtils
+						.toDetailsList(document.get(OrderSearchUtils.getDetailsField(tableName)));
+				if (!CollectionUtils.isEmpty(details)) {
+					baseOrder.setDetails(details);
+				}
+				baseOrders.add(baseOrder);
+			}
+			sPage.setContent(baseOrders);
+		} catch (NumberFormatException | IOException e) {
+			throw new SearchException(e).setDetailedMessage(e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return sPage;
+	}
+
+	/**
+	 * 搜索明细索引
+	 * 
+	 * @param keyword
+	 *            关键词
+	 * @param params
+	 *            其他信息,可能有分页信息,filter中可能有可以过滤的信息
+	 * @param tableName
+	 *            明细表名
+	 * @return 搜索结果
+	 */
+	private SPage<BaseOrder> searchDetails(String keyword, PageParams params, String tableName) {
+		// 与主表不同,不允许keyword为空(不允许利用正则表达式获取所有明细索引,原因是正则搜索与分词有冲突)
+		if (SearchUtils.isKeywordInvalid(keyword)) {
+			throw new SearchException("搜索关键词无效:" + keyword);
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
+
+		SPage<BaseOrder> sPage = new SPage<>();
+		BooleanQuery booleanQuery = new BooleanQuery();
+
+		// 关键词带空格,进行与操作
+		String[] strs = keyword.split(" ");
+		for (String str : strs) {
+			booleanQuery.add(SearchUtils.getBooleanQuery(OrderSearchUtils.getDetailsField(tableName), str),
+					BooleanClause.Occur.MUST);
+		}
+
+		// 分页信息
+		if (params == null) {
+			sPage.setPage(PAGE_INDEX);
+			sPage.setSize(PAGE_SIZE);
+			sPage.setFirst(true);
+		} else {
+			int page = params.getPage();
+			if (page > 0) {
+				sPage.setPage(page);
+			} else {
+				sPage.setPage(PAGE_INDEX);
+				sPage.setFirst(true);
+			}
+			int size = params.getSize();
+			if (size > 0) {
+				sPage.setSize(size);
+			} else {
+				sPage.setSize(PAGE_SIZE);
+			}
+
+			Map<FilterField, Object> filters = params.getFilters();
+			if (!CollectionUtils.isEmpty(filters)) {
+				// 如果最小时间和最大时间至少传了一个参数,说明需要对时间范围进行限定
+				if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_MIN_TIME))
+						|| !StringUtils.isEmpty(filters.get(FilterField.ORDER_MAX_TIME))) {
+					// 最小日期初始化为2016.1.1
+					Calendar calendar = Calendar.getInstance();
+					calendar.set(2016, 0, 1);
+					// 最大日期初始化为现在
+					long minTime = calendar.getTimeInMillis();
+					long maxTime = new Date().getTime();
+					if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_MIN_TIME))) {
+						minTime = Long.parseLong(filters.get(FilterField.ORDER_MIN_TIME).toString());
+					}
+					if (!StringUtils.isEmpty(filters.get(FilterField.ORDER_MAX_TIME))) {
+						maxTime = Long.parseLong(filters.get(FilterField.ORDER_MAX_TIME).toString());
+					}
+					booleanQuery.add(NumericRangeQuery.newLongRange(SearchConstants.CREATETIME_FIELD, minTime, maxTime,
+							true, true), BooleanClause.Occur.MUST);
+				}
+			}
+		}
+		logger.info(booleanQuery.toString());
+
+		try {
+			TopDocs topDocs = null;
+			// 如果页码不为1
+			if (sPage.getPage() > 1) {
+				TopDocs previousTopDocs = indexSearcher.search(booleanQuery, (sPage.getPage() - 1) * sPage.getSize());
+				int totalHits = previousTopDocs.totalHits;
+				ScoreDoc[] previousScoreDocs = previousTopDocs.scoreDocs;
+				if (totalHits != 0) {
+					if ((sPage.getPage() - 1) * sPage.getSize() >= totalHits) {
+						throw new SearchException("页码过大:元素总数量为" + totalHits);
+					}
+					topDocs = indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], booleanQuery,
+							sPage.getSize());
+				}
+			} else {
+				sPage.setFirst(true);
+				topDocs = indexSearcher.search(booleanQuery, sPage.getSize());
+			}
+
+			// 设置总元素个数、页数等信息
+			if (topDocs == null || topDocs.totalHits < 1) {
+				sPage.setTotalElement(0);
+				sPage.setFirst(true);
+				sPage.setLast(true);
+				return sPage;
+			}
+			sPage.setTotalElement(topDocs.totalHits);
+			int totalPage = (int) Math.ceil(topDocs.totalHits / (1.0 * sPage.getSize()));
+			sPage.setTotalPage(totalPage);
+			if (totalPage == sPage.getPage()) {
+				sPage.setLast(true);
+			}
+
+			ScoreDoc[] scoreDocs = topDocs.scoreDocs;
+			List<BaseOrder> baseOrders = new ArrayList<>();
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				Document document = indexSearcher.doc(scoreDoc.doc);
+				Long id = Long.valueOf(document.get(OrderSearchUtils.getIdField(tableName)));
+				String code = document.get(OrderSearchUtils.getCodeField(tableName));
+				BaseOrder baseOrder = new BaseOrder(id, code);
+				List<BaseOrderDetail> details = DocumentToObjectUtils
+						.toDetailsList(document.get(OrderSearchUtils.getDetailsField(tableName)));
+				if (!CollectionUtils.isEmpty(details)) {
+					baseOrder.setDetails(details);
+				}
+				baseOrders.add(baseOrder);
+			}
+			sPage.setContent(baseOrders);
+		} catch (NumberFormatException | IOException e) {
+			throw new SearchException(e).setDetailedMessage(e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return sPage;
+	}
+
+	@Override
+	public Order getOrder(Long id) {
+		return DocumentToObjectUtils.toOrder(
+				SearchUtils.getDocumentById(SearchConstants.ORDER_TABLE_NAME, SearchConstants.ORDER_ID_FIELD, id));
+	}
+
+	@Override
+	public OrderInvoice getOrderInvoice(Long id) {
+		return DocumentToObjectUtils.toOrderInvoice(SearchUtils
+				.getDocumentById(SearchConstants.ORDER_INVOICE_TABLE_NAME, SearchConstants.ORDER_INVOICE_ID_FIELD, id));
+	}
+
+	@Override
+	public Purchase getPurchase(Long id) {
+		return DocumentToObjectUtils.toPurchase(SearchUtils.getDocumentById(SearchConstants.PURCHASE_TABLE_NAME,
+				SearchConstants.PURCHASE_ID_FIELD, id));
+	}
+
+	@Override
+	public PurchaseInvoice getPurchaseInvoice(Long id) {
+		return DocumentToObjectUtils.toPurchaseInvoice(SearchUtils.getDocumentById(
+				SearchConstants.PURCHASE_INVOICE_TABLE_NAME, SearchConstants.PURCHASE_INVOICE_ID_FIELD, id));
+	}
+
+}

+ 1349 - 0
mall-search/src/main/java/com/uas/search/service/impl/SearchServiceImpl.java

@@ -0,0 +1,1349 @@
+package com.uas.search.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.annotation.NotEmpty;
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.constant.model.CollectField;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.constant.model.PageParams.FilterField;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.exception.SearchException;
+import com.uas.search.grouping.DistinctGroupCollector;
+import com.uas.search.grouping.GoodsGroupCollector;
+import com.uas.search.model.*;
+import com.uas.search.service.SearchService;
+import com.uas.search.sort.SimilarValuesFieldComparatorSource;
+import com.uas.search.sort.StringFieldComparatorSource;
+import com.uas.search.util.DocumentToObjectUtils;
+import com.uas.search.util.SearchUtils;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.*;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.SortField.Type;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.StringUtils;
+
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * 搜索索引
+ * 
+ * @author sunyj
+ * @since 2016年8月5日 下午2:21:26
+ */
+@Service
+public class SearchServiceImpl implements SearchService {
+
+	/**
+	 * 获取联想词时返回的最大数目
+	 */
+	private static final int SIMILAR_NUM = 20;
+
+	private static Logger logger = LoggerFactory.getLogger(SearchServiceImpl.class);
+
+	@Override
+	public SPage<Long> getKindIds(String keyword, Integer page, Integer size) {
+		List<Long> ids = new ArrayList<>();
+		SPage<Document> documents = getKindDocuments(keyword, page, size);;
+		SPage<Long> sPage = new SPage<Long>(documents.getTotalPage(), documents.getTotalElement(), documents.getPage(),
+				documents.getSize(), documents.isFirst(), documents.isLast());
+		for (Document document : documents.getContent()) {
+			ids.add(Long.parseLong(document.get(SearchConstants.KIND_ID_FIELD)));
+		}
+		sPage.setContent(ids);
+		return sPage;
+	}
+
+	@Override
+	public SPage<Map<String, Object>> getKinds(String keyword, Integer page, Integer size) {
+		List<Map<String, Object>> kinds = new ArrayList<>();
+		SPage<Document> documents = getKindDocuments(keyword, page, size);
+		SPage<Map<String, Object>> sPage = new SPage<>(documents.getTotalPage(),
+				documents.getTotalElement(), documents.getPage(), documents.getSize(), documents.isFirst(),
+				documents.isLast());
+		for (Document document : documents.getContent()) {
+			Map<String, Object> kind = new HashMap<>();
+			kind.put("id", Long.parseLong(document.get(SearchConstants.KIND_ID_FIELD)));
+			kind.put("nameCn", document.get(SearchConstants.KIND_NAMECN_FIELD));
+			kinds.add(kind);
+		}
+		sPage.setContent(kinds);
+		return sPage;
+	}
+
+	private SPage<Document> getKindDocuments(String keyword, Integer page, Integer size){
+		if (SearchUtils.isKeywordInvalid(keyword)) {
+			throw new SearchException("搜索关键词无效:" + keyword);
+		}
+		BooleanQuery booleanQuery = SearchUtils.getBooleanQuery(SearchConstants.KIND_NAMECN_FIELD, keyword);
+		logger.info(booleanQuery.toString());
+		return SearchUtils.getDocuments(SearchConstants.KIND_TABLE_NAME, booleanQuery, new Sort(sortKind()), page, size);
+	}
+
+    /**
+     * @return 类目排序规则
+     */
+    private SortField[] sortKind() {
+        // 分数 > 访问量 > 搜索次数
+        return new SortField[]{
+                SortField.FIELD_SCORE,
+                sortField(SearchConstants.KIND_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.KIND_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE)
+        };
+    }
+
+    /**
+     * 构造 SortField
+     *
+     * @param field        Name of field to sort by.  Can be <code>null</code> if
+     *                     <code>type</code> is SCORE or DOC.
+     * @param type         Type of values in the terms.
+     * @param reverse      True if natural order should be reversed.
+     * @param missingValue Used for 'sortMissingFirst/Last'
+     * @return SortField
+     */
+    private SortField sortField(String field, Type type, boolean reverse, Object missingValue) {
+        SortField sortField = new SortField(field, type, reverse);
+        sortField.setMissingValue(missingValue);
+        return sortField;
+    }
+
+	@Override
+	public SPage<Long> getBrandIds(String keyword, Integer page, Integer size) {
+		List<Long> ids = new ArrayList<>();
+		SPage<Document> documents = getBrandDocuments(keyword, page, size);
+		SPage<Long> sPage = new SPage<Long>(documents.getTotalPage(), documents.getTotalElement(), documents.getPage(),
+				documents.getSize(), documents.isFirst(), documents.isLast());
+		for (Document document : documents.getContent()) {
+			ids.add(Long.parseLong(document.get(SearchConstants.BRAND_ID_FIELD)));
+		}
+		sPage.setContent(ids);
+		return sPage;
+	}
+
+	@Override
+	public SPage<Map<String, Object>> getBrands(String keyword, Integer page, Integer size) {
+		List<Map<String, Object>> brands = new ArrayList<>();
+		SPage<Document> documents = getBrandDocuments(keyword, page, size);
+		SPage<Map<String, Object>> sPage = new SPage<Map<String, Object>>(documents.getTotalPage(),
+				documents.getTotalElement(), documents.getPage(), documents.getSize(), documents.isFirst(),
+				documents.isLast());
+		for (Document document : documents.getContent()) {
+			Map<String, Object> brand = new HashMap<String, Object>();
+			brand.put("id", Long.parseLong(document.get(SearchConstants.BRAND_ID_FIELD)));
+			brand.put("uuid", document.get(SearchConstants.BRAND_UUID_FIELD));
+			brand.put("nameCn", document.get(SearchConstants.BRAND_NAMECN_FIELD));
+			brand.put("nameEn", document.get(SearchConstants.BRAND_NAMEEN_FIELD));
+			brands.add(brand);
+		}
+		sPage.setContent(brands);
+		return sPage;
+	}
+
+	private SPage<Document> getBrandDocuments(String keyword, Integer page, Integer size){
+		if (SearchUtils.isKeywordInvalid(keyword)) {
+			throw new SearchException("搜索关键词无效:" + keyword);
+		}
+		BooleanQuery booleanQuery = new BooleanQuery();
+		booleanQuery.add(SearchUtils.getBooleanQuery(SearchConstants.BRAND_NAMECN_UNTOKENIZED_FIELD, keyword, true),
+				BooleanClause.Occur.SHOULD);
+		booleanQuery.add(SearchUtils.getBooleanQuery(SearchConstants.BRAND_NAMEEN_UNTOKENIZED_FIELD, keyword, true),
+				BooleanClause.Occur.SHOULD);
+		logger.info(booleanQuery.toString());
+		return SearchUtils.getDocuments(SearchConstants.BRAND_TABLE_NAME, booleanQuery, new Sort(sortBrand(keyword)), page,
+				size);
+	}
+
+	/**
+	 * @return 品牌排序规则
+	 */
+	private SortField[] sortBrand(String keyword) {
+		// 自定义排序 > 权重 > 访问量 > 搜索次数 > 分数
+        // 分数排序放在最后,是因为有的中英文名称相同,分数翻倍,但实际匹配度并不高
+        return new SortField[]{
+				new SortField(SearchConstants.BRAND_NAMEEN_UNTOKENIZED_FIELD, new StringFieldComparatorSource(keyword)),
+				new SortField(SearchConstants.BRAND_NAMECN_UNTOKENIZED_FIELD, new StringFieldComparatorSource(keyword)),
+                sortField(SearchConstants.BRAND_WEIGHT_FIELD, Type.DOUBLE, true, Double.MIN_VALUE),
+                sortField(SearchConstants.BRAND_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.BRAND_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                SortField.FIELD_SCORE
+        };
+	}
+
+	@Override
+	public Map<String, Object> getComponentIds(String keyword, PageParams pageParams) {
+		Map<String, Object> searchComponentIds = getComponentIds(keyword, pageParams, null, null);
+		return searchComponentIds;
+		// TODO 对品牌、类目甚至拼音混合搜索(待完善)
+		// int total = (int) searchComponentIds.get("total");
+		// if (total != 0) {
+		// return searchComponentIds;
+		// }
+		// List<Long> kindIds = getKindIds(keyword, Occur.SHOULD);
+		// List<Long> brandIds = getBrandIds(keyword, Occur.SHOULD);
+		// return getComponentIds(null, pageParams, kindIds, brandIds);
+	}
+
+	/**
+	 * 根据关键词搜索产品
+	 * 
+	 * @param keyword
+	 * @param pageParams
+	 * @param kindIds
+	 * @param brandIds
+	 * @return
+	 * @throws SearchException
+	 */
+	private Map<String, Object> getComponentIds(String keyword, PageParams pageParams, List<Long> kindIds,
+			List<Long> brandIds) throws SearchException {
+		// 因为器件、属性值的数据量远比类目、品牌大得多,而且器件搜索可能还需进行分页,
+		// 所以涉及器件、属性值的搜索,大都不能像类目和品牌一样直接利用SearchUtils.getDocuments方法
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(SearchConstants.COMPONENT_TABLE_NAME);
+
+		if (pageParams == null) {
+			pageParams = new PageParams();
+		}
+		if (pageParams.getPage() <= 0)
+			pageParams.setPage(1);
+		if (pageParams.getSize() <= 0)
+			pageParams.setSize(20);
+
+		Map<String, Object> map = new HashMap<String, Object>();
+		List<Long> ids = new ArrayList<Long>();
+		try {
+			BooleanQuery booleanQuery = new BooleanQuery();
+			if (!SearchUtils.isKeywordInvalid(keyword)) {
+				booleanQuery.add(setBoost(keyword), BooleanClause.Occur.MUST);
+			}
+
+			Map<FilterField, Object> filters = pageParams.getFilters();
+			if (!CollectionUtils.isEmpty(filters)) {
+				// 筛选类目
+				if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_KINDID))) {
+					String kindId = filters.get(FilterField.COMPONENT_KINDID).toString();
+					TermQuery kindQuery = new TermQuery(new Term(SearchConstants.COMPONENT_KI_ID_FIELD, kindId));
+					booleanQuery.add(kindQuery, BooleanClause.Occur.MUST);
+				}
+
+				// 筛选品牌
+				if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_BRANDID))) {
+					String brandId = filters.get(FilterField.COMPONENT_BRANDID).toString();
+					TermQuery brandQuery = new TermQuery(new Term(SearchConstants.COMPONENT_BR_ID_FIELD, brandId));
+					booleanQuery.add(brandQuery, BooleanClause.Occur.MUST);
+				}
+
+				// 库存不为0
+				if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_RESERVE))) {
+					Boolean isReserveNotEmpty = (Boolean) filters.get(FilterField.COMPONENT_HAS_RESERVE);
+					if (isReserveNotEmpty) {
+						booleanQuery.add(NumericRangeQuery.newDoubleRange(SearchConstants.COMPONENT_RESERVE_FIELD, 0.0,
+								Double.MAX_VALUE, false, true), BooleanClause.Occur.MUST);
+					}
+				}
+
+				// 现货、呆滞库存、样品数量不为0,取或的关系
+				if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_SAMPLE))
+						|| !StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_ORIGINAL))
+						|| !StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_INACTION_STOCK))) {
+					BooleanQuery booleanQuery2 = new BooleanQuery();
+					if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_SAMPLE))) {
+						booleanQuery2.add(NumericRangeQuery.newDoubleRange(SearchConstants.COMPONENT_SAMPLE_QTY_FIELD,
+								0.0, Double.MAX_VALUE, false, true), BooleanClause.Occur.SHOULD);
+					}
+					if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_ORIGINAL))) {
+						booleanQuery2.add(NumericRangeQuery.newDoubleRange(SearchConstants.COMPONENT_ORIGINAL_QTY_FIELD,
+								0.0, Double.MAX_VALUE, false, true), BooleanClause.Occur.SHOULD);
+					}
+					if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_HAS_INACTION_STOCK))) {
+						booleanQuery2.add(
+								NumericRangeQuery.newDoubleRange(SearchConstants.COMPONENT_INACTION_STOCK_QTY_FIELD,
+										0.0, Double.MAX_VALUE, false, true),
+								BooleanClause.Occur.SHOULD);
+					}
+					booleanQuery.add(booleanQuery2, Occur.MUST);
+				}
+
+				// 属性过滤
+				if (!StringUtils.isEmpty(filters.get(FilterField.COMPONENT_PROPERTIES))) {
+					JSONObject proJSON = JSONObject
+							.parseObject(String.valueOf(filters.get(FilterField.COMPONENT_PROPERTIES)));
+					for (String key : proJSON.keySet()) {
+						String value = String.valueOf(proJSON.get(key));
+						if (!StringUtils.isEmpty(value)) {
+							if (!key.startsWith(SearchConstants.COMPONENT_PROPERTY_PREFIX)) {
+								key = SearchConstants.COMPONENT_PROPERTY_PREFIX + key;
+							}
+							TermQuery propertyQuery = new TermQuery(new Term(key, value));
+							booleanQuery.add(propertyQuery, BooleanClause.Occur.MUST);
+						}
+					}
+				}
+			}
+			if (!CollectionUtils.isEmpty(kindIds)) {
+				BooleanQuery booleanQuery2 = new BooleanQuery();
+				for (Long id : kindIds) {
+					booleanQuery2.add(new TermQuery(new Term(SearchConstants.COMPONENT_KI_ID_FIELD, id.toString())),
+							Occur.SHOULD);
+				}
+				booleanQuery.add(booleanQuery2, Occur.MUST);
+			}
+			if (!CollectionUtils.isEmpty(brandIds)) {
+				BooleanQuery booleanQuery2 = new BooleanQuery();
+				for (Long id : brandIds) {
+					booleanQuery2.add(new TermQuery(new Term(SearchConstants.COMPONENT_BR_ID_FIELD, id.toString())),
+							Occur.SHOULD);
+				}
+				booleanQuery.add(booleanQuery2, Occur.MUST);
+			}
+			logger.info(booleanQuery.toString());
+
+			Sort sort = new Sort(sortComponent());
+			TopDocs hits;
+			if (pageParams.getPage() > 1) {// 不是第一页
+				TopDocs previousHits = indexSearcher.search(booleanQuery,
+						(pageParams.getPage() - 1) * pageParams.getSize(), sort, true, false);
+				ScoreDoc[] previousScoreDocs = previousHits.scoreDocs;
+				ScoreDoc after = previousScoreDocs[previousScoreDocs.length - 1];
+				hits = indexSearcher.searchAfter(after, booleanQuery, pageParams.getSize(), sort, true, false);
+			} else {
+				hits = indexSearcher.search(booleanQuery, pageParams.getSize(), sort, true, false);
+			}
+
+			ScoreDoc[] scoreDocs = hits.scoreDocs;
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				// 数据量太大,需要指定将获取的数据(以免载入不必要的数据,降低速度)
+				Set<String> fieldsToLoad = new HashSet<>();
+				fieldsToLoad.add(SearchConstants.COMPONENT_ID_FIELD);
+				Document document = indexSearcher.doc(scoreDoc.doc, fieldsToLoad);
+				String componentId = document.get(SearchConstants.COMPONENT_ID_FIELD);
+				ids.add(Long.parseLong(componentId));
+				// System.out.println(componentId + "\t" + scoreDoc.score);
+			}
+			map.put("componentIds", ids);
+			map.put("page", pageParams.getPage());
+			map.put("size", pageParams.getSize());
+			map.put("total", hits.totalHits);
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return map;
+	}
+
+	/**
+	 * 同时搜索器件、类目、品牌,并设置boost
+	 * 
+	 * @param keyword
+	 * @return
+	 */
+	private Query setBoost(String keyword) {
+		BooleanQuery booleanQuery = new BooleanQuery();
+		PrefixQuery prefixQuery = new PrefixQuery(
+				new Term(SearchConstants.COMPONENT_CODE_FIELD, keyword.toLowerCase()));
+		prefixQuery.setBoost(100);
+		booleanQuery.add(prefixQuery, BooleanClause.Occur.SHOULD);
+		BooleanQuery brandNameCnQuery = SearchUtils.getBooleanQuery(SearchConstants.COMPONENT_BR_NAMECN_FIELD,
+				keyword);
+		brandNameCnQuery.setBoost(10);
+		booleanQuery.add(brandNameCnQuery, BooleanClause.Occur.SHOULD);
+		BooleanQuery brandNameEnQuery = SearchUtils.getBooleanQuery(SearchConstants.COMPONENT_BR_NAMEEN_FIELD,
+				keyword);
+		brandNameEnQuery.setBoost(10);
+		booleanQuery.add(brandNameEnQuery, BooleanClause.Occur.SHOULD);
+		BooleanQuery kindNameQuery = SearchUtils.getBooleanQuery(SearchConstants.COMPONENT_KI_NAME_FIELD, keyword);
+		kindNameQuery.setBoost(1);
+		booleanQuery.add(kindNameQuery, BooleanClause.Occur.SHOULD);
+		return booleanQuery;
+	}
+
+    /**
+     * @return 器件排序规则
+     */
+    private SortField[] sortComponent() {
+        // 分数 > 器件(访问量 > 搜索次数) > 品牌(权重 > 访问量 > 搜索次数) > 类目(访问量 > 搜索次数)
+        return new SortField[]{
+                SortField.FIELD_SCORE,
+                sortField(SearchConstants.COMPONENT_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.COMPONENT_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.COMPONENT_BR_WEIGHT_FIELD, Type.DOUBLE, true, Double.MIN_VALUE),
+                sortField(SearchConstants.COMPONENT_BR_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.COMPONENT_BR_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.COMPONENT_KI_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.COMPONENT_KI_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE)
+        };
+    }
+
+	@Override
+	public Set<Long> getKindIdsBySearchComponent(String keyword, String brandId) {
+		Query filter = null;
+		// 筛选品牌
+		if (!StringUtils.isEmpty(brandId)) {
+			filter = new TermQuery(new Term(SearchConstants.COMPONENT_BR_ID_FIELD, brandId));
+		}
+		return collectBySearchComponent(keyword, filter, SearchConstants.COMPONENT_KI_ID_FIELD).getValues();
+	}
+
+	@Override
+	public Set<Map<String, Object>> getKindsBySearchComponent(String keyword, String brandId) {
+		Query filter = null;
+		// 筛选品牌
+		if (!StringUtils.isEmpty(brandId)) {
+			filter = new TermQuery(new Term(SearchConstants.COMPONENT_BR_ID_FIELD, brandId));
+		}
+		return collectBySearchComponent(keyword, filter, SearchConstants.KIND_ID_FIELD,
+				new DistinctGroupCollector.CollectField(SearchConstants.KIND_ID_FIELD, "id"),
+				new DistinctGroupCollector.CollectField(SearchConstants.KIND_NAMECN_FIELD, "nameCn"))
+				.getCollectValues();
+	}
+
+	@Override
+	public Set<Long> getBrandIdsBySearchComponent(String keyword, String kindId) {
+		Query filter = null;
+		// 筛选类目
+		if (!StringUtils.isEmpty(kindId)) {
+			filter = new TermQuery(new Term(SearchConstants.COMPONENT_KI_ID_FIELD, kindId));
+		}
+		return collectBySearchComponent(keyword, filter, SearchConstants.COMPONENT_BR_ID_FIELD).getValues();
+	}
+
+	@Override
+	public Set<Map<String, Object>> getBrandsBySearchComponent(String keyword, String kindId) {
+		Query filter = null;
+		// 筛选类目
+		if (!StringUtils.isEmpty(kindId)) {
+			filter = new TermQuery(new Term(SearchConstants.COMPONENT_KI_ID_FIELD, kindId));
+		}
+		return collectBySearchComponent(keyword, filter, SearchConstants.COMPONENT_BR_ID_FIELD,
+				new DistinctGroupCollector.CollectField(SearchConstants.COMPONENT_BR_ID_FIELD, "id"),
+				new DistinctGroupCollector.CollectField(SearchConstants.COMPONENT_BR_UUID_FIELD, "uuid"),
+				new DistinctGroupCollector.CollectField(SearchConstants.COMPONENT_BR_NAMECN_FIELD, "nameCn"))
+				.getCollectValues();
+	}
+
+    /**
+     * 搜索器件时统计指定信息
+     *
+     * @param keyword       关键词
+     * @param filter        过滤条件
+     * @param groupField    统计的单个字段(多为 id)
+     * @param collectFields 统计的多个字段(详细信息),可为空
+     * @return 统计信息
+     */
+    private DistinctGroupCollector collectBySearchComponent(String keyword, Query filter, String groupField, DistinctGroupCollector.CollectField... collectFields){
+		if (SearchUtils.isKeywordInvalid(keyword)) {
+			throw new SearchException("搜索关键词无效:" + keyword);
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(SearchConstants.COMPONENT_TABLE_NAME);
+		try {
+			BooleanQuery booleanQuery = new BooleanQuery();
+			keyword = URLDecoder.decode(keyword, "UTF-8");
+			booleanQuery.add(setBoost(keyword), BooleanClause.Occur.MUST);
+
+			if(filter != null){
+				booleanQuery.add(filter, Occur.FILTER);
+			}
+			logger.info(booleanQuery.toString());
+
+			DistinctGroupCollector collector = new DistinctGroupCollector(groupField, collectFields);
+			indexSearcher.search(booleanQuery, collector);
+			return collector;
+		} catch (IOException e) {
+			throw new IllegalStateException("统计失败", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+	}
+
+	@Override
+	public List<String> getSimilarKeywords(String keyword, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		List<String> result = new ArrayList<>();
+
+		// 相似的器件原厂型号数量足够,直接返回
+		List<String> componentCodes = getSimilarComponentCodes(keyword, size);
+		result.addAll(componentCodes);
+		removeDuplicate(result);
+		if (result.size() == size) {
+			return result;
+		}
+
+		// 获取相似类目
+		List<String> kindNames = getSimilarKindNames(keyword, size);
+		if (!CollectionUtils.isEmpty(kindNames)) {
+			result.addAll(kindNames);
+			removeDuplicate(result);
+			// 如果总的数量超出SIMILAR_NUM,去除多余的元素
+			if (result.size() > size) {
+				removeElements(result, size);
+				return result;
+			}
+		}
+
+		// 获取相似品牌
+		List<String> brandNames = getSimilarBrandNames(keyword, size);
+		if (!CollectionUtils.isEmpty(brandNames)) {
+			result.addAll(brandNames);
+			removeDuplicate(result);
+			if (result.size() > size) {
+				removeElements(result, size);
+				return result;
+			}
+		}
+
+		return result;
+	}
+
+	@Override
+	public List<Map<String, Object>> getSimilarComponents(String componentCode, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		if (SearchUtils.isKeywordInvalid(componentCode)) {
+			throw new SearchException("输入无效:" + componentCode);
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(SearchConstants.COMPONENT_TABLE_NAME);
+
+		List<Map<String, Object>> components = new ArrayList<>();
+		try {
+			PrefixQuery prefixQuery = new PrefixQuery(
+					new Term(SearchConstants.COMPONENT_CODE_FIELD, componentCode.toLowerCase()));
+			logger.info(prefixQuery.toString());
+			TopDocs hits = indexSearcher.search(prefixQuery, size);
+			ScoreDoc[] scoreDocs = hits.scoreDocs;
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				Set<String> fieldsToLoad = new HashSet<>();
+				fieldsToLoad.add(SearchConstants.COMPONENT_ID_FIELD);
+				fieldsToLoad.add(SearchConstants.COMPONENT_UUID_FIELD);
+				fieldsToLoad.add(SearchConstants.COMPONENT_CODE_FIELD);
+				Document document = indexSearcher.doc(scoreDoc.doc, fieldsToLoad);
+				Map<String, Object> map = new HashMap<>();
+				map.put("id", Long.parseLong(document.get(SearchConstants.COMPONENT_ID_FIELD)));
+				map.put("uuid", document.get(SearchConstants.COMPONENT_UUID_FIELD));
+				map.put("code", document.get(SearchConstants.COMPONENT_CODE_FIELD));
+				components.add(map);
+			}
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return components;
+	}
+
+	@Override
+	public List<Map<String, Object>> getSimilarBrands(String brandName, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		if (SearchUtils.isKeywordInvalid(brandName)) {
+			throw new SearchException("输入无效:" + brandName);
+		}
+		List<Map<String, Object>> brands = new ArrayList<Map<String, Object>>();
+		// 品牌名称带有空格,并且中英文名并无一定顺序,因此对nameCn、nameEn均要搜索
+		BooleanQuery booleanQuery = new BooleanQuery();
+		// 搜索nameCn
+		booleanQuery.add(SearchUtils.getBooleanQuery(SearchConstants.BRAND_NAMECN_FIELD, brandName),
+				BooleanClause.Occur.SHOULD);
+		// 搜索nameEn
+		booleanQuery.add(SearchUtils.getBooleanQuery(SearchConstants.BRAND_NAMEEN_FIELD, brandName),
+				BooleanClause.Occur.SHOULD);
+		logger.info(booleanQuery.toString());
+
+		List<Document> documents = SearchUtils.getDocuments(SearchConstants.BRAND_TABLE_NAME, booleanQuery, null, size)
+				.getContent();
+		for (Document document : documents) {
+			Map<String, Object> brand = new HashMap<>();
+			brand.put("id", Long.parseLong(document.get(SearchConstants.BRAND_ID_FIELD)));
+			brand.put("uuid", document.get(SearchConstants.BRAND_UUID_FIELD));
+			brand.put("nameCn", document.get(SearchConstants.BRAND_NAMECN_FIELD));
+			brand.put("nameEn", document.get(SearchConstants.BRAND_NAMEEN_FIELD));
+			brands.add(brand);
+		}
+		return brands;
+	}
+
+	@Override
+	public List<Map<String, Object>> getSimilarKinds(String kindName, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		return getSimilarKinds(kindName, null, null, size);
+	}
+
+	@Override
+	public List<Map<String, Object>> getSimilarLeafKinds(String kindName, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		return getSimilarKinds(kindName, (short) 1, null, size);
+	}
+
+	@Override
+	public List<Map<String, Object>> getSimilarKindsByLevel(String kindName, Short level, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		return getSimilarKinds(kindName, null, level, size);
+	}
+
+	/**
+	 * 根据输入的类目名获取联想词
+	 * 
+	 * @param kindName
+	 *            类目名
+	 * @param isLeaf
+	 *            是否只获取末级类目
+	 * @param level
+	 *            指定的类目级别
+	 * @param size 指定的联想词数目
+	 * @return
+	 */
+	private List<Map<String, Object>> getSimilarKinds(String kindName, Short isLeaf, Short level, Integer size) {
+		size = size == null || size < 1 ? SIMILAR_NUM : size;
+		if (SearchUtils.isKeywordInvalid(kindName)) {
+			throw new SearchException("输入无效:" + kindName);
+		}
+		List<Map<String, Object>> kinds = new ArrayList<>();
+		BooleanQuery booleanQuery = new BooleanQuery();
+		booleanQuery.add(SearchUtils.getBooleanQuery(SearchConstants.KIND_NAMECN_FIELD, kindName),
+				BooleanClause.Occur.MUST);
+
+		if (isLeaf != null && isLeaf == 1) {
+			booleanQuery.add(new TermQuery(new Term(SearchConstants.KIND_ISLEAF_FIELD, String.valueOf(isLeaf))),
+					BooleanClause.Occur.MUST);
+		} else {
+			if (level != null && level > 0) {
+				booleanQuery.add(new TermQuery(new Term(SearchConstants.KIND_LEVEL_FIELD, String.valueOf(level))),
+						BooleanClause.Occur.MUST);
+			}
+		}
+		logger.info(booleanQuery.toString());
+		List<Document> documents = SearchUtils.getDocuments(SearchConstants.KIND_TABLE_NAME, booleanQuery, null, size).getContent();
+		for (Document document : documents) {
+			Map<String, Object> map = new HashMap<>();
+			map.put("id", Long.parseLong(document.get(SearchConstants.KIND_ID_FIELD)));
+			map.put("nameCn", document.get(SearchConstants.KIND_NAMECN_FIELD));
+			map.put("isLeaf", Short.parseShort(document.get(SearchConstants.KIND_ISLEAF_FIELD)));
+			map.put("level", Short.parseShort(document.get(SearchConstants.KIND_LEVEL_FIELD)));
+			kinds.add(map);
+		}
+		return kinds;
+	}
+
+	@Override
+	public List<Map<String, String>> getSimilarPropertyValues(Long kindId, Long propertyId, String keyword,
+			Long topNum) {
+		if (kindId == null || propertyId == null) {
+			throw new SearchException("类目id和属性id不能为空");
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(SearchConstants.COMPONENT_TABLE_NAME);
+
+		String propertyIdString = String.valueOf(propertyId);
+		if (!propertyIdString.startsWith(SearchConstants.COMPONENT_PROPERTY_PREFIX)) {
+			propertyIdString = SearchConstants.COMPONENT_PROPERTY_PREFIX + propertyIdString;
+		}
+		propertyIdString = propertyIdString + SearchConstants.COMPONENT_PROPERTY_TOKENIZED_SUFFIX;
+		if (keyword == null) {
+			keyword = "";
+		}
+		if (topNum == null || topNum < 1) {
+			topNum = (long) SIMILAR_NUM;
+		}
+
+		List<String> propertyValues = new ArrayList<>();
+		try {
+			BooleanQuery booleanQuery = new BooleanQuery();
+			booleanQuery.add(new TermQuery(new Term(SearchConstants.COMPONENT_KI_ID_FIELD, String.valueOf(kindId))),
+					BooleanClause.Occur.MUST);
+			booleanQuery.add(new PrefixQuery(new Term(propertyIdString, keyword.toLowerCase())),
+					BooleanClause.Occur.MUST);
+			logger.info(booleanQuery.toString());
+			// 如果只搜索topNum个结果,去除重复的属性值后,数目很可能是不够的
+			TopDocs topDocs = indexSearcher.search(booleanQuery, SearchConstants.TOP_NUM);
+			ScoreDoc[] scoreDocs = topDocs.scoreDocs;
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				Set<String> fieldsToLoad = new HashSet<>();
+				fieldsToLoad.add(propertyIdString);
+				Document document = indexSearcher.doc(scoreDoc.doc, fieldsToLoad);
+				String propertyValue = document.get(propertyIdString);
+				if (!StringUtils.isEmpty(propertyValue) && !propertyValues.contains(propertyValue)) {
+					propertyValues.add(propertyValue);
+				}
+				if (propertyValues.size() >= topNum) {
+					break;
+				}
+			}
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+
+		List<Map<String, String>> result = new ArrayList<>();
+		for (String propertyValue : propertyValues) {
+			Map<String, String> map = new HashMap<>();
+			map.put("propertyValue", propertyValue);
+			result.add(map);
+		}
+		return result;
+
+	}
+
+	/**
+	 * 根据输入获取相似的器件原厂型号
+	 * 
+	 * @param componentCode
+	 * @param size 指定的联想词数目
+	 * @return
+	 */
+	private List<String> getSimilarComponentCodes(String componentCode, Integer size) {
+		return getSimilarValues(SearchConstants.COMPONENT_TABLE_NAME, SearchConstants.COMPONENT_CODE_FIELD,
+				componentCode.toLowerCase(), QueryType.PREFIX_QUERY, size);
+	}
+
+	/**
+	 * 根据输入获取相似的品牌名称
+	 * 
+	 * @param brandName
+	 * @param size 指定的联想词数目
+	 * @return
+	 */
+	private List<String> getSimilarBrandNames(String brandName, Integer size) {
+		// 获取相似的中文品牌
+		List<String> nameCns = getSimilarValues(SearchConstants.BRAND_TABLE_NAME, SearchConstants.BRAND_NAMECN_FIELD,
+				brandName, QueryType.BOOLEAN_QUERY, size);
+		// 相似的中文品牌数量足够,直接返回
+		if (nameCns != null && nameCns.size() == SIMILAR_NUM) {
+			return nameCns;
+		}
+
+		List<String> names = nameCns;
+		// 获取相似的英文品牌
+		List<String> nameEns = getSimilarValues(SearchConstants.BRAND_TABLE_NAME, SearchConstants.BRAND_NAMEEN_FIELD,
+				brandName, QueryType.BOOLEAN_QUERY, size);
+		names.addAll(nameEns);
+		return names;
+	}
+
+	/**
+	 * 根据输入获取相似的类目名称
+	 * 
+	 * @param kindName
+	 * @param size 指定的联想词数目
+	 * @return
+	 */
+	private List<String> getSimilarKindNames(String kindName, Integer size) {
+		return getSimilarValues(SearchConstants.KIND_TABLE_NAME, SearchConstants.KIND_NAMECN_FIELD, kindName,
+				QueryType.BOOLEAN_QUERY, size);
+	}
+
+	private enum QueryType {
+		BOOLEAN_QUERY, PREFIX_QUERY
+	}
+
+	/**
+	 * 根据输入值获取该域相似的值
+	 * 
+	 * @param tableName
+	 * @param field
+	 * @param keyword
+	 * @param size 指定的联想词数目
+	 * @return
+	 */
+	private List<String> getSimilarValues(String tableName, String field, String keyword, QueryType queryType, Integer size) {
+		if (SearchUtils.isKeywordInvalid(keyword)) {
+			throw new SearchException("输入无效:" + keyword);
+		}
+		if (queryType == null) {
+			queryType = QueryType.BOOLEAN_QUERY;
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
+
+		List<String> result = new ArrayList<>();
+		try {
+			Query query = null;
+			if (queryType == QueryType.BOOLEAN_QUERY) {
+				query = SearchUtils.getBooleanQuery(field, keyword);
+			} else if (queryType == QueryType.PREFIX_QUERY) {
+				query = new PrefixQuery(new Term(field, keyword));
+			}
+			logger.info(query.toString());
+			Sort sort = new Sort(new SortField(field, new SimilarValuesFieldComparatorSource()));
+			TopDocs hits = indexSearcher.search(query, size, sort, true, false);
+			ScoreDoc[] scoreDocs = hits.scoreDocs;
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				Set<String> fieldsToLoad = new HashSet<>();
+				fieldsToLoad.add(field);
+				Document document = indexSearcher.doc(scoreDoc.doc, fieldsToLoad);
+				result.add(document.get(field));
+				// System.out.println(document.get(field) + "\t" +
+				// scoreDoc.score);
+			}
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return result;
+	}
+
+	/**
+	 * 移除集合中重复的元素
+	 * 
+	 * @param list
+	 * @return
+	 */
+	private void removeDuplicate(List<String> list) {
+		if (list == null) {
+			return;
+		}
+		List<String> result = new ArrayList<>();
+		for (String str : list) {
+			if (!result.contains(str)) {
+				result.add(str);
+			}
+		}
+		list.removeAll(list);
+		list.addAll(result);
+	}
+
+	/**
+	 * 删除collection内startIndex(含)后的元素
+	 * 
+	 * @param collection
+	 * @param startIndex
+	 */
+	private void removeElements(Collection<? extends String> collection, int startIndex) {
+		if (CollectionUtils.isEmpty(collection)) {
+			return;
+		}
+		int listsSize = collection.size();
+		for (int i = listsSize - 1; i >= startIndex; i--) {
+			collection.remove(i);
+		}
+	}
+
+	@Override
+	public Map<String, Object> getGoodsIds(String keyword, PageParams pageParams) throws SearchException {
+		List<String> keywordFields = new ArrayList<>();
+		// 先根据品牌搜索,品牌不存在再搜索型号等
+		keywordFields.add(SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD);
+		keywordFields.add(SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD);
+		Map<String, Object> goodsIds = getGoodsIds(keyword, keywordFields, false, pageParams, false);
+		if (CollectionUtils.isEmpty(goodsIds) || goodsIds.get("componentIds") == null
+				|| JSONObject.parseArray(goodsIds.get("componentIds").toString()).isEmpty()) {
+			goodsIds = getGoodsIds(keyword, null, true, pageParams, true);
+		}
+		return goodsIds;
+	}
+
+	/**
+	 * @param keyword
+	 * @param keywordFields
+	 *            要查询的字段
+	 * @param tokenized
+	 *            是否分词
+	 * @param pageParams
+	 * @param recursivelyGet 是否递归获取(逐步降低精度,直到只匹配一个字符)
+	 * @return
+	 * @throws SearchException
+	 */
+	private Map<String, Object> getGoodsIds(String keyword, List<String> keywordFields, Boolean tokenized,
+			PageParams pageParams, Boolean recursivelyGet) throws SearchException {
+		// 因为器件、属性值的数据量远比类目、品牌大得多,而且器件搜索可能还需进行分页,
+		// 所以涉及器件、属性值的搜索,大都不能像类目和品牌一样直接利用SearchUtils.getDocuments方法
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(SearchConstants.GOODS_TABLE_NAME);
+
+		if (pageParams == null) {
+			pageParams = new PageParams();
+		}
+		if (pageParams.getPage() <= 0)
+			pageParams.setPage(1);
+		if (pageParams.getSize() <= 0)
+			pageParams.setSize(20);
+
+		Map<String, Object> map = new HashMap<String, Object>();
+		List<Long> cmpIds = new ArrayList<>();
+		List<Long> goIds = new ArrayList<>();
+		try {
+			BooleanQuery booleanQuery = queryGoods(keyword, keywordFields, tokenized);
+			Map<FilterField, Object> filters = pageParams.getFilters();
+            // 筛选状态
+            if (!CollectionUtils.isEmpty(filters) && !StringUtils.isEmpty(filters.get(FilterField.GOODS_STATUS))) {
+                filter(filters.get(FilterField.GOODS_STATUS), SearchConstants.GOODS_GO_STATUS_FIELD, booleanQuery);
+            } else {
+                // 未指定时,过滤默认状态
+                filter(Arrays.asList(TradeGoods.VALID_STATUS), SearchConstants.GOODS_GO_STATUS_FIELD, booleanQuery);
+            }
+			if (!CollectionUtils.isEmpty(filters)) {
+				// 筛选类目
+				if (!StringUtils.isEmpty(filters.get(FilterField.GOODS_KINDID))) {
+					filter(filters.get(FilterField.GOODS_KINDID), SearchConstants.GOODS_KI_ID_FIELD, booleanQuery);
+				}
+
+				// 筛选品牌
+				if (!StringUtils.isEmpty(filters.get(FilterField.GOODS_BRANDID))) {
+					filter(filters.get(FilterField.GOODS_BRANDID), SearchConstants.GOODS_BR_ID_FIELD, booleanQuery);
+				}
+
+				// 筛选货源
+				if (!StringUtils.isEmpty(filters.get(FilterField.GOODS_STORE_TYPE))) {
+					filter(filters.get(FilterField.GOODS_STORE_TYPE), SearchConstants.GOODS_ST_TYPE_FIELD, booleanQuery);
+				}
+
+				// 筛选货币
+				if (!StringUtils.isEmpty(filters.get(FilterField.GOODS_CRNAME))) {
+					filter(filters.get(FilterField.GOODS_CRNAME), SearchConstants.GOODS_CRNAME_FIELD, booleanQuery);
+				}
+
+				// 价格筛选
+				Object minPriceRmb = filters.get(FilterField.GOODS_MINPRICERMB);
+				Object maxPriceRmb = filters.get(FilterField.GOODS_MAXPRICERMB);
+				Object minPriceUsd = filters.get(FilterField.GOODS_MINPRICEUSD);
+				Object maxPriceUsd = filters.get(FilterField.GOODS_MAXPRICEUSD);
+				// 筛选人民币价格
+				if (!StringUtils.isEmpty(minPriceRmb) || !StringUtils.isEmpty(maxPriceRmb)) {
+					Double minPrice = null;
+					Double maxPrice = null;
+					if (!StringUtils.isEmpty(minPriceRmb)) {
+						minPrice = Double.valueOf(minPriceRmb.toString());
+					}
+					if (!StringUtils.isEmpty(maxPriceRmb)) {
+						maxPrice = Double.valueOf(maxPriceRmb.toString());
+					}
+					booleanQuery.add(NumericRangeQuery.newDoubleRange(SearchConstants.GOODS_GO_MINPRICERMB_FIELD,
+							minPrice, maxPrice, true, true), BooleanClause.Occur.FILTER);
+				}
+				// 筛选美元价格
+				if (!StringUtils.isEmpty(minPriceUsd) || !StringUtils.isEmpty(maxPriceUsd)) {
+					Double minPrice = null;
+					Double maxPrice = null;
+					if (!StringUtils.isEmpty(minPriceUsd)) {
+						minPrice = Double.valueOf(minPriceUsd.toString());
+					}
+					if (!StringUtils.isEmpty(maxPriceUsd)) {
+						maxPrice = Double.valueOf(maxPriceUsd.toString());
+					}
+					booleanQuery.add(NumericRangeQuery.newDoubleRange(SearchConstants.GOODS_GO_MINPRICEUSD_FIELD,
+							minPrice, maxPrice, true, true), BooleanClause.Occur.FILTER);
+				}
+
+			}
+			logger.info(booleanQuery.toString());
+
+			// 排序
+            // 自定义排序字段 > 分数 > 批次(访问量) > 器件(访问量 > 搜索次数) > 品牌(权重 > 访问量 > 搜索次数) > 类目(访问量 > 搜索次数)
+            List<SortField> sortFieldList = new ArrayList<>();
+			List<com.uas.search.constant.model.Sort> sorts = pageParams.getSort();
+			if (sorts != null && !CollectionUtils.isEmpty(sorts)) {
+				SortField[] sortFields = new SortField[sorts.size()];
+				int i = 0;
+				for (com.uas.search.constant.model.Sort s : sorts) {
+					if (s.getField() == null) {
+						throw new SearchException("排序字段不可为空:" + s);
+					}
+					switch (s.getField()) {
+					// 价格
+					case GO_RESERVE:
+						sortFields[i++] = new SortField(SearchConstants.GOODS_GO_RESERVE_FIELD, Type.DOUBLE,
+								s.isReverse());
+						break;
+					// 人民币价格
+					case GO_MINPRICERMB:
+						sortFields[i++] = new SortField(SearchConstants.GOODS_GO_MINPRICERMB_FIELD, Type.DOUBLE,
+								s.isReverse());
+						break;
+					// 美元价格
+					case GO_MINPRICEUSD:
+						sortFields[i++] = new SortField(SearchConstants.GOODS_GO_MINPRICEUSD_FIELD, Type.DOUBLE,
+								s.isReverse());
+						break;
+					// 打分
+					case GO_SEARCH:
+						sortFields[i++] = (SortField.FIELD_SCORE);
+						break;
+					default:
+						throw new SearchException("不支持该排序方式:" + s.getField());
+					}
+				}
+                sortFieldList.addAll(Arrays.asList(sortFields));
+			} else{
+			    sortFieldList.add(SortField.FIELD_SCORE);
+            }
+            sortFieldList.addAll(Arrays.asList(sortGoods(keyword)));
+            SortField[] sortFields = new SortField[sortFieldList.size()];
+            sortFieldList.toArray(sortFields);
+            Sort sort = new Sort(sortFields);
+
+			TopDocs hits;
+			if (pageParams.getPage() > 1) {// 不是第一页
+				TopDocs previousHits = indexSearcher.search(booleanQuery,
+						(pageParams.getPage() - 1) * pageParams.getSize(), sort, true, false);
+				int totalHits = previousHits.totalHits;
+				if ((pageParams.getPage() - 1) * pageParams.getSize() >= totalHits) {
+					return map;
+				}
+				ScoreDoc[] previousScoreDocs = previousHits.scoreDocs;
+				ScoreDoc after = previousScoreDocs[previousScoreDocs.length - 1];
+				hits = indexSearcher.searchAfter(after, booleanQuery, pageParams.getSize(), sort, true, false);
+			} else {
+				hits = indexSearcher.search(booleanQuery, pageParams.getSize(), sort, true, false);
+			}
+
+			// 如果没有结果,则降低精度,直至 keyword 长度为 1
+			if(recursivelyGet && hits.totalHits < 1 && !SearchUtils.isKeywordInvalid(keyword) && keyword.length() > 1){
+				return getGoodsIds(keyword.substring(0, keyword.length() - 1), keywordFields, tokenized, pageParams, recursivelyGet);
+			}
+			// 数据量太大,需要指定将获取的数据(以免载入不必要的数据,降低速度)
+			Set<String> fieldsToLoad = new HashSet<>();
+			fieldsToLoad.add(SearchConstants.GOODS_CMP_ID_FIELD);
+			fieldsToLoad.add(SearchConstants.GOODS_GO_ID_FIELD);
+			ScoreDoc[] scoreDocs = hits.scoreDocs;
+			for (ScoreDoc scoreDoc : scoreDocs) {
+				Document document = indexSearcher.doc(scoreDoc.doc, fieldsToLoad);
+				String cmpId = document.get(SearchConstants.GOODS_CMP_ID_FIELD);
+				cmpIds.add(StringUtils.isEmpty(cmpId) ? null : Long.valueOf(cmpId));
+				String goId = document.get(SearchConstants.GOODS_GO_ID_FIELD);
+				goIds.add(StringUtils.isEmpty(goId) ? null : Long.valueOf(goId));
+				// System.out.println(cmpId + "\t" + goId + "\t" +
+				// scoreDoc.score);
+				// System.out.println(indexSearcher.explain(booleanQuery,
+				// scoreDoc.doc).toString());
+			}
+			map.put("componentIds", cmpIds);
+			map.put("goodsIds", goIds);
+			map.put("page", pageParams.getPage());
+			map.put("size", pageParams.getSize());
+			map.put("total", hits.totalHits);
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return map;
+	}
+
+    /**
+     * @return 批次排序规则
+     */
+    private SortField[] sortGoods(String keyword) {
+        // 器件、非标型号自定义排序 > 批次(访问量) > 器件(自定义排序 > 访问量 > 搜索次数) > 品牌(自定义排序 > 权重 > 访问量 > 搜索次数) > 类目(访问量 > 搜索次数)
+        return new SortField[]{
+                new SortField(SearchConstants.GOODS_CMP_CODE_FIELD, new StringFieldComparatorSource(keyword)),
+                new SortField(SearchConstants.GOODS_PR_PCMPCODE_FIELD, new StringFieldComparatorSource(keyword)),
+                sortField(SearchConstants.GOODS_GO_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.GOODS_CMP_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.GOODS_CMP_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                new SortField(SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD, new StringFieldComparatorSource(keyword)),
+                new SortField(SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD, new StringFieldComparatorSource(keyword)),
+                sortField(SearchConstants.GOODS_BR_WEIGHT_FIELD, Type.DOUBLE, true, Double.MIN_VALUE),
+                sortField(SearchConstants.GOODS_BR_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.GOODS_BR_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.GOODS_KI_VISIT_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE),
+                sortField(SearchConstants.GOODS_KI_SEARCH_COUNT_FIELD, Type.LONG, true, Long.MIN_VALUE)
+        };
+    }
+
+	@Override
+	public List<Map<String, Object>> collectBySearchGoods(String keyword, CollectField collectedField,
+			Map<FilterField, Object> filters) {
+		List<String> keywordFields = new ArrayList<>();
+		// 先根据品牌搜索,品牌不存在再搜索型号等
+		keywordFields.add(SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD);
+		keywordFields.add(SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD);
+		List<Map<String, Object>> result = collectBySearchGoods(keyword, keywordFields, false, collectedField, filters, false);
+		if (CollectionUtils.isEmpty(result)) {
+			result = collectBySearchGoods(keyword, null, true, collectedField, filters, true);
+		}
+		return result;
+
+	}
+
+	/**
+	 * @param keyword
+	 * @param keywordFields
+	 *            要查询的字段
+	 * @param tokenized
+	 *            是否分词
+	 * @param collectedField
+	 * @param filters
+	 * @param recursivelyGet 是否递归获取(逐步降低精度,直到只匹配一个字符)
+	 * @return
+	 */
+	private List<Map<String, Object>> collectBySearchGoods(String keyword, List<String> keywordFields,
+			Boolean tokenized, CollectField collectedField, Map<FilterField, Object> filters, Boolean recursivelyGet) {
+		if (collectedField == null && CollectionUtils.isEmpty(filters)) {
+			throw new SearchException("参数不合法:collectedField=" + collectedField + ", filter=" + filters);
+		}
+		// 与批次搜索的搜索词保持一致,最终有结果的 keyword 是相同的
+		if(recursivelyGet != null && recursivelyGet){
+			PageParams pageParams = new PageParams();
+			pageParams.setFilters(filters);
+			Map<String, Object> goodsIds = getGoodsIds(keyword, keywordFields, tokenized, pageParams, false);
+			if(Integer.parseInt(goodsIds.get("total").toString()) < 1 && !SearchUtils.isKeywordInvalid(keyword) && keyword.length() > 1){
+				return collectBySearchGoods(keyword.substring(0, keyword.length() - 1), keywordFields, tokenized, collectedField, filters, true);
+			}
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(SearchConstants.GOODS_TABLE_NAME);
+
+		List<Map<String, Object>> result = new ArrayList<>();
+		try {
+			BooleanQuery booleanQuery = queryGoods(keyword, keywordFields, tokenized);
+            // 筛选状态
+            if (!CollectionUtils.isEmpty(filters) && !StringUtils.isEmpty(filters.get(FilterField.GOODS_STATUS))) {
+                filter(filters.get(FilterField.GOODS_STATUS), SearchConstants.GOODS_GO_STATUS_FIELD, booleanQuery);
+            } else {
+                // 未指定时,过滤默认状态
+                filter(Arrays.asList(TradeGoods.VALID_STATUS), SearchConstants.GOODS_GO_STATUS_FIELD, booleanQuery);
+            }
+			// 过滤
+			Set<Entry<FilterField, Object>> entrySet = filters.entrySet();
+			for (Entry<FilterField, Object> entry : entrySet) {
+				switch (entry.getKey()) {
+				case GOODS_KINDID:
+					filter(entry.getValue(), SearchConstants.GOODS_KI_ID_FIELD, booleanQuery);
+					break;
+				case GOODS_BRANDID:
+					filter(entry.getValue(), SearchConstants.GOODS_BR_ID_FIELD, booleanQuery);
+					break;
+				case GOODS_STORE_TYPE:
+					filter(entry.getValue(), SearchConstants.GOODS_ST_TYPE_FIELD, booleanQuery);
+					break;
+				case GOODS_CRNAME:
+					filter(entry.getValue(), SearchConstants.GOODS_CRNAME_FIELD, booleanQuery);
+					break;
+				default:
+					throw new SearchException("不支持该过滤字段:" + entry.getKey());
+				}
+			}
+			logger.info(booleanQuery.toString());
+
+			// 统计
+			String uniqueField;
+			Set<String> fieldsToLoad = new HashSet<>();
+			switch (collectedField) {
+			case GOODS_KIND:
+				uniqueField = SearchConstants.GOODS_KI_ID_FIELD;
+				fieldsToLoad.add(SearchConstants.GOODS_KI_ID_FIELD);
+				fieldsToLoad.add(SearchConstants.GOODS_KI_NAME_CN_FIELD);
+				break;
+			case GOODS_BRAND:
+				uniqueField = SearchConstants.GOODS_BR_ID_FIELD;
+				fieldsToLoad.add(SearchConstants.GOODS_BR_ID_FIELD);
+				fieldsToLoad.add(SearchConstants.GOODS_BR_UUID_FIELD);
+				fieldsToLoad.add(SearchConstants.GOODS_BR_NAME_CN_FIELD);
+				fieldsToLoad.add(SearchConstants.GOODS_BR_NAME_EN_FIELD);
+				break;
+			case GOODS_STORE_TYPE:
+				uniqueField = SearchConstants.GOODS_ST_TYPE_FIELD;
+				fieldsToLoad.add(SearchConstants.GOODS_ST_TYPE_FIELD);
+				break;
+			case GOODS_CRNAME:
+				uniqueField = SearchConstants.GOODS_CRNAME_FIELD;
+				fieldsToLoad.add(SearchConstants.GOODS_CRNAME_FIELD);
+				break;
+			default:
+				throw new SearchException("不支持该统计字段:" + collectedField);
+			}
+			GoodsGroupCollector collector = new GoodsGroupCollector(uniqueField, fieldsToLoad);
+			indexSearcher.search(booleanQuery, collector);
+			result = collector.getValues();
+		} catch (IOException e) {
+			logger.error("", e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return result;
+	}
+
+	/**
+	 * 获取查询批次的query
+	 * 
+	 * @param keyword
+	 * @param keywordFields
+	 * @param tokenized
+	 * @return
+	 */
+	private BooleanQuery queryGoods(String keyword, List<String> keywordFields, Boolean tokenized) {
+		BooleanQuery booleanQuery = new BooleanQuery();
+		if (!SearchUtils.isKeywordInvalid(keyword)) {
+			// 未指定搜索的字段,则采用默认搜索逻辑
+			if (CollectionUtils.isEmpty(keywordFields)) {
+				booleanQuery.add(setGoodsBoost(keyword), BooleanClause.Occur.MUST);
+			} else {
+				BooleanQuery booleanQuery2 = new BooleanQuery();
+				for (String keywordField : keywordFields) {
+					// 是否分词
+					if (tokenized == null || !tokenized.booleanValue()) {
+						booleanQuery2.add(new TermQuery(new Term(keywordField, keyword.toLowerCase())), BooleanClause.Occur.SHOULD);
+					} else {
+						booleanQuery2.add(SearchUtils.getBooleanQuery(keywordField, keyword), BooleanClause.Occur.SHOULD);
+					}
+				}
+				booleanQuery.add(booleanQuery2, BooleanClause.Occur.MUST);
+			}
+		}
+		return booleanQuery;
+	}
+
+	/**
+	 * 同时搜索器件、类目、品牌等,并设置boost
+	 */
+	private Query setGoodsBoost(String keyword) {
+		BooleanQuery booleanQuery = new BooleanQuery();
+		// 原厂型号
+		booleanQuery.add(createQuery(SearchConstants.GOODS_CMP_CODE_FIELD, keyword, true, 100), BooleanClause.Occur.SHOULD);
+		// 非标
+		booleanQuery.add(createQuery(SearchConstants.GOODS_PR_PCMPCODE_FIELD, keyword, true, 100), Occur.SHOULD);
+		// 品牌
+		booleanQuery.add(createQuery(SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD, keyword, true, 10), BooleanClause.Occur.SHOULD);
+		booleanQuery.add(createQuery(SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD, keyword, true, 10), BooleanClause.Occur.SHOULD);
+		// 类目
+		booleanQuery.add(createQuery(SearchConstants.GOODS_KI_NAME_CN_FIELD, keyword, 1), BooleanClause.Occur.SHOULD);
+		// 属性值
+		booleanQuery.add(createQuery(SearchConstants.GOODS_CMP_DESCRIPTION_FIELD, keyword, 1), BooleanClause.Occur.SHOULD);
+		return booleanQuery;
+	}
+
+    private BooleanQuery createQuery(String field, String keyword, float boost){
+       return createQuery(field, keyword, false, boost);
+    }
+
+
+    private BooleanQuery createQuery(String field, String keyword, boolean useRegexpQuery, float boost){
+        BooleanQuery booleanQuery = new BooleanQuery();
+        if (StringUtils.isEmpty(field) || StringUtils.isEmpty(keyword)) {
+            return booleanQuery;
+        }
+        // 根据空格分隔关键词,分隔的词取或的关系
+        String[] array = keyword.split(" ");
+        for(String str : array){
+            if(!StringUtils.isEmpty(str)){
+                booleanQuery.add(SearchUtils.getBooleanQuery(field, str, useRegexpQuery), Occur.SHOULD);
+            }
+        }
+        booleanQuery.setBoost(boost);
+        return booleanQuery;
+    }
+
+	/**
+	 * 过滤
+	 * 
+	 * @param list
+	 *            过滤值列表
+	 * @param field
+	 *            过滤的字段
+	 * @param booleanQuery
+	 *            查询条件
+	 */
+	@SuppressWarnings("unchecked")
+	private void filter(Object list, String field, BooleanQuery booleanQuery) {
+		List<Object> values;
+		if (list instanceof List) {
+			values = (List<Object>) list;
+		}else{
+			values = new ArrayList<>();
+			values.add(list);
+		}
+		BooleanQuery booleanQuery2 = new BooleanQuery();
+		for (Object value : values) {
+			TermQuery query = new TermQuery(new Term(field, value.toString().toLowerCase()));
+			booleanQuery2.add(query, BooleanClause.Occur.SHOULD);
+		}
+		booleanQuery.add(booleanQuery2, BooleanClause.Occur.FILTER);
+	}
+
+	@Override
+	public Kind getKind(Long id) {
+		return DocumentToObjectUtils.toKind(
+				SearchUtils.getDocumentById(SearchConstants.KIND_TABLE_NAME, SearchConstants.KIND_ID_FIELD, id));
+	}
+
+	@Override
+	public Brand getBrand(Long id) {
+		return DocumentToObjectUtils.toBrand(
+				SearchUtils.getDocumentById(SearchConstants.BRAND_TABLE_NAME, SearchConstants.BRAND_ID_FIELD, id));
+	}
+
+	@Override
+	public Component getComponent(Long id) {
+		return DocumentToObjectUtils.toComponent(SearchUtils.getDocumentById(SearchConstants.COMPONENT_TABLE_NAME,
+				SearchConstants.COMPONENT_ID_FIELD, id));
+	}
+
+	@Override
+	public Goods getGoods(String id) {
+		return DocumentToObjectUtils.toGoods(
+				SearchUtils.getDocumentById(SearchConstants.GOODS_TABLE_NAME, SearchConstants.GOODS_GO_ID_FIELD, id));
+	}
+
+	@Override
+	public SPage<Object> getObjects(String tableName, String keyword, String field, Boolean tokenized, @NotEmpty("page") Integer page, @NotEmpty("size") Integer size) {
+		if (keyword == null) {
+			keyword = "";
+		}
+		if (field == null) {
+			field = SearchUtils.getIdField(tableName);
+		}
+		if (tokenized == null) {
+			tokenized = false;
+		}
+		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
+		SPage<Object> sPage = new SPage<>();
+		try {
+			Query query;
+			if (tokenized) {
+				query = SearchUtils.getBooleanQuery(field, keyword);
+			} else {
+				query = SearchUtils.getRegexpQuery(field, keyword);
+			}
+			// 分页信息
+			if (page > 0) {
+				sPage.setPage(page);
+			} else {
+				sPage.setPage(1);
+				sPage.setFirst(true);
+			}
+			if (size > 0) {
+				sPage.setSize(size);
+			} else {
+				sPage.setSize(20);
+			}
+
+			TopDocs topDocs;
+			// 如果页码不为1
+			if (sPage.getPage() > 1) {
+				TopDocs previousTopDocs = indexSearcher.search(query, (sPage.getPage() - 1) * sPage.getSize());
+				int totalHits = previousTopDocs.totalHits;
+				ScoreDoc[] previousScoreDocs = previousTopDocs.scoreDocs;
+				if ((sPage.getPage() - 1) * sPage.getSize() >= totalHits) {
+					throw new SearchException("页码过大:元素总数量为" + totalHits);
+				}
+				topDocs = indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], query,
+						sPage.getSize());
+			} else {
+				sPage.setFirst(true);
+				topDocs = indexSearcher.search(query, sPage.getSize());
+			}
+
+			int totalHits = topDocs.totalHits;
+			// 设置总元素个数、页数等信息
+			sPage.setTotalElement(totalHits);
+			int totalPage = (int) Math.ceil(totalHits / (1.0 * sPage.getSize()));
+			sPage.setTotalPage(totalPage);
+			if (totalPage == sPage.getPage()) {
+				sPage.setLast(true);
+			}
+			List<Object> content = new ArrayList<>();
+			for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
+				Document document = indexSearcher.doc(scoreDoc.doc);
+				content.add(DocumentToObjectUtils.toObject(document, tableName));
+			}
+			sPage.setContent(content);
+		} catch (IOException e) {
+			throw new SearchException(e).setDetailedMessage(e);
+		} finally {
+			SearchUtils.releaseIndexSearcher(indexSearcher);
+		}
+		return sPage;
+	}
+
+}

+ 271 - 0
mall-search/src/main/java/com/uas/search/sort/CustomSimilarity.java

@@ -0,0 +1,271 @@
+package com.uas.search.sort;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.lucene.index.FieldInvertState;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.NumericDocValues;
+import org.apache.lucene.search.CollectionStatistics;
+import org.apache.lucene.search.Explanation;
+import org.apache.lucene.search.TermStatistics;
+import org.apache.lucene.search.similarities.Similarity;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.SmallFloat;
+
+/**
+ * @author sunyj
+ * @since 2017年7月3日 下午5:08:11
+ */
+public class CustomSimilarity extends Similarity {
+
+	/** Cache of decoded bytes. */
+	private static final float[] NORM_TABLE = new float[256];
+
+	static {
+		for (int i = 0; i < 256; i++) {
+			NORM_TABLE[i] = SmallFloat.byte315ToFloat((byte) i);
+		}
+	}
+
+	@Override
+	public long computeNorm(FieldInvertState state) {
+		float normValue = lengthNorm(state);
+		return encodeNormValue(normValue);
+	}
+
+	@Override
+	public SimWeight computeWeight(float queryBoost, CollectionStatistics collectionStats,
+			TermStatistics... termStats) {
+		final Explanation idf = termStats.length == 1 ? idfExplain(collectionStats, termStats[0])
+				: idfExplain(collectionStats, termStats);
+		return new IDFStats(collectionStats.field(), idf, queryBoost);
+	}
+
+	@Override
+	public SimScorer simScorer(SimWeight weight, LeafReaderContext context) throws IOException {
+		IDFStats idfstats = (IDFStats) weight;
+		return new TFIDFSimScorer(idfstats, context.reader().getNormValues(idfstats.field));
+	}
+
+	/**
+	 * True if overlap tokens (tokens with a position of increment of zero) are
+	 * discounted from the document's length.
+	 */
+	protected boolean discountOverlaps = true;
+
+	public float lengthNorm(FieldInvertState state) {
+		System.out.println("lengthNorm... ");
+		final int numTerms;
+		if (discountOverlaps)
+			numTerms = state.getLength() - state.getNumOverlap();
+		else
+			numTerms = state.getLength();
+		return state.getBoost() * ((float) (1.0 / Math.sqrt(numTerms)));
+	}
+
+	public final long encodeNormValue(float f) {
+		return SmallFloat.floatToByte315(f);
+	}
+
+	/**
+	 * Computes a score factor for a simple term and returns an explanation for
+	 * that score factor.
+	 * 
+	 * <p>
+	 * The default implementation uses:
+	 * 
+	 * <pre class="prettyprint">
+	 * idf(docFreq, searcher.maxDoc());
+	 * </pre>
+	 * 
+	 * Note that {@link CollectionStatistics#maxDoc()} is used instead of
+	 * {@link org.apache.lucene.index.IndexReader#numDocs()
+	 * IndexReader#numDocs()} because also {@link TermStatistics#docFreq()} is
+	 * used, and when the latter is inaccurate, so is
+	 * {@link CollectionStatistics#maxDoc()}, and in the same direction. In
+	 * addition, {@link CollectionStatistics#maxDoc()} is more efficient to
+	 * compute
+	 * 
+	 * @param collectionStats
+	 *            collection-level statistics
+	 * @param termStats
+	 *            term-level statistics for the term
+	 * @return an Explain object that includes both an idf score factor and an
+	 *         explanation for the term.
+	 */
+	public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats) {
+		final long df = termStats.docFreq();
+		final long max = collectionStats.maxDoc();
+		final float idf = idf(df, max);
+		return Explanation.match(idf, "idf(docFreq=" + df + ", maxDocs=" + max + ")");
+	}
+
+	/**
+	 * Computes a score factor for a phrase.
+	 * 
+	 * <p>
+	 * The default implementation sums the idf factor for each term in the
+	 * phrase.
+	 * 
+	 * @param collectionStats
+	 *            collection-level statistics
+	 * @param termStats
+	 *            term-level statistics for the terms in the phrase
+	 * @return an Explain object that includes both an idf score factor for the
+	 *         phrase and an explanation for each term.
+	 */
+	public Explanation idfExplain(CollectionStatistics collectionStats, TermStatistics termStats[]) {
+		final long max = collectionStats.maxDoc();
+		float idf = 0.0f;
+		List<Explanation> subs = new ArrayList<>();
+		for (final TermStatistics stat : termStats) {
+			final long df = stat.docFreq();
+			final float termIdf = idf(df, max);
+			subs.add(Explanation.match(termIdf, "idf(docFreq=" + df + ", maxDocs=" + max + ")"));
+			idf += termIdf;
+		}
+		return Explanation.match(idf, "idf(), sum of:", subs);
+	}
+
+	/** Implemented as <code>log(numDocs/(docFreq+1)) + 1</code>. */
+	public float idf(long docFreq, long numDocs) {
+		System.out.println("idf... ");
+		return (float) (Math.log(numDocs / (double) (docFreq + 1)) + 1.0);
+	}
+
+	/** Implemented as <code>sqrt(freq)</code>. */
+	public float tf(float freq) {
+		System.out.println("tf... ");
+		return (float) Math.sqrt(freq);
+	}
+
+	/**
+	 * Decodes the norm value, assuming it is a single byte.
+	 * 
+	 * @see #encodeNormValue(float)
+	 */
+	public final float decodeNormValue(long norm) {
+		return NORM_TABLE[(int) (norm & 0xFF)]; // & 0xFF maps negative bytes to
+												// positive above 127
+	}
+
+	/** Implemented as <code>1 / (distance + 1)</code>. */
+	public float sloppyFreq(int distance) {
+		return 1.0f / (distance + 1);
+	}
+
+	/** The default implementation returns <code>1</code> */
+	public float scorePayload(int doc, int start, int end, BytesRef payload) {
+		return 1;
+	}
+
+	private Explanation explainQuery(IDFStats stats) {
+		List<Explanation> subs = new ArrayList<>();
+
+		Explanation boostExpl = Explanation.match(stats.queryBoost, "boost");
+		if (stats.queryBoost != 1.0f)
+			subs.add(boostExpl);
+		subs.add(stats.idf);
+
+		Explanation queryNormExpl = Explanation.match(stats.queryNorm, "queryNorm");
+		subs.add(queryNormExpl);
+
+		return Explanation.match(boostExpl.getValue() * stats.idf.getValue() * queryNormExpl.getValue(),
+				"queryWeight, product of:", subs);
+	}
+
+	private Explanation explainField(int doc, Explanation freq, IDFStats stats, NumericDocValues norms) {
+		Explanation tfExplanation = Explanation.match(tf(freq.getValue()),
+				"tf(freq=" + freq.getValue() + "), with freq of:", freq);
+		Explanation fieldNormExpl = Explanation.match(norms != null ? decodeNormValue(norms.get(doc)) : 1.0f,
+				"fieldNorm(doc=" + doc + ")");
+
+		return Explanation.match(tfExplanation.getValue() * stats.idf.getValue() * fieldNormExpl.getValue(),
+				"fieldWeight in " + doc + ", product of:", tfExplanation, stats.idf, fieldNormExpl);
+	}
+
+	private Explanation explainScore(int doc, Explanation freq, IDFStats stats, NumericDocValues norms) {
+		Explanation queryExpl = explainQuery(stats);
+		Explanation fieldExpl = explainField(doc, freq, stats, norms);
+		if (queryExpl.getValue() == 1f) {
+			return fieldExpl;
+		}
+		return Explanation.match(queryExpl.getValue() * fieldExpl.getValue(),
+				"score(doc=" + doc + ",freq=" + freq.getValue() + "), product of:", queryExpl, fieldExpl);
+	}
+
+	/**
+	 * Collection statistics for the TF-IDF model. The only statistic of
+	 * interest to this model is idf.
+	 */
+	private static class IDFStats extends SimWeight {
+		private final String field;
+		/** The idf and its explanation */
+		private final Explanation idf;
+		private float queryNorm;
+		private float queryWeight;
+		private final float queryBoost;
+		private float value;
+
+		public IDFStats(String field, Explanation idf, float queryBoost) {
+			// TODO: Validate?
+			this.field = field;
+			this.idf = idf;
+			this.queryBoost = queryBoost;
+			this.queryWeight = idf.getValue() * queryBoost; // compute query
+															// weight
+		}
+
+		@Override
+		public float getValueForNormalization() {
+			// TODO: (sorta LUCENE-1907) make non-static class and expose this
+			// squaring via a nice method to subclasses?
+			return queryWeight * queryWeight; // sum of squared weights
+		}
+
+		@Override
+		public void normalize(float queryNorm, float topLevelBoost) {
+			this.queryNorm = queryNorm * topLevelBoost;
+			queryWeight *= this.queryNorm; // normalize query weight
+			value = queryWeight * idf.getValue(); // idf for document
+		}
+	}
+
+	private final class TFIDFSimScorer extends SimScorer {
+		private final IDFStats stats;
+		private final float weightValue;
+		private final NumericDocValues norms;
+
+		TFIDFSimScorer(IDFStats stats, NumericDocValues norms) throws IOException {
+			this.stats = stats;
+			this.weightValue = stats.value;
+			this.norms = norms;
+		}
+
+		@Override
+		public float score(int doc, float freq) {
+			final float raw = tf(freq) * weightValue; // compute tf(f)*weight
+
+			return norms == null ? raw : raw * decodeNormValue(norms.get(doc)); // normalize
+																				// for
+																				// field
+		}
+
+		@Override
+		public float computeSlopFactor(int distance) {
+			return sloppyFreq(distance);
+		}
+
+		@Override
+		public float computePayloadFactor(int doc, int start, int end, BytesRef payload) {
+			return scorePayload(doc, start, end, payload);
+		}
+
+		@Override
+		public Explanation explain(int doc, Explanation freq) {
+			return explainScore(doc, freq, stats, norms);
+		}
+	}
+}

+ 97 - 0
mall-search/src/main/java/com/uas/search/sort/DefaultFieldComparator.java

@@ -0,0 +1,97 @@
+package com.uas.search.sort;
+
+import org.apache.lucene.index.BinaryDocValues;
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.search.SimpleFieldComparator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.uas.search.util.StringUtils;
+
+import java.io.IOException;
+
+/**
+ * 默认排序实现
+ *
+ * @author sunyj
+ * @since 2017年7月3日 下午4:27:54
+ */
+abstract class DefaultFieldComparator extends SimpleFieldComparator<String> {
+
+    protected int AHEAD = -1;
+    protected int BEHIND = 1;
+    protected int PARALLEL = 0;
+    protected int UNKOWN = -2;
+    private String fieldname;
+    private String[] values;
+    private BinaryDocValues binaryDocValues;
+    private String top;
+    private String bottom;
+
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+    DefaultFieldComparator(String fieldname, int numHits) {
+        this.fieldname = fieldname;
+        this.values = new String[numHits];
+    }
+
+    @Override
+    public int compare(int slot1, int slot2) {
+        String value1 = values[slot1];
+        String value2 = values[slot2];
+        return compare(value1, value2);
+    }
+
+    /**
+     * @param str1
+     * @param str2
+     * @return {@link #PARALLEL} 表示并列,{@link #BEHIND} 表示 str1 排在 str2 后面,{@link #AHEAD} 表示排在前面,{@link #UNKOWN} 表示需要进一步判断
+     */
+    protected int compare(String str1, String str2) {
+        if (StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) {
+            return PARALLEL;
+        } else if (StringUtils.isEmpty(str1)) {
+            return BEHIND;
+        } else if (StringUtils.isEmpty(str2)) {
+            return AHEAD;
+        }
+        return UNKOWN;
+    }
+
+    @Override
+    public void copy(int slot, int doc) throws IOException {
+        values[slot] = binaryDocValues == null ? null : binaryDocValues.get(doc).utf8ToString();
+    }
+
+    @Override
+    protected void doSetNextReader(LeafReaderContext context) throws IOException {
+        binaryDocValues = context.reader().getBinaryDocValues(fieldname);
+    }
+
+    @Override
+    public String value(int slot) {
+        return values[slot];
+    }
+
+    @Override
+    public void setBottom(int slot) {
+        bottom = values[slot];
+    }
+
+    @Override
+    public int compareBottom(int doc) throws IOException {
+        String value = binaryDocValues == null ? null : binaryDocValues.get(doc).utf8ToString();
+        return compare(bottom, value);
+    }
+
+    @Override
+    public int compareTop(int doc) throws IOException {
+        String value = binaryDocValues == null ? null : binaryDocValues.get(doc).utf8ToString();
+        return compare(top, value);
+    }
+
+    @Override
+    public void setTopValue(String value) {
+        top = value;
+    }
+
+}

+ 73 - 0
mall-search/src/main/java/com/uas/search/sort/SimilarValuesFieldComparatorSource.java

@@ -0,0 +1,73 @@
+package com.uas.search.sort;
+
+import com.uas.search.util.CharUtils;
+import org.apache.lucene.search.FieldComparator;
+import org.apache.lucene.search.FieldComparatorSource;
+
+import java.io.IOException;
+
+/**
+ * 用于联想词的排序
+ *
+ * @author sunyj
+ * @since 2017年7月3日 下午4:28:03
+ */
+public class SimilarValuesFieldComparatorSource extends FieldComparatorSource {
+    @Override
+    public FieldComparator<?> newComparator(String fieldname, int numHits, int sortPos, boolean reversed)
+            throws IOException {
+        return new DefaultFieldComparator(fieldname, numHits) {
+            @Override
+            protected int compare(String str1, String str2) {
+                int superCompare = super.compare(str1, str2);
+                if (superCompare != UNKOWN) {
+                    return superCompare;
+                }
+
+                // 优先级:数字 > 字母 > 其他字符(优先级视为相等)
+                if (str1.length() > str2.length()) {
+                    return 1;
+                } else if (str1.length() < str2.length()) {
+                    return -1;
+                }
+
+                char[] array1 = str1.toCharArray();
+                char[] array2 = str2.toCharArray();
+                for (int i = 0; i < array1.length; i++) {
+                    char c1 = array1[i];
+                    char c2 = array2[i];
+                    // 优先级:数字 > 字母 > 其他字符(优先级视为相等)
+                    if (CharUtils.isNumber(c1)) {
+                        if (CharUtils.isNumber(c2)) {
+                            // 均为数字,数字越小优先级越高
+                            if (c1 > c2) {
+                                return 1;
+                            } else if (c1 < c2) {
+                                return -1;
+                            }
+                            continue;
+                        }
+                        return -1;
+                    } else if (CharUtils.isCharacter(c1)) {
+                        if (CharUtils.isNumber(c2)) {
+                            return 1;
+                        } else if (CharUtils.isCharacter(c2)) {
+                            c1 = CharUtils.toUpper(c1);
+                            c2 = CharUtils.toUpper(c2);
+                            // 均为字母,值越小优先级越高
+                            if (c1 > c2) {
+                                return 1;
+                            } else if (c1 < c2) {
+                                return -1;
+                            }
+                            continue;
+                        }
+                        return -1;
+                    }
+                }
+                return 0;
+            }
+        };
+    }
+
+}

+ 127 - 0
mall-search/src/main/java/com/uas/search/sort/StringFieldComparatorSource.java

@@ -0,0 +1,127 @@
+package com.uas.search.sort;
+
+import com.uas.search.util.CharUtils;
+import org.apache.lucene.search.FieldComparator;
+import org.apache.lucene.search.FieldComparatorSource;
+import com.uas.search.util.StringUtils;
+
+import java.io.IOException;
+
+/**
+ * 用于 StringField 的排序
+ *
+ * @author sunyj
+ * @since 2017/11/23 17:27
+ */
+public class StringFieldComparatorSource extends FieldComparatorSource {
+
+    private String keyword;
+
+    public StringFieldComparatorSource(String keyword) {
+        this.keyword = keyword;
+    }
+
+    @Override
+    public FieldComparator<?> newComparator(final String fieldname, int numHits, int sortPos, boolean reversed)
+            throws IOException {
+        return new DefaultFieldComparator(fieldname, numHits) {
+            @Override
+            protected int compare(String str1, String str2) {
+                int superCompare = super.compare(str1, str2);
+                if (superCompare != UNKOWN || StringUtils.isEmpty(keyword)) {
+                    return superCompare;
+                }
+
+                str1 = str1.toLowerCase();
+                str2 = str2.toLowerCase();
+                keyword = keyword.toLowerCase();
+
+                int index1 = str1.indexOf(keyword);
+                int index2 = str2.indexOf(keyword);
+                // 一方包含关键词,另一方不包含时,该方排序靠前
+                if (index1 >= 0) {
+                    if (index2 >= 0) {
+                        // 均包含关键词时,比较关键词在字符串中的位置,位置越靠前,最终排序越靠前
+                        if (index1 < index2) {
+                            return AHEAD;
+                        } else if (index1 > index2) {
+                            return BEHIND;
+                        } else {
+                            // 关键词在字符串中的位置相同时,比较字符串长度,长度较短,则排序靠前
+                            int compareLength = compareLength(str1, str2);
+                            return compareLength != PARALLEL ? compareLength : compareCharacter(str1, str2);
+                        }
+                    } else {
+                        return AHEAD;
+                    }
+                } else {
+                    if (index2 >= 0) {
+                        return BEHIND;
+                    } else {
+                        // 均不包含关键词时,比较字符串长度,长度较短,则排序靠前
+                        int compareLength = compareLength(str1, str2);
+                        return compareLength != PARALLEL ? compareLength : compareCharacter(str1, str2);
+                    }
+                }
+            }
+
+            /**
+             * 比较长度,长度较短,则排序靠前
+             */
+            private int compareLength(String str1, String str2) {
+                int length1 = str1.length();
+                int length2 = str2.length();
+                if (length1 < length2) {
+                    return AHEAD;
+                } else if (length1 > length2) {
+                    return BEHIND;
+                } else {
+                    return PARALLEL;
+                }
+            }
+
+            /**
+             * 比较字母顺序,优先级:字母 > 其他字符(优先级视为相等) > 数字
+             */
+            private int compareCharacter(String str1, String str2) {
+                char[] array1 = str1.toLowerCase().toCharArray();
+                char[] array2 = str2.toLowerCase().toCharArray();
+                for (int i = 0; i < array1.length; i++) {
+                    char c1 = array1[i];
+                    char c2 = array2[i];
+                    // 字母 > 其他字符(优先级视为相等) > 数字
+                    if (CharUtils.isCharacter(c1)) {
+                        if (CharUtils.isCharacter(c2)) {
+                            // 均为字母,值越小优先级越高
+                            if (c1 < c2) {
+                                return AHEAD;
+                            } else if (c1 > c2) {
+                                return BEHIND;
+                            }
+                        } else {
+                            return AHEAD;
+                        }
+                    } else if (CharUtils.isNumber(c1)) {
+                        if (CharUtils.isNumber(c2)) {
+                            // 均为数字,数字越小优先级越高
+                            if (c1 > c2) {
+                                return BEHIND;
+                            } else if (c1 < c2) {
+                                return AHEAD;
+                            }
+                        } else {
+                            return BEHIND;
+                        }
+                    } else {
+                        if (CharUtils.isCharacter(c2)) {
+                            return BEHIND;
+                        } else if (CharUtils.isNumber(c2)) {
+                            return AHEAD;
+                        }
+                    }
+                }
+                return PARALLEL;
+            }
+        };
+    }
+}

+ 389 - 0
mall-search/src/main/java/com/uas/search/support/DownloadHelper.java

@@ -0,0 +1,389 @@
+package com.uas.search.support;
+
+import com.uas.search.constant.model.PageInfo;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.util.ArrayUtils;
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.FileUtils;
+import com.uas.search.util.SearchUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
+
+import static com.uas.search.service.impl.IndexServiceImpl.PAGE_SIZE;
+import static com.uas.search.service.impl.IndexServiceImpl.SINGLE_FILE_MAX_SIZE;
+import static com.uas.search.support.DownloadHelper.ValidateResult.*;
+
+
+/**
+ * 下载数据的辅助类
+ *
+ * @author sunyj
+ * @since 2017/11/25 17:30
+ */
+public class DownloadHelper<T> {
+
+    /**
+     * 线程最小数量
+     */
+    private final int MIN_THREAD_SIZE = 1;
+
+    /**
+     * 线程最大数量
+     */
+    private final int MAX_THREAD_SIZE = 10000;
+
+    /**
+     * 默认开始的文件
+     */
+    private final int DEFAULT_START_FILE_INDEX = 1;
+
+    /**
+     * 默认结束的文件
+     */
+    private final int DEFAULT_END_FILE_INDEX = 1024 * 1024 * 1024;
+
+    /**
+     * 线程数量
+     */
+    private Integer threadSize;
+
+    /**
+     * 开始的文件
+     */
+    private Integer startFileIndex;
+
+    /**
+     * 结束的文件
+     */
+    private Integer endFileIndex;
+
+    /**
+     * 要下载的表
+     */
+    private String tableName;
+
+    /**
+     * 排序字段
+     */
+    private String sortField;
+
+    /**
+     * dao
+     */
+    private JpaRepository<T, Long> dao;
+
+    /**
+     * 下载的实现
+     */
+    private DownloadService<T> downloadService;
+
+    /**
+     * 下载完成后,是否对结果进行校验
+     */
+    private ValidateResult validateResult;
+
+    /**
+     * 线程管理
+     */
+    private ExecutorService executorService;
+
+    /**
+     * 收集执行结果
+     */
+    private CompletionService<Long> completionService;
+
+    /**
+     * 要下载表的数据总行数
+     */
+    private Long totalElements;
+
+    /**
+     * 执行结果
+     */
+    private Long result;
+
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * @param threadSize      线程数量
+     * @param startFileIndex  开始的文件
+     * @param endFileIndex    结束的文件
+     * @param tableName       要下载的表
+     * @param sortField       排序字段
+     * @param dao             dao
+     * @param downloadService 下载的实现
+     */
+    public DownloadHelper(Integer threadSize, Integer startFileIndex, Integer endFileIndex, String tableName, String sortField, JpaRepository<T, Long> dao, DownloadService<T> downloadService) {
+        this(threadSize, startFileIndex, endFileIndex, tableName, sortField, dao, downloadService, CURRENT);
+    }
+
+    /**
+     * @param threadSize      线程数量
+     * @param startFileIndex  开始的文件
+     * @param endFileIndex    结束的文件
+     * @param tableName       要下载的表
+     * @param sortField       排序字段
+     * @param dao             dao
+     * @param downloadService 下载的实现
+     * @param validateResult  下载完成后,是否对结果进行校验
+     */
+    public DownloadHelper(Integer threadSize, Integer startFileIndex, Integer endFileIndex, String tableName, String sortField, JpaRepository<T, Long> dao, DownloadService<T> downloadService, ValidateResult validateResult) {
+        if (threadSize == null || threadSize < MIN_THREAD_SIZE || threadSize > MAX_THREAD_SIZE) {
+            throw new IllegalArgumentException("threadSize is between " + MIN_THREAD_SIZE + " and " + MAX_THREAD_SIZE);
+        }
+        if (downloadService == null) {
+            throw new IllegalArgumentException("runnable is null");
+        }
+        this.threadSize = threadSize;
+        this.downloadService = downloadService;
+        this.startFileIndex = startFileIndex == null || startFileIndex < DEFAULT_START_FILE_INDEX ? DEFAULT_START_FILE_INDEX : startFileIndex;
+        this.endFileIndex = endFileIndex == null || endFileIndex < DEFAULT_START_FILE_INDEX ? DEFAULT_END_FILE_INDEX : endFileIndex;
+        this.tableName = tableName;
+        this.sortField = sortField;
+        this.dao = dao;
+        this.validateResult = validateResult;
+        start();
+    }
+
+    /**
+     * 开始下载
+     */
+    private void start() {
+        executorService = Executors.newCachedThreadPool();
+        completionService = new ExecutorCompletionService<>(executorService);
+        if (startFileIndex == DEFAULT_START_FILE_INDEX && endFileIndex == DEFAULT_END_FILE_INDEX) {
+            // 删除旧的文件
+            FileUtils.deleteSubFiles(new File(SearchUtils.getDataPath(tableName)));
+        }
+        getTotalElements();
+        logger.info(tableName + " 发现数据 " + totalElements + " 条");
+        // 线程数量不可高于下载的文件数量
+        threadSize = threadSize <= endFileIndex - startFileIndex + 1 ? threadSize : endFileIndex - startFileIndex + 1;
+        for (int i = 0; i < threadSize; i++) {
+            completionService.submit(getTask(i, threadSize, startFileIndex + i, endFileIndex, tableName, sortField, dao));
+        }
+        waitResult();
+        if (validateResult != null && validateResult != NONE) {
+            // 对结果进行校验,只校验一定的次数,防止因特殊原因,某些数据始终无法成功下载,陷入死循环
+            int retry = 1;
+            List<Integer> missingFiles;
+            logger.error("校验下载结果");
+            while (retry <= 5 && !CollectionUtils.isEmpty(missingFiles = validateResult())) {
+                logger.error("第 " + retry + " 次校验,下载遗失的文件:" + missingFiles);
+                List<DownloadHelper<T>> downloadHelpers = new ArrayList<>();
+                for (Integer missingFile : missingFiles) {
+                    downloadHelpers.add(new DownloadHelper<T>(1, missingFile, missingFile, tableName, sortField, dao, downloadService, NONE));
+                }
+                for (DownloadHelper downloadHelper : downloadHelpers) {
+                    downloadHelper.getResult();
+                }
+                retry++;
+            }
+            if (!CollectionUtils.isEmpty(missingFiles = validateResult())) {
+                throw new IllegalStateException("部分数据下载失败:" + missingFiles);
+            }
+        }
+    }
+
+    /**
+     * 获取要下载表的数据总行数
+     */
+    private void getTotalElements() {
+        PageParams pageParams = new PageParams();
+        pageParams.setPage(startFileIndex);
+        pageParams.setSize(PAGE_SIZE);
+        PageInfo pageInfo = new PageInfo(pageParams);
+        totalElements = dao.findAll(pageInfo).getTotalElements();
+    }
+
+    /**
+     * 获取任务
+     *
+     * @param id             线程 id
+     * @param step           新增文件时,文件 id 的自增步长,(即线程数量)
+     * @param startFileIndex 开始的文件
+     * @param endFileIndex   结束的文件
+     * @param tableName      要下载的表
+     * @param sortField      排序字段
+     * @param dao            dao
+     * @return 任务
+     */
+    private Callable<Long> getTask(final int id, final int step, final int startFileIndex, final int endFileIndex, final String tableName, final String sortField, final JpaRepository<T, Long> dao) {
+        return new Callable<Long>() {
+            @Override
+            public Long call() throws Exception {
+                return downloadService.download(id, step, startFileIndex, endFileIndex, tableName, sortField, dao);
+            }
+        };
+    }
+
+    /**
+     * 等待执行结果
+     */
+    private void waitResult() {
+        if (executorService.isShutdown() || executorService.isTerminated()) {
+            throw new IllegalStateException("结果已返回,不可再次获取");
+        }
+        result = 0L;
+        for (int i = 0; i < threadSize; i++) {
+            try {
+                Future<Long> future = completionService.take();
+                Long count = future.get();
+                result += (count == null ? 0L : count);
+            } catch (InterruptedException | ExecutionException e) {
+                throw new IllegalStateException("获取下载结果失败", e);
+            }
+        }
+        executorService.shutdown();
+    }
+
+    /**
+     * 获取执行结果
+     *
+     * @return 下载的总数量
+     */
+    public long getResult() {
+        if (result == null) {
+            waitResult();
+        }
+        return result;
+    }
+
+    /**
+     * 校验结果
+     *
+     * @return 未下载成功的文件
+     */
+    private List<Integer> validateResult() {
+        int totalFiles = (int) Math.ceil(totalElements / (1.0 * SINGLE_FILE_MAX_SIZE));
+        // 期待的起始、结束文件
+        final int expectStartFileIndex;
+        final int expectEndFileIndex;
+        if (validateResult == CURRENT) {
+            // 校验本次下载的文件
+            expectStartFileIndex = startFileIndex;
+            expectEndFileIndex = endFileIndex < totalFiles ? endFileIndex : totalFiles;
+        } else if (validateResult == ALL) {
+            // 校验所有文件
+            expectStartFileIndex = DEFAULT_START_FILE_INDEX;
+            expectEndFileIndex = totalFiles;
+        } else {
+            return null;
+        }
+        List<Integer> missingFiles = new ArrayList<>();
+
+        File[] files = new File(SearchUtils.getDataPath(tableName)).listFiles(new FileFilter() {
+            @Override
+            public boolean accept(File pathname) {
+                // 只保留指定格式的文件,类似 0000000323.txt
+                if (!pathname.isFile() || !pathname.getName().matches("[0]*?[\\d]+?.txt")) {
+                    return false;
+                }
+                int id = parseName(pathname);
+                return id >= expectStartFileIndex && id <= expectEndFileIndex;
+            }
+        });
+        if (ArrayUtils.isEmpty(files)) {
+            return missingFiles;
+        }
+        // 将文件按名称排序
+        sort(files);
+        // 默认把所有文件视为未下载成功
+        for (int i = expectStartFileIndex; i <= expectEndFileIndex; i++) {
+            missingFiles.add(i);
+        }
+        result = 0L;
+        for (File file : files) {
+            // 读取文件,如果文件行数正常(如果不是最后一个文件,行数必须超过限定值),根据文件 id 将其从 missingFiles 中移出
+            int id = parseName(file);
+            int readLineCount = readLineCount(file);
+            if (readLineCount >= SINGLE_FILE_MAX_SIZE || (id == totalFiles && readLineCount > 0)) {
+                result += readLineCount;
+                missingFiles.remove(new Integer(id));
+            }
+        }
+        return missingFiles;
+    }
+
+    /**
+     * 获取文件行数
+     *
+     * @param file 文件
+     * @return 行数
+     */
+    private int readLineCount(File file) {
+        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
+            int count = 0;
+            while (bufferedReader.readLine() != null) {
+                count++;
+            }
+            return count;
+        } catch (IOException e) {
+            throw new IllegalStateException("校验下载的文件时,读取文件出错", e);
+        }
+    }
+
+    /**
+     * 根据文件的名称进行排序(增序)
+     *
+     * @param files
+     */
+    public void sort(File[] files) {
+        int N = files.length;
+        int h = 1;
+        while (h < N / 3) {
+            h = h * 3 + 1;
+        }
+        while (h >= 1) {
+            for (int i = h; i < N; i++) {
+                for (int j = i; j >= h && less(files[j], files[j - h]); j -= h) {
+                    exchange(files, j, j - h);
+                }
+            }
+            h /= 3;
+        }
+    }
+
+    private boolean less(File f1, File f2) {
+        return parseName(f1) - parseName(f2) < 0;
+    }
+
+    /**
+     * 解析文件名称为数字
+     */
+    private int parseName(File f) {
+        return Integer.parseInt(f.getName().replaceAll(".txt", ""));
+    }
+
+    /**
+     * 交换位置
+     */
+    private void exchange(File[] files, int i, int j) {
+        File temp = files[i];
+        files[i] = files[j];
+        files[j] = temp;
+    }
+
+    public enum ValidateResult {
+        /**
+         * 不校验
+         */
+        NONE,
+        /**
+         * 校验所有文件
+         */
+        ALL,
+
+        /**
+         * 只校验本次下载的文件
+         */
+        CURRENT
+    }
+
+}

+ 144 - 0
mall-search/src/main/java/com/uas/search/support/DownloadService.java

@@ -0,0 +1,144 @@
+package com.uas.search.support;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.constant.model.PageInfo;
+import com.uas.search.constant.model.PageParams;
+import com.uas.search.service.impl.IndexServiceImpl;
+import com.uas.search.util.CollectionUtils;
+import com.uas.search.util.SearchUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.Date;
+import java.util.List;
+
+import static com.uas.search.service.impl.IndexServiceImpl.PAGE_SIZE;
+import static com.uas.search.service.impl.IndexServiceImpl.SINGLE_FILE_MAX_SIZE;
+
+/**
+ * 下载线程
+ *
+ * @author sunyj
+ * @since 2017/11/25 18:24
+ */
+public class DownloadService<T> {
+
+    /**
+     * @param id             线程 id
+     * @param step           新增文件时,文件 id 的自增步长,(即线程数量)
+     * @param startFileIndex 开始的文件
+     * @param endFileIndex   结束的文件
+     * @param tableName      要下载的表
+     * @param sortField      排序字段
+     * @param dao            dao
+     * @return 下载的数量
+     */
+    public long download(int id, int step, int startFileIndex, int endFileIndex, String tableName, String sortField, JpaRepository<T, Long> dao) {
+        long size = 0L;
+        Logger logger = LoggerFactory.getLogger(getClass());
+        String name = "Thread_" + id + "_" + startFileIndex + "-" + endFileIndex;
+        File file = null;
+        PrintWriter printWriter = null;
+        try {
+            if (SINGLE_FILE_MAX_SIZE % PAGE_SIZE != 0) {
+                throw new IllegalArgumentException("SINGLE_FILE_MAX_SIZE 并非 PAGE_SIZE 的整数倍:" + SINGLE_FILE_MAX_SIZE + "/" + PAGE_SIZE);
+            }
+            if (startFileIndex > endFileIndex) {
+                logger.info(name + " startFileIndex 超过 " + endFileIndex);
+                return size;
+            }
+            Long startTime = new Date().getTime();
+            logger.info(name + " 下载 " + tableName + "...");
+
+            Sort sort = new Sort(Sort.Direction.ASC, sortField);
+            // 分页获取数据
+            PageParams pageParams = new PageParams();
+            pageParams.setPage((startFileIndex - 1) * SINGLE_FILE_MAX_SIZE / PAGE_SIZE + 1);
+            pageParams.setSize(IndexServiceImpl.PAGE_SIZE);
+            PageInfo pageInfo = new PageInfo(pageParams, sort);
+            Page<T> pageResult = dao.findAll(pageInfo);
+
+            int totalFiles = (int) Math.ceil(pageResult.getTotalElements() / (1.0 * SINGLE_FILE_MAX_SIZE));
+            if (startFileIndex > totalFiles) {
+                logger.info(name + " startFileIndex 超过 " + totalFiles);
+                return size;
+            }
+            // 实际的最后一个文件
+            endFileIndex = endFileIndex < totalFiles ? endFileIndex : totalFiles;
+
+            File dataDir = new File(SearchUtils.getDataPath(tableName));
+            if (!dataDir.exists()) {
+                if (!dataDir.mkdirs()) {
+                    logger.error("创建文件夹失败:" + dataDir.getPath());
+                }
+            }
+            int fileIndex = startFileIndex;
+            String fileName = String.format("%010d", fileIndex) + ".txt";
+            file = new File(dataDir, fileName);
+            printWriter = new PrintWriter(file);
+            int count = 0;
+            while (endFileIndex >= fileIndex) {
+                // 数据以 JSON 格式写入文件
+                List<T> content = pageResult.getContent();
+                for (T element : content) {
+                    println(printWriter, element);
+                }
+                count += content.size();
+                size += content.size();
+                logger.info(name + " " + fileName + " - Downloaded..................." + size);
+
+                // 一个文件的函数达到指定值,写入新文件
+                if (count == SINGLE_FILE_MAX_SIZE) {
+                    count = 0;
+                    close(printWriter);
+                    fileIndex += step;
+                    pageParams.setPage(pageParams.getPage() + step * SINGLE_FILE_MAX_SIZE / PAGE_SIZE);
+                    // 不创建多余文件
+                    if (endFileIndex >= fileIndex) {
+                        fileName = String.format("%010d", fileIndex) + ".txt";
+                        file = new File(dataDir, fileName);
+                        printWriter = new PrintWriter(file);
+                    }
+                } else {
+                    pageParams.setPage(pageParams.getPage() + 1);
+                }
+                pageInfo = new PageInfo(pageParams, sort);
+                pageResult = dao.findAll(pageInfo);
+                // 已经下载到最后一页
+                if (CollectionUtils.isEmpty(pageResult.getContent())) {
+                    break;
+                }
+            }
+            logger.info(String.format("%s 下载完成,耗时%.2fs\n ", name, (new Date().getTime() - startTime) / 1000.0));
+        } catch (Throwable e) {
+            close(printWriter);
+            // 出现错误时,删除最近出错的文件
+            if (file != null) {
+                if (!file.delete()) {
+                    logger.error("出错时,清理出错文件失败:" + file.getPath());
+                }
+            }
+            logger.error(name + " " + tableName + " 下载失败", e);
+        } finally {
+            close(printWriter);
+        }
+        return size;
+    }
+
+    protected void println(PrintWriter printWriter, T element) {
+        printWriter.println(JSONObject.toJSONString(element));
+    }
+
+    private void close(PrintWriter printWriter) {
+        if (printWriter != null) {
+            printWriter.flush();
+            printWriter.close();
+        }
+    }
+
+}

+ 245 - 0
mall-search/src/main/java/com/uas/search/support/IndexSearcherManager.java

@@ -0,0 +1,245 @@
+package com.uas.search.support;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.store.NIOFSDirectory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.uas.search.util.SearchUtils;
+import com.uas.search.util.StringUtils;
+
+/**
+ * 将索引加入缓存,对IndexSearcher进行管理
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午8:50:55
+ */
+public class IndexSearcherManager {
+
+	private static volatile IndexSearcherManager instance;
+
+	/**
+	 * 用于存放指定表的索引IndexSearcher
+	 */
+	private Map<String, IndexSearcher> indexSearchers = new ConcurrentHashMap<>();
+
+	/**
+	 * 标识指定表的IndexSearcher是否正在重新打开
+	 */
+	private Map<String, Boolean> reopening = new ConcurrentHashMap<>();
+
+	/**
+	 * 标识在后台是否存在线程在等待刷新缓存中的索引
+	 */
+	private Map<String, Boolean> waitingFlushCache = new ConcurrentHashMap<>();
+
+	private static Logger logger = LoggerFactory.getLogger(IndexSearcherManager.class);
+
+	private IndexSearcherManager() {
+		List<String> tableNames = SearchUtils.getTableNames();
+		for (String tableName : tableNames) {
+			reopening.put(tableName, false);
+		}
+	}
+
+	public static IndexSearcherManager getInstance() {
+		if (instance == null) {
+			synchronized (IndexSearcherManager.class) {
+				if (instance == null) {
+					instance = new IndexSearcherManager();
+				}
+			}
+		}
+		return instance;
+	}
+
+	/**
+	 * 对指定表的IndexSearcher进行初始化操作
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return 指定表的FSDirectory
+	 */
+	private synchronized void warm(String tableName) {
+		IndexSearcher indexSearcher = indexSearchers.get(tableName);
+		// 不为空的话说明索引已成功加载
+		if (indexSearcher == null) {
+			String indexPath = SearchUtils.getIndexPath(tableName);
+			FSDirectory directory = null;
+			// 本来NIOFSDirectory速度更快,但因为jre的bug,在Windows平台上,其性能很差
+			try {
+				if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
+					directory = FSDirectory.open(Paths.get(indexPath));
+				} else {
+					directory = NIOFSDirectory.open(Paths.get(indexPath));
+				}
+
+				File[] files = directory.getDirectory().toFile().listFiles();
+				// 路径不为空文件夹
+				if (files.length != 0) {
+					try {
+						indexSearcher = new IndexSearcher(DirectoryReader.open(directory));
+						// indexSearcher.setSimilarity(new CustomSimilarity());
+						indexSearchers.put(tableName, indexSearcher);
+					} catch (IOException e) {
+						logger.error("", e);
+					}
+				} else {
+					logger.error("索引文件不存在:" + tableName);
+				}
+			} catch (IOException e) {
+				logger.error("索引路径打开失败:" + indexPath, e);
+			}
+		}
+	}
+
+	/**
+	 * 内存中指定表的IndexReader(可能需要)更新,根据版本号确定是否更新
+	 * 
+	 * @param tableName
+	 *            表名
+	 */
+	public void maybeReopen(String tableName) {
+		if (StringUtils.isEmpty(tableName)) {
+			return;
+		}
+		startReopen(tableName);
+
+		try {
+			IndexSearcher currentSearcher = indexSearchers.get(tableName);
+			if (currentSearcher == null) {
+				return;
+			}
+			IndexSearcher searcher = get(tableName);
+			try {
+				// 本地索引有更改,更新IndexReader
+				DirectoryReader newReader = DirectoryReader.openIfChanged((DirectoryReader) searcher.getIndexReader());
+				if (newReader != null) {
+					IndexSearcher newSearcher = new IndexSearcher(newReader);
+					// newSearcher.setSimilarity(new CustomSimilarity());
+					updateIndexSearcher(tableName, newSearcher);
+				}
+			} catch (IOException e) {
+				logger.error("", e);
+			}
+			release(searcher);
+		} finally {
+			doneReopen(tableName);
+		}
+
+	}
+
+	private synchronized void startReopen(String tableName) {
+		while (reopening.get(tableName)) {
+			try {
+				wait();
+			} catch (InterruptedException e) {
+				logger.error("", e);
+			}
+		}
+		reopening.put(tableName, true);
+	}
+
+	private synchronized void doneReopen(String tableName) {
+		reopening.put(tableName, false);
+		notifyAll();
+	}
+
+	/**
+	 * 得到指定表的IndexSearcher
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return
+	 */
+	public synchronized IndexSearcher get(String tableName) {
+		if (StringUtils.isEmpty(tableName)) {
+			return null;
+		}
+		warm(tableName);
+		IndexSearcher currentSearcher = indexSearchers.get(tableName);
+		if (currentSearcher != null) {
+			currentSearcher.getIndexReader().incRef();
+		}
+		return currentSearcher;
+	}
+
+	/**
+	 * 释放indexSearcher
+	 * 
+	 * @param indexSearcher
+	 */
+	public synchronized void release(IndexSearcher indexSearcher) {
+		if (indexSearcher != null) {
+			try {
+				// 对indexReader的引用减1,为0时成为垃圾
+				indexSearcher.getIndexReader().decRef();
+			} catch (IOException e) {
+				logger.error("", e);
+			}
+		}
+	}
+
+	/**
+	 * 更新指定表的IndexSearcher
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param newIndexSearcher
+	 *            新IndexSearcher
+	 */
+	private synchronized void updateIndexSearcher(String tableName, IndexSearcher newIndexSearcher) {
+		IndexSearcher currentSearcher = indexSearchers.get(tableName);
+		release(currentSearcher);
+		indexSearchers.put(tableName, newIndexSearcher);
+	}
+
+	/**
+	 * 刷新缓存中的索引
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param indexWriter
+	 * @param delay
+	 *            延迟一定时间刷新/ms,可为空,默认延迟3000ms
+	 */
+	public void flushCache(final String tableName, final IndexWriter indexWriter, final Long delay) {
+		if (StringUtils.isEmpty(tableName)) {
+			return;
+		}
+		Boolean waiting = waitingFlushCache.get(tableName);
+		// 有等待的线程,则不增加线程
+		if (waiting == null || !waiting.booleanValue()) {
+			final long delayCopy = (delay == null || delay.longValue() < 0) ? 3000 : delay.longValue();
+			new Thread(new Runnable() {
+				@Override
+				public void run() {
+					try {
+						// 延迟一定时间再提交更改(提高更新索引的速度)并刷新缓存(提高搜索速度)
+						Thread.sleep(delayCopy);
+						// 提交更改后再刷新
+						indexWriter.commit();
+						maybeReopen(tableName);
+					} catch (InterruptedException | IOException e) {
+						logger.error("", e);
+					} finally {
+						// 线程执行完毕,无论是否成功,均将状态改为false
+						waitingFlushCache.put(tableName, false);
+					}
+				}
+			}).start();
+			// 将状态改为true
+			waitingFlushCache.put(tableName, true);
+		}
+	}
+}

+ 113 - 0
mall-search/src/main/java/com/uas/search/support/IndexWriterManager.java

@@ -0,0 +1,113 @@
+package com.uas.search.support;
+
+import com.uas.search.analyzer.IKAnalyzer;
+import com.uas.search.util.SearchUtils;
+import com.uas.search.util.StringUtils;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.store.NIOFSDirectory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 对IndexWriter进行管理,防止同时有多个方法对索引进行修改,抛出LockObtainFailedException异常
+ * 
+ * @author sunyj
+ * @since 2016年7月7日 下午8:50:04
+ */
+public class IndexWriterManager {
+
+	/**
+	 * 用于存放指定表的索引IndexWriter
+	 */
+	private Map<String, IndexWriter> indexWriters = new ConcurrentHashMap<>();
+
+	/**
+	 * 标识指定表的索引是否正在使用
+	 */
+	private Map<String, Boolean> using = new ConcurrentHashMap<>();
+
+	private static Logger logger = LoggerFactory.getLogger(IndexWriterManager.class);
+
+	public IndexWriterManager() {
+		List<String> tableNames = SearchUtils.getTableNames();
+		for (String tableName : tableNames) {
+			using.put(tableName, false);
+		}
+	}
+
+	/**
+	 * 得到指定表的IndexWriter,用完后需调用release释放IndexWriter
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return IndexWriter
+	 * @throws InterruptedException
+	 */
+	public synchronized IndexWriter get(String tableName) throws InterruptedException {
+		if (StringUtils.isEmpty(tableName)) {
+			return null;
+		}
+		startUsing(tableName);
+		IndexWriter indexWriter = indexWriters.get(tableName);
+		if (indexWriter == null) {
+			// 此处用IK
+			IndexWriterConfig config = new IndexWriterConfig(new IKAnalyzer());
+			// 获取表对应的索引目录
+			String indexPath = SearchUtils.getIndexPath(tableName);
+			try {
+				FSDirectory directory = null;
+				// 本来NIOFSDirectory速度更快,但因为jre的bug,在Windows平台上,其性能很差
+				if (System.getProperty("os.name").toLowerCase().startsWith("win")) {
+					directory = FSDirectory.open(Paths.get(indexPath));
+				} else {
+					directory = NIOFSDirectory.open(Paths.get(indexPath));
+				}
+				indexWriter = new IndexWriter(directory, config);
+				indexWriters.put(tableName, indexWriter);
+				return indexWriter;
+			} catch (IOException e) {
+				logger.error("索引路径打开失败:" + indexPath, e);
+				return null;
+			}
+		} else {
+			return indexWriter;
+		}
+	}
+
+	/**
+	 * 释放对指定表的indexWriter的使用
+	 * 
+	 * @param tableName
+	 *            表名
+	 */
+	public synchronized void release(String tableName) {
+		if (StringUtils.isEmpty(tableName)) {
+			throw new NullPointerException("参数不能为空:tableName");
+		}
+		// indexWriter一直保持打开状态,也只存在一个实例,
+		// indexWriter用完后只是释放对其的占用,不会进行close
+		// TODO close方法有时会卡住(多出现在插入记录之后),暂时还未找到原因
+		doneUsing(tableName);
+	}
+
+	private synchronized void startUsing(String tableName) throws InterruptedException {
+		while (using.get(tableName)) {
+			wait();
+		}
+		using.put(tableName, true);
+	}
+
+	private synchronized void doneUsing(String tableName) {
+		using.put(tableName, false);
+		notifyAll();
+	}
+
+}

+ 46 - 0
mall-search/src/main/java/com/uas/search/util/ArrayUtils.java

@@ -0,0 +1,46 @@
+package com.uas.search.util;
+
+/**
+ * 数组工具类
+ * 
+ * @author sunyj
+ * @since 2017年4月21日 下午9:01:53
+ */
+public class ArrayUtils {
+
+	public static boolean isEmpty(Object[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(long[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(int[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(short[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(char[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(byte[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(double[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(float[] array) {
+		return array == null || array.length == 0;
+	}
+
+	public static boolean isEmpty(boolean[] array) {
+		return array == null || array.length == 0;
+	}
+}

+ 27 - 0
mall-search/src/main/java/com/uas/search/util/CharUtils.java

@@ -0,0 +1,27 @@
+package com.uas.search.util;
+
+public class CharUtils {
+
+	public static boolean isNumber(char c) {
+		return c >= 48 && c <= 57;
+	}
+
+	public static boolean isCharacter(char c) {
+		return isUpperCharacter(c) || isLowerCharacter(c);
+	}
+
+	public static boolean isUpperCharacter(char c) {
+		return c >= 65 && c <= 90;
+	}
+
+	public static boolean isLowerCharacter(char c) {
+		return c >= 97 && c <= 122;
+	}
+
+	public static char toUpper(char c) {
+		if (isLowerCharacter(c)) {
+			return (char) (c - 32);
+		}
+		return c;
+	}
+}

+ 21 - 0
mall-search/src/main/java/com/uas/search/util/CollectionUtils.java

@@ -0,0 +1,21 @@
+package com.uas.search.util;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * 集合工具类
+ * 
+ * @author sunyj
+ * @since 2017年4月21日 下午9:43:31
+ */
+public class CollectionUtils {
+
+	public static boolean isEmpty(Collection<?> coll) {
+		return (coll == null || coll.isEmpty());
+	}
+
+	public static boolean isEmpty(Map<?, ?> map) {
+		return (map == null || map.isEmpty());
+	}
+}

+ 77 - 0
mall-search/src/main/java/com/uas/search/util/ContextUtils.java

@@ -0,0 +1,77 @@
+package com.uas.search.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.event.ApplicationPreparedEvent;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationListener;
+
+/**
+ * spring容器上下文对象
+ * 
+ * @author yingp
+ *
+ */
+public class ContextUtils implements ApplicationListener<ApplicationPreparedEvent> {
+
+	private static ApplicationContext applicationContext;
+
+	private static Logger logger = LoggerFactory.getLogger(ContextUtils.class);
+
+	@Override
+	public void onApplicationEvent(ApplicationPreparedEvent event) {
+		synchronized (ContextUtils.class) {
+			logger.debug("setApplicationContext, notifyAll");
+			ContextUtils.applicationContext = event.getApplicationContext();
+			ContextUtils.class.notifyAll();
+		}
+	}
+
+	public static ApplicationContext getApplicationContext() {
+		synchronized (ContextUtils.class) {
+			while (applicationContext == null) {
+				try {
+					logger.debug("getApplicationContext, wait...");
+					ContextUtils.class.wait(60000);
+					if (applicationContext == null) {
+						logger.warn("Have been waiting for ApplicationContext to be set for 1 minute", new Exception());
+					}
+				} catch (InterruptedException ex) {
+					logger.debug("getApplicationContext, wait interrupted");
+				}
+			}
+			return applicationContext;
+		}
+	}
+
+	/**
+	 * 获取bean
+	 * 
+	 * @param name
+	 * @return
+	 */
+	public static Object getBean(String name) {
+		return getApplicationContext().getBean(name);
+	}
+
+	/**
+	 * 获取bean
+	 * 
+	 * @param cls
+	 * @return
+	 */
+	public static <T> T getBean(Class<T> cls) {
+		return getApplicationContext().getBean(cls);
+	}
+
+	/**
+	 * 触发事件
+	 * 
+	 * @param event
+	 */
+	public static void publishEvent(ApplicationEvent event) {
+		getApplicationContext().publishEvent(event);
+	}
+
+}

+ 423 - 0
mall-search/src/main/java/com/uas/search/util/DocumentToObjectUtils.java

@@ -0,0 +1,423 @@
+package com.uas.search.util;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.constant.model.BaseOrderDetail;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.*;
+import org.apache.lucene.document.Document;
+
+import java.util.*;
+
+/**
+ * 将Document转换为对象的工具类
+ * 
+ * @author sunyj
+ * @since 2016年10月24日 下午2:24:39
+ */
+public class DocumentToObjectUtils {
+
+	/**
+	 * 将Document转换为对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Object toObject(Document document, String tableName) {
+		if (tableName.equals(SearchConstants.KIND_TABLE_NAME)) {
+			return toKind(document);
+		} else if (tableName.equals(SearchConstants.BRAND_TABLE_NAME)) {
+			return toBrand(document);
+		} else if (tableName.equals(SearchConstants.COMPONENT_TABLE_NAME)) {
+			return toComponent(document);
+		} else if (tableName.equals(SearchConstants.GOODS_TABLE_NAME)) {
+			return toGoods(document);
+		} else if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return toOrder(document);
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return toOrderInvoice(document);
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return toPurchase(document);
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return toPurchaseInvoice(document);
+		}
+		throw new SearchException("不支持的表:" + tableName);
+	}
+
+	/**
+	 * 将Document转换为类目对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Kind toKind(Document document) {
+		if (document == null) {
+			return null;
+		}
+		Kind kind = new Kind();
+		kind.setId(Long.valueOf(document.get(SearchConstants.KIND_ID_FIELD)));
+		kind.setNameCn(document.get(SearchConstants.KIND_NAMECN_FIELD));
+		kind.setIsLeaf(Short.valueOf(document.get(SearchConstants.KIND_ISLEAF_FIELD)));
+		kind.setLevel(Short.valueOf(document.get(SearchConstants.KIND_LEVEL_FIELD)));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.KIND_VISIT_COUNT_FIELD))) {
+            kind.setVisitCount(Long.valueOf(document.get(SearchConstants.KIND_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.KIND_SEARCH_COUNT_FIELD))) {
+            kind.setSearchCount(Long.valueOf(document.get(SearchConstants.KIND_SEARCH_COUNT_FIELD)));
+        }
+		return kind;
+	}
+
+	/**
+	 * 将Document转换为品牌对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Brand toBrand(Document document) {
+		if (document == null) {
+			return null;
+		}
+		Brand brand = new Brand();
+		brand.setId(Long.valueOf(document.get(SearchConstants.BRAND_ID_FIELD)));
+		brand.setNameCn(document.get(SearchConstants.BRAND_NAMECN_FIELD));
+		brand.setUuid(document.get(SearchConstants.BRAND_UUID_FIELD));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.BRAND_NAMEEN_FIELD))) {
+            brand.setNameEn(document.get(SearchConstants.BRAND_NAMEEN_FIELD));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.BRAND_VISIT_COUNT_FIELD))) {
+            brand.setVisitCount(Long.valueOf(document.get(SearchConstants.BRAND_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.BRAND_SEARCH_COUNT_FIELD))) {
+            brand.setSearchCount(Long.valueOf(document.get(SearchConstants.BRAND_SEARCH_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.BRAND_WEIGHT_FIELD))) {
+            brand.setWeight(Double.valueOf(document.get(SearchConstants.BRAND_WEIGHT_FIELD)));
+        }
+		return brand;
+	}
+
+	/**
+	 * 将Document转换为器件对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Component toComponent(Document document) {
+		if (document == null) {
+			return null;
+		}
+		Component component = new Component();
+		component.setId(Long.valueOf(document.get(SearchConstants.COMPONENT_ID_FIELD)));
+		component.setCode(document.get(SearchConstants.COMPONENT_CODE_FIELD));
+		component.setUuid(document.get(SearchConstants.COMPONENT_UUID_FIELD));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_VISIT_COUNT_FIELD))) {
+            component.setVisitCount(Long.valueOf(document.get(SearchConstants.COMPONENT_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_SEARCH_COUNT_FIELD))) {
+            component.setSearchCount(Long.valueOf(document.get(SearchConstants.COMPONENT_SEARCH_COUNT_FIELD)));
+        }
+
+		Kind kind = new Kind();
+		kind.setId(Long.valueOf(document.get(SearchConstants.COMPONENT_KI_ID_FIELD)));
+		kind.setNameCn(document.get(SearchConstants.COMPONENT_KI_NAME_FIELD));
+		kind.setLevel(Short.valueOf(document.get(SearchConstants.COMPONENT_KI_LEVEL_FIELD)));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_KI_VISIT_COUNT_FIELD))) {
+            kind.setVisitCount(Long.valueOf(document.get(SearchConstants.COMPONENT_KI_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_KI_SEARCH_COUNT_FIELD))) {
+            kind.setSearchCount(Long.valueOf(document.get(SearchConstants.COMPONENT_KI_SEARCH_COUNT_FIELD)));
+        }
+		component.setKind(kind);
+
+		Brand brand = new Brand();
+		brand.setId(Long.valueOf(document.get(SearchConstants.COMPONENT_BR_ID_FIELD)));
+		brand.setNameCn(document.get(SearchConstants.COMPONENT_BR_NAMECN_FIELD));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_BR_NAMEEN_FIELD))) {
+            brand.setNameEn(document.get(SearchConstants.COMPONENT_BR_NAMEEN_FIELD));
+        }
+		brand.setUuid(document.get(SearchConstants.COMPONENT_BR_UUID_FIELD));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_BR_VISIT_COUNT_FIELD))) {
+            brand.setVisitCount(Long.valueOf(document.get(SearchConstants.COMPONENT_BR_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_BR_SEARCH_COUNT_FIELD))) {
+            brand.setSearchCount(Long.valueOf(document.get(SearchConstants.COMPONENT_BR_SEARCH_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_BR_WEIGHT_FIELD))) {
+            brand.setWeight(Double.valueOf(document.get(SearchConstants.COMPONENT_BR_WEIGHT_FIELD)));
+        }
+		component.setBrand(brand);
+
+		if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_RESERVE_FIELD))) {
+			component.setReserve(Double.valueOf(document.get(SearchConstants.COMPONENT_RESERVE_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_SAMPLE_QTY_FIELD))) {
+			component.setSampleQty(Double.valueOf(document.get(SearchConstants.COMPONENT_SAMPLE_QTY_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_ORIGINAL_QTY_FIELD))) {
+			component.setOriginalQty(Double.valueOf(document.get(SearchConstants.COMPONENT_ORIGINAL_QTY_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.COMPONENT_INACTION_STOCK_QTY_FIELD))) {
+			component.setInactionStockQty(
+					Double.valueOf(document.get(SearchConstants.COMPONENT_INACTION_STOCK_QTY_FIELD)));
+		}
+		return component;
+	}
+
+	/**
+	 * 将Document转换为批次对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Goods toGoods(Document document) {
+		if (document == null) {
+			return null;
+		}
+		Goods goods = new Goods();
+		TradeGoods tradeGoods = new TradeGoods();
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_GO_ID_FIELD))) {
+			tradeGoods.setId(Long.valueOf(document.get(SearchConstants.GOODS_GO_ID_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_GO_RESERVE_FIELD))) {
+			tradeGoods.setReserve(Double.valueOf(document.get(SearchConstants.GOODS_GO_RESERVE_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_GO_STATUS_FIELD))) {
+			tradeGoods.setStatus(Long.valueOf(document.get(SearchConstants.GOODS_GO_STATUS_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_GO_MINPRICERMB_FIELD))) {
+			tradeGoods.setMinPriceRMB(Double.valueOf(document.get(SearchConstants.GOODS_GO_MINPRICERMB_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_GO_MINPRICEUSD_FIELD))) {
+			tradeGoods.setMinPriceUSD(Double.valueOf(document.get(SearchConstants.GOODS_GO_MINPRICEUSD_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_CRNAME_FIELD))) {
+			tradeGoods.setCrName(document.get(SearchConstants.GOODS_CRNAME_FIELD));
+		}
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_GO_VISIT_COUNT_FIELD))) {
+            tradeGoods.setVisitCount(Long.valueOf(document.get(SearchConstants.GOODS_GO_VISIT_COUNT_FIELD)));
+        }
+		goods.setTradeGoods(tradeGoods);
+
+		Store store = new Store();
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_ST_UUID_FIELD))) {
+			store.setUuid(document.get(SearchConstants.GOODS_ST_UUID_FIELD));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_ST_TYPE_FIELD))) {
+			store.setType(document.get(SearchConstants.GOODS_ST_TYPE_FIELD));
+		}
+		goods.setStore(store);
+
+		Component component = new Component();
+		component.setId(Long.valueOf(document.get(SearchConstants.GOODS_CMP_ID_FIELD)));
+		component.setCode(document.get(SearchConstants.GOODS_CMP_CODE_FIELD));
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_CMP_DESCRIPTION_FIELD))) {
+            component.setDescription(document.get(SearchConstants.GOODS_CMP_DESCRIPTION_FIELD));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_CMP_VISIT_COUNT_FIELD))) {
+            component.setVisitCount(Long.valueOf(document.get(SearchConstants.GOODS_CMP_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_CMP_SEARCH_COUNT_FIELD))) {
+            component.setSearchCount(Long.valueOf(document.get(SearchConstants.GOODS_CMP_SEARCH_COUNT_FIELD)));
+        }
+
+		Kind kind = new Kind();
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_KI_ID_FIELD))) {
+			kind.setId(Long.valueOf(document.get(SearchConstants.GOODS_KI_ID_FIELD)));
+		}
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_KI_NAME_CN_FIELD))) {
+		    kind.setNameCn(document.get(SearchConstants.GOODS_KI_NAME_CN_FIELD));
+        }
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_KI_LEVEL_FIELD))) {
+			kind.setLevel(Short.valueOf(document.get(SearchConstants.GOODS_KI_LEVEL_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_KI_ISLEAF_FIELD))) {
+			kind.setIsLeaf(Short.valueOf(document.get(SearchConstants.GOODS_KI_ISLEAF_FIELD)));
+		}
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_KI_VISIT_COUNT_FIELD))) {
+            kind.setVisitCount(Long.valueOf(document.get(SearchConstants.GOODS_KI_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_KI_SEARCH_COUNT_FIELD))) {
+            kind.setSearchCount(Long.valueOf(document.get(SearchConstants.GOODS_KI_SEARCH_COUNT_FIELD)));
+        }
+		component.setKind(kind);
+
+		Brand brand = new Brand();
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_ID_FIELD))) {
+			brand.setId(Long.valueOf(document.get(SearchConstants.GOODS_BR_ID_FIELD)));
+		}
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_NAME_CN_FIELD))) {
+		    brand.setNameCn(document.get(SearchConstants.GOODS_BR_NAME_CN_FIELD));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_UUID_FIELD))) {
+		    brand.setUuid(document.get(SearchConstants.GOODS_BR_UUID_FIELD));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_NAME_EN_FIELD))) {
+            brand.setNameEn(document.get(SearchConstants.GOODS_BR_NAME_EN_FIELD));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_VISIT_COUNT_FIELD))) {
+            brand.setVisitCount(Long.valueOf(document.get(SearchConstants.GOODS_BR_VISIT_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_SEARCH_COUNT_FIELD))) {
+            brand.setSearchCount(Long.valueOf(document.get(SearchConstants.GOODS_BR_SEARCH_COUNT_FIELD)));
+        }
+        if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_BR_WEIGHT_FIELD))) {
+            brand.setWeight(Double.valueOf(document.get(SearchConstants.GOODS_BR_WEIGHT_FIELD)));
+        }
+		component.setBrand(brand);
+		goods.setComponent(component);
+
+		Products products = new Products();
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_PR_ID_FIELD))) {
+			products.setId(Long.valueOf(document.get(SearchConstants.GOODS_PR_ID_FIELD)));
+		}
+		if (!StringUtils.isEmpty(document.get(SearchConstants.GOODS_PR_PCMPCODE_FIELD))) {
+			products.setPcmpCode(document.get(SearchConstants.GOODS_PR_PCMPCODE_FIELD));
+		}
+		return goods;
+	}
+
+	/**
+	 * 将Document转换为销售单对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Order toOrder(Document document) {
+		if (document == null) {
+			return null;
+		}
+		Order order = new Order();
+		order.setId(Long.valueOf(document.get(SearchConstants.ORDER_ID_FIELD)));
+		order.setCode(document.get(SearchConstants.ORDER_CODE_FIELD));
+		order.setBuyeruu(Long.valueOf(document.get(SearchConstants.ORDER_BUYERUU_FIELD)));
+		order.setBuyername(document.get(SearchConstants.ORDER_BUYERNAME_FIELD));
+		Enterprise buyerEnterprise = new Enterprise();
+		buyerEnterprise.setUu(Long.valueOf(document.get(SearchConstants.ORDER_BUYERENUU_FIELD)));
+		buyerEnterprise.setEnName(document.get(SearchConstants.ORDER_BUYERENNAME_FIELD));
+		order.setBuyerEnterprise(buyerEnterprise);
+		Enterprise sellerEnterprise = new Enterprise();
+		if (document.get(SearchConstants.ORDER_SELLERENUU_FIELD) != null) {
+			sellerEnterprise.setUu(Long.valueOf(document.get(SearchConstants.ORDER_SELLERENUU_FIELD)));
+		}
+		if (document.get(SearchConstants.ORDER_SELLERENNAME_FIELD) != null) {
+			sellerEnterprise.setEnName(document.get(SearchConstants.ORDER_SELLERENNAME_FIELD));
+		}
+		order.setSellerEnterprise(sellerEnterprise);
+		Date createtime = new Date(Long.valueOf(document.get(SearchConstants.CREATETIME_FIELD)));
+		order.setCreatetime(createtime);
+		order.setStatus(Integer.valueOf(document.get(SearchConstants.ORDER_STATUS_FIELD)));
+		order.setDetails(toDetailsSet(document.get(SearchConstants.ORDER_DETAILS_FIELD), OrderDetail.class));
+		return order;
+	}
+
+	/**
+	 * 将Document转换为销售发货单对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static OrderInvoice toOrderInvoice(Document document) {
+		if (document == null) {
+			return null;
+		}
+		OrderInvoice orderInvoice = new OrderInvoice();
+		orderInvoice.setId(Long.valueOf(document.get(SearchConstants.ORDER_INVOICE_ID_FIELD)));
+		orderInvoice.setCode(document.get(SearchConstants.ORDER_INVOICE_CODE_FIELD));
+		orderInvoice.setBuyeruu(Long.valueOf(document.get(SearchConstants.ORDER_INVOICE_BUYERUU_FIELD)));
+		orderInvoice.setBuyername(document.get(SearchConstants.ORDER_INVOICE_BUYERNAME_FIELD));
+		Enterprise buyerEnterprise = new Enterprise();
+		buyerEnterprise.setUu(Long.valueOf(document.get(SearchConstants.ORDER_INVOICE_BUYERENUU_FIELD)));
+		buyerEnterprise.setEnName(document.get(SearchConstants.ORDER_INVOICE_BUYERENNAME_FIELD));
+		orderInvoice.setBuyerEnterprise(buyerEnterprise);
+		Date createtime = new Date(Long.valueOf(document.get(SearchConstants.CREATETIME_FIELD)));
+		orderInvoice.setCreatetime(createtime);
+		orderInvoice.setStatus(Integer.valueOf(document.get(SearchConstants.ORDER_INVOICE_STATUS_FIELD)));
+		orderInvoice.setDetails(toDetailsSet(document.get(SearchConstants.ORDER_INVOICE_DETAILS_FIELD),
+				OrderInvoiceDetail.class));
+		return orderInvoice;
+	}
+
+	/**
+	 * 将Document转换为采购单对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static Purchase toPurchase(Document document) {
+		if (document == null) {
+			return null;
+		}
+		Purchase purchase = new Purchase();
+		purchase.setId(Long.valueOf(document.get(SearchConstants.PURCHASE_ID_FIELD)));
+		purchase.setCode(document.get(SearchConstants.PURCHASE_CODE_FIELD));
+		purchase.setSellerenuu(Long.valueOf(document.get(SearchConstants.PURCHASE_SELLERENUU_FIELD)));
+		purchase.setSellerenname(document.get(SearchConstants.PURCHASE_SELLERENNAME_FIELD));
+		Date createtime = new Date(Long.valueOf(document.get(SearchConstants.CREATETIME_FIELD)));
+		purchase.setCreatetime(createtime);
+		purchase.setStatus(Integer.valueOf(document.get(SearchConstants.PURCHASE_STATUS_FIELD)));
+		purchase.setDetails(
+				toDetailsSet(document.get(SearchConstants.PURCHASE_DETAILS_FIELD), PurchaseDetail.class));
+		return purchase;
+	}
+
+	/**
+	 * 将Document转换为采购发货单对象
+	 * 
+	 * @param document
+	 * @return
+	 */
+	public static PurchaseInvoice toPurchaseInvoice(Document document) {
+		if (document == null) {
+			return null;
+		}
+		PurchaseInvoice purchaseInvoice = new PurchaseInvoice();
+		purchaseInvoice.setId(Long.valueOf(document.get(SearchConstants.PURCHASE_INVOICE_ID_FIELD)));
+		purchaseInvoice.setCode(document.get(SearchConstants.PURCHASE_INVOICE_CODE_FIELD));
+		purchaseInvoice.setSellerenuu(Long.valueOf(document.get(SearchConstants.PURCHASE_INVOICE_SELLERENUU_FIELD)));
+		purchaseInvoice.setSellerenname(document.get(SearchConstants.PURCHASE_INVOICE_SELLERENNAME_FIELD));
+		Date createtime = new Date(Long.valueOf(document.get(SearchConstants.CREATETIME_FIELD)));
+		purchaseInvoice.setCreatetime(createtime);
+		purchaseInvoice.setStatus(Integer.valueOf(document.get(SearchConstants.PURCHASE_INVOICE_STATUS_FIELD)));
+		purchaseInvoice.setDetails(toDetailsSet(document.get(SearchConstants.PURCHASE_INVOICE_DETAILS_FIELD),
+				PurchaseInvoiceDetail.class));
+		return purchaseInvoice;
+	}
+
+	/**
+	 * 将json字符串转为明细对象
+	 * 
+	 * @param <T>
+	 * 
+	 * @param jsonString
+	 *            索引中存储的明细数据
+	 * @return 明细对象
+	 */
+	public static <T> Set<T> toDetailsSet(String jsonString, Class<T> clazz) {
+		if (StringUtils.isEmpty(jsonString)) {
+			return null;
+		}
+		Set<T> details = new HashSet<>();
+		details.addAll(JSONObject.parseArray(jsonString, clazz));
+		return details;
+	}
+
+	/**
+	 * 将json字符串转为明细对象
+	 * 
+	 * @param jsonString
+	 *            索引中存储的明细数据
+	 * @return 明细对象
+	 */
+	public static List<BaseOrderDetail> toDetailsList(String jsonString) {
+		if (StringUtils.isEmpty(jsonString)) {
+			return null;
+		}
+		List<com.uas.search.model.BaseOrderDetail> details = JSONObject.parseArray(jsonString, com.uas.search.model.BaseOrderDetail.class);
+		List<BaseOrderDetail> result = new ArrayList<>();
+		for (com.uas.search.model.BaseOrderDetail detail : details) {
+			result.add(new BaseOrderDetail(detail.getId(), detail.getDetno(), detail.getCode()));
+		}
+		return result;
+	}
+}

+ 44 - 0
mall-search/src/main/java/com/uas/search/util/ExceptionUtils.java

@@ -0,0 +1,44 @@
+package com.uas.search.util;
+
+/**
+ * 处理异常的工具类
+ * 
+ * @author sunyj
+ * @since 2017年8月10日 下午3:26:55
+ */
+public class ExceptionUtils {
+
+	/**
+	 * 获取异常及其Cause拼接成的字符串
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 拼接后的结果
+	 */
+	public static String getMessage(Throwable e) {
+		StringBuilder sb = new StringBuilder(e.toString());
+		if (e.getCause() != null) {
+			sb.append("\nCaused by: ").append(getMessage(e.getCause()));
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * 获取异常及其Cause的StackTrace拼接成的字符串
+	 * 
+	 * @param e
+	 *            异常
+	 * @return 拼接后的结果
+	 */
+	public static String getDetailedMessage(Throwable e) {
+		StringBuilder sb = new StringBuilder(e.toString());
+		StackTraceElement[] stackTraceElements = e.getStackTrace();
+		for (StackTraceElement stackTraceElement : stackTraceElements) {
+			sb.append("\n\t").append(stackTraceElement.toString());
+		}
+		if (e.getCause() != null) {
+			sb.append("\nCaused by: ").append(getDetailedMessage(e.getCause()));
+		}
+		return sb.toString();
+	}
+}

+ 52 - 0
mall-search/src/main/java/com/uas/search/util/FileUtils.java

@@ -0,0 +1,52 @@
+package com.uas.search.util;
+
+import java.io.File;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 文件操作
+ * 
+ * @author sunyj
+ * @since 2017年3月22日 上午9:40:41
+ */
+public class FileUtils {
+
+	private static Logger logger = LoggerFactory.getLogger(FileUtils.class);
+
+	/**
+	 * 删除文件夹下所有子文件(不含该文件夹本身)
+	 * 
+	 * @param dir
+	 */
+	public static void deleteSubFiles(File dir) {
+		if (dir == null || !dir.exists() || !dir.isDirectory()) {
+			return;
+		}
+		File[] files = dir.listFiles();
+		for (File file : files) {
+			delete(file);
+		}
+	}
+
+	/**
+	 * 删除文件(子文件及其本身均删除)
+	 * 
+	 * @param file
+	 */
+	public static void delete(File file) {
+		if (file == null || !file.exists()) {
+			return;
+		}
+		if (file.isDirectory()) {
+			File[] files = file.listFiles();
+			for (File f : files) {
+				delete(f);
+			}
+		}
+		file.delete();
+		logger.info("Deleted ..." + file.getPath());
+	}
+
+}

+ 56 - 0
mall-search/src/main/java/com/uas/search/util/IpHelper.java

@@ -0,0 +1,56 @@
+package com.uas.search.util;
+
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * <p>
+ * 获取IP地址类
+ * </p>
+ * 
+ */
+public class IpHelper {
+
+	/**
+	 * 获取客户端IP
+	 * 
+	 * @param request
+	 * @return
+	 */
+	public static String getIp(HttpServletRequest request) {
+		String ipAddress = null;
+		ipAddress = request.getHeader("X-Forwarded-For");
+		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+			ipAddress = request.getHeader("Proxy-Client-IP");
+		}
+		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+			ipAddress = request.getHeader("WL-Proxy-Client-IP");
+		}
+		if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+			ipAddress = request.getRemoteAddr();
+		}
+		// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+		if (ipAddress != null && ipAddress.length() > 15) {
+			if (ipAddress.indexOf(",") > 0) {
+				ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+			}
+		}
+		// window7系统下,用localhost访问时,ip会变成0:0:0:0:0:0:0:1
+		if (ipAddress != null && "0:0:0:0:0:0:0:1".equals(ipAddress)) {
+			ipAddress = "127.0.0.1";
+		}
+		return ipAddress;
+	}
+
+	/**
+	 * 判断指定的ip地址是否在局域网内
+	 * 
+	 * @param ipAddress
+	 * @return
+	 */
+	public static boolean isLAN(String ipAddress) {
+		return Pattern.matches("^((192.168.)|(10.)|(172.16)|(127.0))+[0-9.]+$", ipAddress);
+	}
+
+}

+ 36 - 0
mall-search/src/main/java/com/uas/search/util/NumberGenerator.java

@@ -0,0 +1,36 @@
+package com.uas.search.util;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * 数字生成工具类
+ *
+ * @author sunyj
+ * @since 2017年8月22日 上午11:34:44
+ */
+public class NumberGenerator {
+
+    /**
+     * 生成id时的偏移量
+     */
+    private static int offset = 0;
+
+    /**
+     * @return 产生唯一的id(一毫秒内不超过100个则不重复)
+     */
+    public static String generateId() {
+        // 偏移量,不超过100个
+        offset = (offset + 1) % 100;
+        // 当前毫秒数
+        long now = new Date().getTime();
+        // 起始时间 2017-01-01 00:00:00:000
+        Calendar startCalendar = Calendar.getInstance();
+        startCalendar.set(2017, 0, 1, 0, 0, 0);
+        startCalendar.set(Calendar.MILLISECOND, 0);
+        // 偏移量加16,保证得到的是两位16进制字符
+        String hex = Long.toHexString(now - startCalendar.getTimeInMillis()) + Integer.toHexString(offset + 16);
+        // 最后反转并转为大写
+        return hex.toUpperCase();
+    }
+}

+ 507 - 0
mall-search/src/main/java/com/uas/search/util/ObjectToDocumentUtils.java

@@ -0,0 +1,507 @@
+package com.uas.search.util;
+
+import com.alibaba.fastjson.JSONObject;
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.model.*;
+import net.sf.ehcache.search.SearchException;
+import org.apache.lucene.document.*;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.util.BytesRef;
+
+import java.util.Set;
+
+/**
+ * 将对象转换为Document的工具类
+ * 
+ * @author sunyj
+ * @since 2016年10月17日 上午11:32:19
+ */
+public class ObjectToDocumentUtils {
+
+	/**
+	 * 将对象转为Document
+	 * 
+	 * @param object
+	 *            对象,可为Kind、Brand、Component、
+	 *            Order、OrderInvoice、Purchase、
+	 *            PurchaseInvoice
+	 * 
+	 * @return 转换的Document
+	 */
+	public static Document toDocument(Object object) {
+		if (object == null) {
+			return null;
+		}
+		if (object instanceof Kind) {
+			return toDocument((Kind) object);
+		} else if (object instanceof Brand) {
+			return toDocument((Brand) object);
+		} else if (object instanceof Component) {
+			return toDocument((Component) object);
+		} else if (object instanceof Goods) {
+			return toDocument((Goods) object);
+		} else if (object instanceof Order) {
+			return toDocument((Order) object);
+		} else if (object instanceof OrderInvoice) {
+			return toDocument((OrderInvoice) object);
+		} else if (object instanceof Purchase) {
+			return toDocument((Purchase) object);
+		} else if (object instanceof PurchaseInvoice) {
+			return toDocument((PurchaseInvoice) object);
+		} else {
+			throw new SearchException("不支持将以下类型转换为Document:" + object.getClass().getName());
+		}
+	}
+
+	/**
+	 * Kind对象转为Document
+	 * 
+	 * @param kind
+	 * @return
+	 */
+	public static Document toDocument(Kind kind) {
+		if (kind == null || kind.getId() == null || StringUtils.isEmpty(kind.getNameCn()) || kind.getIsLeaf() == null
+				|| kind.getLevel() == null) {
+			return null;
+		}
+		Document document = new Document();
+		// 不能用LongField,否则后续实时更新索引时,方法updateDocument(new Term("", ""),
+		// doc)无法根据id进行更新
+		document.add(new StringField(SearchConstants.KIND_ID_FIELD, String.valueOf(kind.getId()), Store.YES));
+		document.add(new TextField(SearchConstants.KIND_NAMECN_FIELD, kind.getNameCn(), Store.YES));
+		document.add(new BinaryDocValuesField(SearchConstants.KIND_NAMECN_FIELD, new BytesRef(kind.getNameCn())));
+		document.add(new StringField(SearchConstants.KIND_ISLEAF_FIELD, String.valueOf(kind.getIsLeaf()), Store.YES));
+		document.add(new StringField(SearchConstants.KIND_LEVEL_FIELD, String.valueOf(kind.getLevel()), Store.YES));
+        if (kind.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.KIND_VISIT_COUNT_FIELD, kind.getVisitCount()));
+            document.add(new LongField(SearchConstants.KIND_VISIT_COUNT_FIELD, kind.getVisitCount(), Store.YES));
+        }
+		if (kind.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.KIND_SEARCH_COUNT_FIELD, kind.getSearchCount()));
+			document.add(new LongField(SearchConstants.KIND_SEARCH_COUNT_FIELD, kind.getSearchCount(), Store.YES));
+		}
+		return document;
+	}
+
+	/**
+	 * Brand对象转为Document
+	 * 
+	 * @param brand
+	 * @return
+	 */
+	public static Document toDocument(Brand brand) {
+		if (brand == null || brand.getId() == null || StringUtils.isEmpty(brand.getNameCn())
+				|| StringUtils.isEmpty(brand.getUuid())) {
+			return null;
+		}
+		Document document = new Document();
+		document.add(new StringField(SearchConstants.BRAND_ID_FIELD, String.valueOf(brand.getId()), Store.YES));
+		document.add(new TextField(SearchConstants.BRAND_NAMECN_FIELD, brand.getNameCn(), Store.YES));
+		document.add(new BinaryDocValuesField(SearchConstants.BRAND_NAMECN_FIELD, new BytesRef(brand.getNameCn())));
+        document.add(new StringField(SearchConstants.BRAND_NAMECN_UNTOKENIZED_FIELD, brand.getNameCn().toLowerCase(), Store.YES));
+        document.add(new BinaryDocValuesField(SearchConstants.BRAND_NAMECN_UNTOKENIZED_FIELD, new BytesRef(brand.getNameCn())));
+		document.add(new StringField(SearchConstants.BRAND_UUID_FIELD, brand.getUuid(), Store.YES));
+		if (brand.getNameEn() != null) {
+			document.add(new TextField(SearchConstants.BRAND_NAMEEN_FIELD, brand.getNameEn(), Store.YES));
+			document.add(new BinaryDocValuesField(SearchConstants.BRAND_NAMEEN_FIELD, new BytesRef(brand.getNameEn())));
+            document.add(new StringField(SearchConstants.BRAND_NAMEEN_UNTOKENIZED_FIELD, brand.getNameEn().toLowerCase(), Store.YES));
+            document.add(new BinaryDocValuesField(SearchConstants.BRAND_NAMEEN_UNTOKENIZED_FIELD, new BytesRef(brand.getNameEn())));
+		}
+        if (brand.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.BRAND_VISIT_COUNT_FIELD, brand.getVisitCount()));
+            document.add(new LongField(SearchConstants.BRAND_VISIT_COUNT_FIELD, brand.getVisitCount(), Store.YES));
+        }
+		if (brand.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.BRAND_SEARCH_COUNT_FIELD, brand.getSearchCount()));
+			document.add(new LongField(SearchConstants.BRAND_SEARCH_COUNT_FIELD, brand.getSearchCount(), Store.YES));
+		}
+		if (brand.getWeight() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.BRAND_WEIGHT_FIELD, brand.getWeight()));
+			document.add(new DoubleField(SearchConstants.BRAND_WEIGHT_FIELD, brand.getWeight(), Store.YES));
+		}
+		return document;
+	}
+
+	/**
+	 * Component对象转为Document
+	 * 
+	 * @param component
+	 * @return
+	 */
+	public static Document toDocument(Component component) {
+        if (component == null || component.getId() == null || StringUtils.isEmpty(component.getUuid())
+				|| StringUtils.isEmpty(component.getCode()) || component.getKind() == null
+                || component.getKind().getId() == null || StringUtils.isEmpty(component.getKind().getNameCn())
+                || component.getKind().getLevel() == null || component.getBrand() == null
+                || component.getBrand().getId() == null || StringUtils.isEmpty(component.getBrand().getNameCn())
+				|| StringUtils.isEmpty(component.getBrand().getUuid())) {
+			return null;
+		}
+		Document document = new Document();
+		document.add(new StringField(SearchConstants.COMPONENT_ID_FIELD, String.valueOf(component.getId()), Store.YES));
+		document.add(new StringField(SearchConstants.COMPONENT_UUID_FIELD, component.getUuid(), Store.YES));
+		// 转小写,以避免分词,又不会因大小写影响搜索
+		document.add(new StringField(SearchConstants.COMPONENT_CODE_FIELD, component.getCode().toLowerCase(), Store.YES));
+		document.add(new BinaryDocValuesField(SearchConstants.COMPONENT_CODE_FIELD, new BytesRef(component.getCode())));
+        if (component.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_VISIT_COUNT_FIELD, component.getVisitCount()));
+            document.add(new LongField(SearchConstants.COMPONENT_VISIT_COUNT_FIELD, component.getVisitCount(), Store.YES));
+        }
+		if (component.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_SEARCH_COUNT_FIELD, component.getSearchCount()));
+			document.add(new LongField(SearchConstants.COMPONENT_SEARCH_COUNT_FIELD, component.getSearchCount(), Store.YES));
+		}
+
+        Kind kind = component.getKind();
+        document.add(new StringField(SearchConstants.COMPONENT_KI_ID_FIELD, String.valueOf(kind.getId()), Store.YES));
+		document.add(new TextField(SearchConstants.COMPONENT_KI_NAME_FIELD, kind.getNameCn(), Store.YES));
+		document.add(new StringField(SearchConstants.COMPONENT_KI_LEVEL_FIELD, String.valueOf(kind.getLevel()), Store.YES));
+        if (kind.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_KI_VISIT_COUNT_FIELD, kind.getVisitCount()));
+            document.add(new LongField(SearchConstants.COMPONENT_KI_VISIT_COUNT_FIELD, kind.getVisitCount(), Store.YES));
+        }
+		if (kind.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_KI_SEARCH_COUNT_FIELD, kind.getSearchCount()));
+			document.add(new LongField(SearchConstants.COMPONENT_KI_SEARCH_COUNT_FIELD, kind.getSearchCount(), Store.YES));
+		}
+
+        Brand brand = component.getBrand();
+        document.add(new StringField(SearchConstants.COMPONENT_BR_ID_FIELD, String.valueOf(brand.getId()), Store.YES));
+		document.add(new TextField(SearchConstants.COMPONENT_BR_NAMECN_FIELD, brand.getNameCn(), Store.YES));
+		if(brand.getNameEn() != null){
+			document.add(new TextField(SearchConstants.COMPONENT_BR_NAMEEN_FIELD, brand.getNameEn(), Store.YES));
+		}
+		document.add(new StringField(SearchConstants.COMPONENT_BR_UUID_FIELD, brand.getUuid(), Store.YES));
+        if (brand.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_BR_VISIT_COUNT_FIELD, brand.getVisitCount()));
+            document.add(new LongField(SearchConstants.COMPONENT_BR_VISIT_COUNT_FIELD, brand.getVisitCount(), Store.YES));
+        }
+		if (brand.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_BR_SEARCH_COUNT_FIELD, brand.getSearchCount()));
+			document.add(new LongField(SearchConstants.COMPONENT_BR_SEARCH_COUNT_FIELD, brand.getSearchCount(), Store.YES));
+		}
+		if (brand.getWeight() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.COMPONENT_BR_WEIGHT_FIELD, brand.getWeight()));
+			document.add(new DoubleField(SearchConstants.COMPONENT_BR_WEIGHT_FIELD, brand.getWeight(), Store.YES));
+		}
+
+		if (component.getReserve() != null) {
+			document.add(new DoubleField(SearchConstants.COMPONENT_RESERVE_FIELD, component.getReserve(), Store.YES));
+		}
+		if (component.getSampleQty() != null) {
+			document.add(
+					new DoubleField(SearchConstants.COMPONENT_SAMPLE_QTY_FIELD, component.getSampleQty(), Store.YES));
+		}
+		if (component.getOriginalQty() != null) {
+			document.add(new DoubleField(SearchConstants.COMPONENT_ORIGINAL_QTY_FIELD, component.getOriginalQty(),
+					Store.YES));
+		}
+		if (component.getInactionStockQty() != null) {
+			document.add(new DoubleField(SearchConstants.COMPONENT_INACTION_STOCK_QTY_FIELD,
+					component.getInactionStockQty(), Store.YES));
+		}
+
+		// 属性值加入索引,索引中field的键:"pr_"前缀连接属性的id
+		Set<PropertyValue> propertyValues = component.getProperties();
+		for (PropertyValue propertyValue : propertyValues) {
+			if (!StringUtils.isEmpty(propertyValue.getValue())) {
+				String fieldKey = SearchConstants.COMPONENT_PROPERTY_PREFIX + propertyValue.getPropertyid();
+				document.add(new StringField(fieldKey, propertyValue.getValue(), Store.YES));
+
+				// 另建一份分词的属性索引,用于属性值联想时不区分大小写
+				String fieldKeyTokenized = fieldKey + SearchConstants.COMPONENT_PROPERTY_TOKENIZED_SUFFIX;
+				document.add(new TextField(fieldKeyTokenized, propertyValue.getValue(), Store.YES));
+			}
+		}
+		return document;
+	}
+
+	/**
+	 * Goods对象转为Document
+	 * 
+	 * @param goods
+	 * @return
+	 */
+	public static Document toDocument(Goods goods) {
+		if (goods == null || goods.getComponent() == null || StringUtils.isEmpty(goods.getComponent().getId()) ||
+				StringUtils.isEmpty(goods.getComponent().getCode()) || goods.getComponent().getKind() == null ||
+				goods.getComponent().getBrand() == null) {
+			return null;
+		}
+
+		Document document = new Document();
+		if(goods.getTradeGoods() != null){
+			TradeGoods tradeGoods = goods.getTradeGoods();
+			if (tradeGoods.getId() != null) {
+				document.add(new StringField(SearchConstants.GOODS_GO_ID_FIELD, String.valueOf(tradeGoods.getId()), Store.YES));
+			}
+			if (tradeGoods.getReserve() != null) {
+				document.add(new DoubleDocValuesField(SearchConstants.GOODS_GO_RESERVE_FIELD, tradeGoods.getReserve()));
+				document.add(new DoubleField(SearchConstants.GOODS_GO_RESERVE_FIELD, tradeGoods.getReserve(), Store.YES));
+			}
+			if (tradeGoods.getStatus() != null) {
+				document.add(new StringField(SearchConstants.GOODS_GO_STATUS_FIELD, String.valueOf(tradeGoods.getStatus()), Store.YES));
+			}
+			if (tradeGoods.getMinPriceRMB() != null) {
+				document.add(
+						new DoubleDocValuesField(SearchConstants.GOODS_GO_MINPRICERMB_FIELD, tradeGoods.getMinPriceRMB()));
+				document.add(
+						new DoubleField(SearchConstants.GOODS_GO_MINPRICERMB_FIELD, tradeGoods.getMinPriceRMB(), Store.YES));
+			}
+			if (tradeGoods.getMinPriceUSD() != null) {
+				document.add(
+						new DoubleDocValuesField(SearchConstants.GOODS_GO_MINPRICEUSD_FIELD, tradeGoods.getMinPriceUSD()));
+				document.add(
+						new DoubleField(SearchConstants.GOODS_GO_MINPRICEUSD_FIELD, tradeGoods.getMinPriceUSD(), Store.YES));
+			}
+			if (tradeGoods.getCrName() != null) {
+				document.add(new TextField(SearchConstants.GOODS_CRNAME_FIELD, tradeGoods.getCrName(), Store.YES));
+			}
+            if (tradeGoods.getVisitCount() != null) {
+                document.add(new DoubleDocValuesField(SearchConstants.GOODS_GO_VISIT_COUNT_FIELD, tradeGoods.getVisitCount()));
+                document.add(new LongField(SearchConstants.GOODS_GO_VISIT_COUNT_FIELD, tradeGoods.getVisitCount(), Store.YES));
+            }
+		}
+
+		if (goods.getStore() != null) {
+			com.uas.search.model.Store store = goods.getStore();
+			if (store.getUuid() != null) {
+				document.add(new TextField(SearchConstants.GOODS_ST_UUID_FIELD, store.getUuid(), Store.YES));
+			}
+			if (store.getType() != null) {
+				document.add(new TextField(SearchConstants.GOODS_ST_TYPE_FIELD, store.getType(), Store.YES));
+			}
+		}
+
+		Component component = goods.getComponent();
+		document.add(new StringField(SearchConstants.GOODS_CMP_ID_FIELD, String.valueOf(component.getId()), Store.YES));
+		document.add(new StringField(SearchConstants.GOODS_CMP_CODE_FIELD, component.getCode().toLowerCase(), Store.YES));
+        document.add(new BinaryDocValuesField(SearchConstants.GOODS_CMP_CODE_FIELD, new BytesRef(component.getCode())));
+        if (component.getDescription() != null) {
+            document.add(new TextField(SearchConstants.GOODS_CMP_DESCRIPTION_FIELD, component.getDescription(), Store.YES));
+        }
+        if (component.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.GOODS_CMP_VISIT_COUNT_FIELD, component.getVisitCount()));
+            document.add(new LongField(SearchConstants.GOODS_CMP_VISIT_COUNT_FIELD, component.getVisitCount(), Store.YES));
+        }
+		if (component.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.GOODS_CMP_SEARCH_COUNT_FIELD, component.getSearchCount()));
+			document.add(new LongField(SearchConstants.GOODS_CMP_SEARCH_COUNT_FIELD, component.getSearchCount(), Store.YES));
+		}
+
+		Kind kind = component.getKind();
+		if (kind.getId() != null) {
+			document.add(new StringField(SearchConstants.GOODS_KI_ID_FIELD, String.valueOf(kind.getId()), Store.YES));
+		}
+		if (kind.getNameCn() != null) {
+			document.add(new TextField(SearchConstants.GOODS_KI_NAME_CN_FIELD, kind.getNameCn(), Store.YES));
+		}
+		if (kind.getLevel() != null) {
+			document.add(new NumericDocValuesField(SearchConstants.GOODS_KI_LEVEL_FIELD, kind.getIsLeaf()));
+			document.add(new LongField(SearchConstants.GOODS_KI_LEVEL_FIELD, kind.getIsLeaf(), Store.YES));
+		}
+		if (kind.getIsLeaf() != null) {
+			document.add(
+					new StringField(SearchConstants.GOODS_KI_ISLEAF_FIELD, String.valueOf(kind.getLevel()), Store.YES));
+		}
+        if (kind.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.GOODS_KI_VISIT_COUNT_FIELD, kind.getVisitCount()));
+            document.add(new LongField(SearchConstants.GOODS_KI_VISIT_COUNT_FIELD, kind.getVisitCount(), Store.YES));
+        }
+		if (kind.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.GOODS_KI_SEARCH_COUNT_FIELD, kind.getSearchCount()));
+			document.add(new LongField(SearchConstants.GOODS_KI_SEARCH_COUNT_FIELD, kind.getSearchCount(), Store.YES));
+		}
+
+		Brand brand = component.getBrand();
+		if (brand.getId() != null) {
+			document.add(new StringField(SearchConstants.GOODS_BR_ID_FIELD, String.valueOf(brand.getId()), Store.YES));
+		}
+		if (brand.getNameCn() != null) {
+			document.add(new TextField(SearchConstants.GOODS_BR_NAME_CN_FIELD, brand.getNameCn(), Store.YES));
+			document.add(new StringField(SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD, brand.getNameCn().toLowerCase(), Store.YES));
+            document.add(new BinaryDocValuesField(SearchConstants.GOODS_BR_NAME_CN_UNTOKENIZED_FIELD, new BytesRef(brand.getNameCn())));
+		}
+		if (brand.getNameEn() != null) {
+			document.add(new TextField(SearchConstants.GOODS_BR_NAME_EN_FIELD, brand.getNameEn(), Store.YES));
+			document.add(new StringField(SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD, brand.getNameEn().toLowerCase(), Store.YES));
+            document.add(new BinaryDocValuesField(SearchConstants.GOODS_BR_NAME_EN_UNTOKENIZED_FIELD, new BytesRef(brand.getNameEn())));
+		}
+		if (brand.getUuid() != null) {
+			document.add(new StringField(SearchConstants.GOODS_BR_UUID_FIELD, brand.getUuid(), Store.YES));
+		}
+        if (brand.getVisitCount() != null) {
+            document.add(new DoubleDocValuesField(SearchConstants.GOODS_BR_VISIT_COUNT_FIELD, brand.getVisitCount()));
+            document.add(new LongField(SearchConstants.GOODS_BR_VISIT_COUNT_FIELD, brand.getVisitCount(), Store.YES));
+        }
+		if (brand.getSearchCount() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.GOODS_BR_SEARCH_COUNT_FIELD, brand.getSearchCount()));
+			document.add(new LongField(SearchConstants.GOODS_BR_SEARCH_COUNT_FIELD, brand.getSearchCount(), Store.YES));
+		}
+		if (brand.getWeight() != null) {
+			document.add(new DoubleDocValuesField(SearchConstants.GOODS_BR_WEIGHT_FIELD, brand.getWeight()));
+			document.add(new DoubleField(SearchConstants.GOODS_BR_WEIGHT_FIELD, brand.getWeight(), Store.YES));
+		}
+
+        if(goods.getProducts() != null){
+            Products products = goods.getProducts();
+            if (products.getId() != null) {
+                document.add(new StringField(SearchConstants.GOODS_PR_ID_FIELD, String.valueOf(products.getId()), Store.YES));
+            }
+            if(products.getPcmpCode() != null){
+                document.add(new StringField(SearchConstants.GOODS_PR_PCMPCODE_FIELD, products.getPcmpCode().toLowerCase(), Store.YES));
+                document.add(new BinaryDocValuesField(SearchConstants.GOODS_PR_PCMPCODE_FIELD, new BytesRef(products.getPcmpCode())));
+            }
+        }
+
+        return document;
+	}
+
+	/**
+	 * Order对象转为Document
+	 * 
+	 * @param order
+	 *            Order对象
+	 * @return 转换的Document
+	 */
+	public static Document toDocument(Order order) {
+		if (order == null || order.getId() == null || StringUtils.isEmpty(order.getCode()) || order.getBuyeruu() == null
+				|| StringUtils.isEmpty(order.getBuyername()) || order.getBuyerEnterprise() == null
+				|| order.getBuyerEnterprise().getUu() == null
+				|| StringUtils.isEmpty(order.getBuyerEnterprise().getEnName()) || order.getCreatetime() == null
+				|| order.getStatus() == null) {
+			return null;
+		}
+		Document document = new Document();
+		document.add(new StringField(SearchConstants.ORDER_ID_FIELD, String.valueOf(order.getId()), Store.YES));
+		// 利用正则搜索,code等不会被分词为多个词的字段存为TextField
+		document.add(new TextField(SearchConstants.ORDER_CODE_FIELD, order.getCode(), Store.YES));
+		document.add(
+				new StringField(SearchConstants.ORDER_BUYERUU_FIELD, String.valueOf(order.getBuyeruu()), Store.YES));
+		// 利用正则搜索,name等会被分词为多个词的字段存为StringField(不然会有边界问题,影响准确度)
+		document.add(new StringField(SearchConstants.ORDER_BUYERNAME_FIELD, order.getBuyername(), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_BUYERENUU_FIELD,
+				String.valueOf(order.getBuyerEnterprise().getUu()), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_BUYERENNAME_FIELD, order.getBuyerEnterprise().getEnName(),
+				Store.YES));
+		if (order.getSellerEnterprise() != null) {
+			if (order.getSellerEnterprise().getUu() != null) {
+				document.add(new StringField(SearchConstants.ORDER_SELLERENUU_FIELD,
+						String.valueOf(order.getSellerEnterprise().getUu()), Store.YES));
+			}
+			if (order.getSellerEnterprise().getEnName() != null) {
+				document.add(new StringField(SearchConstants.ORDER_SELLERENNAME_FIELD,
+						order.getSellerEnterprise().getEnName(), Store.YES));
+			}
+		}
+		document.add(new LongField(SearchConstants.CREATETIME_FIELD, order.getCreatetime().getTime(), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_STATUS_FIELD, String.valueOf(order.getStatus()), Store.YES));
+		// 明细以json的格式存储
+		if (!CollectionUtils.isEmpty(order.getDetails())) {
+			document.add(new TextField(SearchConstants.ORDER_DETAILS_FIELD, JSONObject.toJSONString(order.getDetails()),
+					Store.YES));
+		}
+		return document;
+	}
+
+	/**
+	 * OrderInvoice对象转为Document
+	 * 
+	 * @param orderInvoice
+	 *            OrderInvoice对象
+	 * @return 转换的Document
+	 */
+	public static Document toDocument(OrderInvoice orderInvoice) {
+		if (orderInvoice == null || orderInvoice.getId() == null || StringUtils.isEmpty(orderInvoice.getCode())
+				|| orderInvoice.getBuyeruu() == null || StringUtils.isEmpty(orderInvoice.getBuyername())
+				|| orderInvoice.getBuyerEnterprise() == null || orderInvoice.getBuyerEnterprise().getUu() == null
+				|| StringUtils.isEmpty(orderInvoice.getBuyerEnterprise().getEnName())
+				|| orderInvoice.getCreatetime() == null || orderInvoice.getStatus() == null) {
+			return null;
+		}
+		Document document = new Document();
+		document.add(new StringField(SearchConstants.ORDER_INVOICE_ID_FIELD, String.valueOf(orderInvoice.getId()),
+				Store.YES));
+		document.add(new TextField(SearchConstants.ORDER_INVOICE_CODE_FIELD, orderInvoice.getCode(), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_INVOICE_BUYERUU_FIELD,
+				String.valueOf(orderInvoice.getBuyeruu()), Store.YES));
+		document.add(
+				new StringField(SearchConstants.ORDER_INVOICE_BUYERNAME_FIELD, orderInvoice.getBuyername(), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_INVOICE_BUYERENUU_FIELD,
+				String.valueOf(orderInvoice.getBuyerEnterprise().getUu()), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_INVOICE_BUYERENNAME_FIELD,
+				orderInvoice.getBuyerEnterprise().getEnName(), Store.YES));
+		document.add(
+				new LongField(SearchConstants.CREATETIME_FIELD, orderInvoice.getCreatetime().getTime(), Store.YES));
+		document.add(new StringField(SearchConstants.ORDER_INVOICE_STATUS_FIELD,
+				String.valueOf(orderInvoice.getStatus()), Store.YES));
+		if (!CollectionUtils.isEmpty(orderInvoice.getDetails())) {
+			document.add(new TextField(SearchConstants.ORDER_INVOICE_DETAILS_FIELD,
+					JSONObject.toJSONString(orderInvoice.getDetails()), Store.YES));
+		}
+		return document;
+	}
+
+	/**
+	 * Purchase对象转为Document
+	 * 
+	 * @param purchase
+	 *            Purchase对象
+	 * @return 转换的Document
+	 */
+	public static Document toDocument(Purchase purchase) {
+		if (purchase == null || purchase.getId() == null || StringUtils.isEmpty(purchase.getCode())
+				|| purchase.getSellerenuu() == null || StringUtils.isEmpty(purchase.getSellerenname())
+				|| purchase.getCreatetime() == null || purchase.getStatus() == null) {
+			return null;
+		}
+		Document document = new Document();
+		document.add(new StringField(SearchConstants.PURCHASE_ID_FIELD, String.valueOf(purchase.getId()), Store.YES));
+		document.add(new TextField(SearchConstants.PURCHASE_CODE_FIELD, purchase.getCode(), Store.YES));
+		document.add(new StringField(SearchConstants.PURCHASE_SELLERENUU_FIELD,
+				String.valueOf(purchase.getSellerenuu()), Store.YES));
+		document.add(
+				new StringField(SearchConstants.PURCHASE_SELLERENNAME_FIELD, purchase.getSellerenname(), Store.YES));
+		document.add(new LongField(SearchConstants.CREATETIME_FIELD, purchase.getCreatetime().getTime(), Store.YES));
+		document.add(new StringField(SearchConstants.PURCHASE_STATUS_FIELD, String.valueOf(purchase.getStatus()),
+				Store.YES));
+		if (!CollectionUtils.isEmpty(purchase.getDetails())) {
+			document.add(new TextField(SearchConstants.PURCHASE_DETAILS_FIELD,
+					JSONObject.toJSONString(purchase.getDetails()), Store.YES));
+		}
+		return document;
+	}
+
+	/**
+	 * PurchaseInvoice对象转为Document
+	 * 
+	 * @param purchaseInvoice
+	 *            PurchaseInvoice对象
+	 * @return 转换的Document
+	 */
+	public static Document toDocument(PurchaseInvoice purchaseInvoice) {
+		if (purchaseInvoice == null || purchaseInvoice.getId() == null || StringUtils.isEmpty(purchaseInvoice.getCode())
+				|| purchaseInvoice.getSellerenuu() == null || StringUtils.isEmpty(purchaseInvoice.getSellerenname())
+				|| purchaseInvoice.getCreatetime() == null || purchaseInvoice.getStatus() == null) {
+			return null;
+		}
+		Document document = new Document();
+		document.add(new StringField(SearchConstants.PURCHASE_INVOICE_ID_FIELD, String.valueOf(purchaseInvoice.getId()),
+				Store.YES));
+		document.add(new TextField(SearchConstants.PURCHASE_INVOICE_CODE_FIELD, purchaseInvoice.getCode(), Store.YES));
+		document.add(new StringField(SearchConstants.PURCHASE_INVOICE_SELLERENUU_FIELD,
+				String.valueOf(purchaseInvoice.getSellerenuu()), Store.YES));
+		document.add(new StringField(SearchConstants.PURCHASE_INVOICE_SELLERENNAME_FIELD,
+				purchaseInvoice.getSellerenname(), Store.YES));
+		document.add(
+				new LongField(SearchConstants.CREATETIME_FIELD, purchaseInvoice.getCreatetime().getTime(), Store.YES));
+		document.add(new StringField(SearchConstants.PURCHASE_INVOICE_STATUS_FIELD,
+				String.valueOf(purchaseInvoice.getStatus()), Store.YES));
+		if (!CollectionUtils.isEmpty(purchaseInvoice.getDetails())) {
+			document.add(new TextField(SearchConstants.PURCHASE_INVOICE_DETAILS_FIELD,
+					JSONObject.toJSONString(purchaseInvoice.getDetails()), Store.YES));
+		}
+		return document;
+	}
+
+}

+ 183 - 0
mall-search/src/main/java/com/uas/search/util/ObjectUtils.java

@@ -0,0 +1,183 @@
+package com.uas.search.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.NotSerializableException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * 对象工具类
+ * 
+ * @author sunyj
+ * @since 2017年9月1日 下午3:54:26
+ */
+public class ObjectUtils {
+
+	/**
+	 * 判断是否为null、空数组、空串或者空集合
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	public static boolean isEmpty(Object obj) {
+		if (obj == null) {
+			return true;
+		}
+		if (obj.getClass().isArray()) {
+			if (obj instanceof long[]) {
+				return ArrayUtils.isEmpty((long[]) obj);
+			} else if (obj instanceof int[]) {
+				return ArrayUtils.isEmpty((int[]) obj);
+			} else if (obj instanceof short[]) {
+				return ArrayUtils.isEmpty((short[]) obj);
+			} else if (obj instanceof byte[]) {
+				return ArrayUtils.isEmpty((byte[]) obj);
+			} else if (obj instanceof char[]) {
+				return ArrayUtils.isEmpty((char[]) obj);
+			} else if (obj instanceof boolean[]) {
+				return ArrayUtils.isEmpty((boolean[]) obj);
+			} else if (obj instanceof float[]) {
+				return ArrayUtils.isEmpty((float[]) obj);
+			} else if (obj instanceof double[]) {
+				return ArrayUtils.isEmpty((double[]) obj);
+			}
+			return ArrayUtils.isEmpty((Object[]) obj);
+		}
+		return (obj instanceof String && StringUtils.isEmpty((String) obj))
+				|| (obj instanceof Collection && CollectionUtils.isEmpty((Collection<?>) obj))
+				|| (obj instanceof Map && CollectionUtils.isEmpty((Map<?, ?>) obj));
+	}
+
+	/**
+	 * 将对象转为 String
+	 * 
+	 * @param obj
+	 * @return
+	 */
+	public static String toString(Object obj) {
+		if (obj == null) {
+			return "";
+		}
+		if (obj instanceof String) {
+			return (String) obj;
+		}
+		if (obj.getClass().isArray()) {
+			if (obj instanceof long[]) {
+				return Arrays.toString((long[]) obj);
+			} else if (obj instanceof int[]) {
+				return Arrays.toString((int[]) obj);
+			} else if (obj instanceof short[]) {
+				return Arrays.toString((short[]) obj);
+			} else if (obj instanceof byte[]) {
+				return Arrays.toString((byte[]) obj);
+			} else if (obj instanceof char[]) {
+				return Arrays.toString((char[]) obj);
+			} else if (obj instanceof boolean[]) {
+				return Arrays.toString((boolean[]) obj);
+			} else if (obj instanceof float[]) {
+				return Arrays.toString((float[]) obj);
+			} else if (obj instanceof double[]) {
+				return Arrays.toString((double[]) obj);
+			}
+			return Arrays.toString((Object[]) obj);
+		}
+		return obj.toString();
+	}
+
+	/**
+	 * 深克隆对象
+	 * 
+	 * @param t
+	 *            要克隆的对象
+	 * @return 克隆得到的对象
+	 * @throws IOException
+	 * @throws ClassNotFoundException
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T extends Serializable> T clone(T t)
+			throws IOException, ClassNotFoundException, NotSerializableException {
+		if (t == null) {
+			return null;
+		}
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		new ObjectOutputStream(out).writeObject(t);
+		ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
+		return (T) in.readObject();
+	}
+
+	/**
+	 * 深克隆集合对象
+	 * 
+	 * @param collection
+	 *            要克隆的对象
+	 * @return 克隆得到的对象
+	 * @throws InstantiationException
+	 * @throws IllegalAccessException
+	 * @throws NotSerializableException
+	 * @throws ClassNotFoundException
+	 * @throws IOException
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> Collection<T> clone(Collection<T> collection) throws InstantiationException,
+			IllegalAccessException, NotSerializableException, ClassNotFoundException, IOException {
+		if (collection == null) {
+			return null;
+		}
+		Collection<T> result = collection.getClass().newInstance();
+		for (T t : collection) {
+			if (t instanceof Serializable) {
+				result.add((T) clone((Serializable) t));
+			} else if (t instanceof Collection) {
+				result.add((T) clone((Collection<?>) t));
+			} else if (t instanceof Map) {
+				result.add((T) clone((Map<String, ?>) t));
+			} else {
+				throw new IllegalArgumentException("未实现 Serializable 接口");
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * 深克隆 Map 对象
+	 * 
+	 * @param map
+	 *            要克隆的对象
+	 * @return 克隆得到的对象
+	 * @throws InstantiationException
+	 * @throws IllegalAccessException
+	 * @throws NotSerializableException
+	 * @throws ClassNotFoundException
+	 * @throws IOException
+	 */
+	@SuppressWarnings("unchecked")
+	public static <T> Map<String, T> clone(Map<String, T> map) throws InstantiationException, IllegalAccessException,
+			NotSerializableException, ClassNotFoundException, IOException {
+		if (map == null) {
+			return null;
+		}
+		Map<String, T> result = map.getClass().newInstance();
+		Set<Entry<String, T>> entrySet = map.entrySet();
+		for (Entry<String, T> entry : entrySet) {
+			T value = entry.getValue();
+			if (value instanceof Serializable) {
+				result.put(entry.getKey(), (T) clone((Serializable) value));
+			} else if (value instanceof Collection) {
+				result.put(entry.getKey(), (T) clone((Collection<?>) value));
+			} else if (value instanceof Map) {
+				result.put(entry.getKey(), (T) clone((Map<String, ?>) value));
+			} else {
+				throw new IllegalArgumentException("未实现 Serializable 接口");
+			}
+		}
+		return result;
+	}
+}

+ 182 - 0
mall-search/src/main/java/com/uas/search/util/OrderSearchUtils.java

@@ -0,0 +1,182 @@
+package com.uas.search.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.exception.SearchException;
+
+/**
+ * 单据搜索相关的工具类
+ * 
+ * @author sunyj
+ * @since 2016年10月18日 下午5:58:42
+ */
+public class OrderSearchUtils {
+
+	/**
+	 * 获取每个表在搜索关键词时,所能搜索的索引域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return 域的列表
+	 */
+	public static List<String> getKeywordFields(String tableName) {
+		List<String> keywordFields = new ArrayList<>();
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			keywordFields.add(SearchConstants.ORDER_CODE_FIELD);
+			keywordFields.add(SearchConstants.ORDER_BUYERNAME_FIELD);
+			keywordFields.add(SearchConstants.ORDER_BUYERENNAME_FIELD);
+			keywordFields.add(SearchConstants.ORDER_SELLERENNAME_FIELD);
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			keywordFields.add(SearchConstants.ORDER_INVOICE_CODE_FIELD);
+			keywordFields.add(SearchConstants.ORDER_INVOICE_BUYERNAME_FIELD);
+			keywordFields.add(SearchConstants.ORDER_INVOICE_BUYERENNAME_FIELD);
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			keywordFields.add(SearchConstants.PURCHASE_CODE_FIELD);
+			keywordFields.add(SearchConstants.PURCHASE_SELLERENNAME_FIELD);
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			keywordFields.add(SearchConstants.PURCHASE_INVOICE_CODE_FIELD);
+			keywordFields.add(SearchConstants.PURCHASE_INVOICE_SELLERENNAME_FIELD);
+		} else {
+			throw new SearchException("表不存在:" + tableName);
+		}
+		return keywordFields;
+	}
+
+	/**
+	 * 获取每个表在索引中的id域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return id域
+	 */
+	public static String getIdField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return SearchConstants.ORDER_INVOICE_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_INVOICE_ID_FIELD;
+		} else {
+			throw new SearchException("表不存在:" + tableName);
+		}
+	}
+
+	/**
+	 * 获取每个表在索引中的code域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return code域
+	 */
+	public static String getCodeField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_CODE_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return SearchConstants.ORDER_INVOICE_CODE_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_CODE_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_INVOICE_CODE_FIELD;
+		} else {
+			throw new SearchException("表不存在:" + tableName);
+		}
+	}
+
+	/**
+	 * 获取每个表在索引中的status域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return status域
+	 */
+	public static String getStatusField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_STATUS_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return SearchConstants.ORDER_INVOICE_STATUS_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_STATUS_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_INVOICE_STATUS_FIELD;
+		} else {
+			throw new SearchException("表不存在:" + tableName);
+		}
+	}
+
+	/**
+	 * 获取每个表在索引中的明细details域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return details域
+	 */
+	public static String getDetailsField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_DETAILS_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return SearchConstants.ORDER_INVOICE_DETAILS_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_DETAILS_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_INVOICE_DETAILS_FIELD;
+		} else {
+			throw new SearchException("表不存在:" + tableName);
+		}
+	}
+
+	/**
+	 * 获取每个表在索引中的buyeruu域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return buyeruu域
+	 */
+	public static String getBuyeruuField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_BUYERUU_FIELD;
+		} else {
+			throw new SearchException("表中不存在买方个人uu:" + tableName);
+		}
+	}
+
+	/**
+	 * 获取每个表在索引中的buyerenuu域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return buyerenuu域
+	 */
+	public static String getBuyerenuuField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_BUYERENUU_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return SearchConstants.ORDER_INVOICE_BUYERENUU_FIELD;
+		} else {
+			throw new SearchException("表中不存在买方企业uu:" + tableName);
+		}
+	}
+
+	/**
+	 * 获取每个表在索引中的sellerenuu域
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return sellerenuu域
+	 */
+	public static String getSellerenuuField(String tableName) {
+		if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_SELLERENUU_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_SELLERENUU_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_INVOICE_SELLERENUU_FIELD;
+		} else {
+			throw new SearchException("表中不存在卖方企业uu:" + tableName);
+		}
+	}
+
+}

+ 448 - 0
mall-search/src/main/java/com/uas/search/util/SearchUtils.java

@@ -0,0 +1,448 @@
+package com.uas.search.util;
+
+import com.uas.search.LuceneProperties;
+import com.uas.search.analyzer.IKAnalyzer;
+import com.uas.search.constant.SearchConstants;
+import com.uas.search.constant.model.SPage;
+import com.uas.search.exception.SearchException;
+import com.uas.search.model.*;
+import com.uas.search.support.IndexSearcherManager;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 搜索相关的工具类
+ * 
+ * @author sunyj
+ * @since 2016年10月17日 下午1:56:16
+ */
+public class SearchUtils {
+
+	/**
+	 * 默认的页码
+	 */
+	private static final int PAGE_INDEX = 1;
+
+	/**
+	 * 默认每页的大小
+	 */
+	private static final int PAGE_SIZE = 20;
+
+	private static Logger logger = LoggerFactory.getLogger(SearchUtils.class);
+
+	/**
+	 * IndexSearcher的管理器
+	 */
+	private static IndexSearcherManager searcherManager = IndexSearcherManager.getInstance();
+
+	private static LuceneProperties luceneProperties = ContextUtils.getBean(LuceneProperties.class);
+
+	/**
+	 * 判断搜索词是否为无效的(比如只包含特殊字符,不含有任何字母、数字、汉字等有意义的字符)
+	 * 
+	 * @param keyword
+	 * @return 搜索词是否无效
+	 */
+	public static boolean isKeywordInvalid(String keyword) {
+		if (!StringUtils.isEmpty(keyword)) {
+			// 将特殊字符剔除
+			keyword = keyword.replaceAll("[`~!@#$^&*()=|{}':;',\\[\\].<>/?~!@#¥……&*()——|{}【】‘;:”“'。,、? ]+", "");
+			return StringUtils.isEmpty(keyword);
+		}
+		return true;
+	}
+
+	/**
+	 * 对搜索词进行分词后组合得到BooleanQuery
+	 * 
+	 * @param field
+	 *            搜索的域名
+	 * @param keyword
+	 *            搜索关键词
+	 * @return
+	 */
+	public static BooleanQuery getBooleanQuery(String field, String keyword) {
+		return getBooleanQuery(field, keyword, true, true, Occur.MUST);
+	}
+
+	/**
+	 * 对搜索词进行分词后组合得到BooleanQuery
+	 *
+	 * @param field
+	 *            搜索的域名
+	 * @param keyword
+	 *            搜索关键词
+	 * @param useRegexpQuery
+	 *            是否使用 RegexpQuery
+	 * @return
+	 */
+	public static BooleanQuery getBooleanQuery(String field, String keyword, boolean useRegexpQuery) {
+		return getBooleanQuery(field, keyword, true, true, Occur.MUST, useRegexpQuery);
+	}
+
+	/**
+	 * 对搜索词进行分词后组合得到BooleanQuery
+	 * 
+	 * @param field
+	 *            搜索的域名
+	 * @param keyword
+	 *            搜索关键词
+     * @param useSmart
+     *            是否以最大粒度进行分词
+	 * @param loadExtDic
+	 *            是否加载自定义词典
+	 * @param occur
+	 *            多个Query之间的关系
+	 * @return
+	 */
+	public static BooleanQuery getBooleanQuery(String field, String keyword, boolean useSmart, boolean loadExtDic, Occur occur) {
+		return getBooleanQuery(field, keyword, useSmart, loadExtDic, occur, false);
+	}
+
+	/**
+	 * 对搜索词进行分词后组合得到BooleanQuery
+	 *
+	 * @param field
+	 *            搜索的域名
+	 * @param keyword
+	 *            搜索关键词
+	 * @param useSmart
+	 *            是否以最大粒度进行分词
+	 * @param loadExtDic
+	 *            是否加载自定义词典
+	 * @param occur
+	 *            多个Query之间的关系
+	 * @param useRegexpQuery
+	 *            是否使用 RegexpQuery
+	 * @return
+	 */
+	public static BooleanQuery getBooleanQuery(String field, String keyword, boolean useSmart, boolean loadExtDic, Occur occur, boolean useRegexpQuery) {
+		if (StringUtils.isEmpty(field) || StringUtils.isEmpty(keyword)) {
+			return null;
+		}
+		BooleanQuery booleanQuery = new BooleanQuery();
+		Analyzer analyzer = new IKAnalyzer(useSmart, loadExtDic);
+		try {
+			TokenStream tokenStream = analyzer.tokenStream(field, keyword);
+			tokenStream.reset();
+			CharTermAttribute cta = tokenStream.addAttribute(CharTermAttribute.class);
+			while (tokenStream.incrementToken()) {
+				if(!useRegexpQuery){
+					booleanQuery.add(new PrefixQuery(new Term(field, cta.toString())), occur);
+				} else{
+					booleanQuery.add(new RegexpQuery(new Term(field, ".*" + cta.toString().toLowerCase() + ".*")), occur);
+				}
+			}
+			tokenStream.close();
+			analyzer.close();
+		} catch (IOException e) {
+			logger.error("", e);
+		}
+		return booleanQuery;
+	}
+
+	/**
+	 * 对搜索词进行分词后组合得到RegexpQuery
+	 * 
+	 * @param field
+	 *            搜索的域名
+	 * @param keyword
+	 *            搜索关键词
+	 * @return
+	 */
+	// 如果用正则表达式进行搜索,就不需要再分词(分词的字段可能会导致边界问题)
+	public static RegexpQuery getRegexpQuery(String field, String keyword) {
+		if (StringUtils.isEmpty(field)) {
+			return null;
+		}
+		// TODO 针对的是分词了的字段(未分词的字段可能搜索不准确)
+		return new RegexpQuery(new Term(field, ".*" + keyword.toLowerCase() + ".*"));
+	}
+
+	/**
+	 * 获取指定表的IndexSearcher对象,若为空,抛出异常
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @return IndexSearcher对象
+	 */
+	public static IndexSearcher getIndexSearcher(String tableName) {
+		IndexSearcher indexSearcher = searcherManager.get(tableName);
+		if (indexSearcher == null) {
+			throw new SearchException("获取索引文件失败");
+		}
+		return indexSearcher;
+	}
+
+	/**
+	 * 释放indexSearcher
+	 * 
+	 * @param indexSearcher
+	 */
+	public static void releaseIndexSearcher(IndexSearcher indexSearcher) {
+		searcherManager.release(indexSearcher);
+	}
+
+	/**
+	 * 根据域和搜索词获取指定表的Document(每个id最多只能对应一条数据)
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param field
+	 *            搜索域
+	 * @param id
+	 *            搜索词
+	 * @return Document
+	 */
+	public static Document getDocumentById(String tableName, String field, Long id) {
+		if (id == null) {
+			throw new SearchException("输入无效:" + id);
+		}
+		return getDocumentById(tableName, field, Long.toString(id));
+	}
+
+	/**
+	 * 根据域和搜索词获取指定表的Document(每个id最多只能对应一条数据)
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param field
+	 *            搜索域
+	 * @param id
+	 *            搜索词
+	 * @return Document
+	 */
+	public static Document getDocumentById(String tableName, String field, String id) {
+		if (StringUtils.isEmpty(field) || StringUtils.isEmpty(id)) {
+			throw new SearchException("搜索的域和搜索词不能为空");
+		}
+		TermQuery termQuery = new TermQuery(new Term(field, id));
+		List<Document> documents = getDocuments(tableName, termQuery).getContent();
+		if (CollectionUtils.isEmpty(documents)) {
+			return null;
+		} else if (documents.size() > 1) {
+			throw new SearchException("索引中存在不止一个对象:" + field + "=" + id);
+		}
+		return documents.get(0);
+	}
+
+	/**
+	 * 根据查询条件获取指定表的Document列表
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param query
+	 *            查询条件
+	 * @return
+	 */
+	public static SPage<Document> getDocuments(String tableName, Query query) {
+		return getDocuments(tableName, query, null, null);
+	}
+
+	/**
+	 * 根据查询条件获取指定表的Document列表
+	 * 
+	 * @param tableName
+	 *            表名
+	 * @param query
+	 *            查询条件
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 * @return
+	 */
+	public static SPage<Document> getDocuments(String tableName, Query query, Integer page, Integer size) {
+		return getDocuments(tableName, query, null, page, size);
+	}
+
+	/**
+	 * 根据查询条件获取指定表的Document列表
+	 *
+	 * @param tableName
+	 *            表名
+	 * @param query
+	 *            查询条件
+	 * @param sort 排序规则
+	 * @param page
+	 *            页码
+	 * @param size
+	 *            页大小
+	 * @return
+	 */
+	public static SPage<Document> getDocuments(String tableName, Query query, Sort sort, Integer page, Integer size) {
+		if (query == null) {
+			throw new SearchException("query不能为null");
+		}
+		SPage<Document> sPage = new SPage<>();
+		if (page != null && page > 0) {
+			sPage.setPage(page);
+		} else {
+			sPage.setPage(PAGE_INDEX);
+			sPage.setFirst(true);
+		}
+		if (size != null && size > 0) {
+			sPage.setSize(size);
+		} else {
+			sPage.setSize(PAGE_SIZE);
+		}
+		IndexSearcher indexSearcher = getIndexSearcher(tableName);
+
+		TopDocs topDocs;
+		try {
+			// 如果页码不为1
+			if (sPage.getPage() > 1) {
+				TopDocs previousTopDocs;
+				previousTopDocs = sort == null ? indexSearcher.search(query, (sPage.getPage() - 1) * sPage.getSize()) :
+						indexSearcher.search(query, (sPage.getPage() - 1) * sPage.getSize(), sort);
+				int totalHits = previousTopDocs.totalHits;
+				ScoreDoc[] previousScoreDocs = previousTopDocs.scoreDocs;
+				if ((sPage.getPage() - 1) * sPage.getSize() >= totalHits) {
+					throw new SearchException("页码过大:元素总数量为" + totalHits);
+				}
+				topDocs = sort == null ? indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], query, sPage.getSize()) :
+						indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], query, sPage.getSize(), sort);
+			} else {
+				sPage.setFirst(true);
+				topDocs = sort == null ? indexSearcher.search(query, sPage.getSize()) : indexSearcher.search(query, sPage.getSize(), sort);
+			}
+
+			int totalHits = topDocs.totalHits;
+			// 设置总元素个数、页数等信息
+			sPage.setTotalElement(totalHits);
+			int totalPage = (int) Math.ceil(totalHits / (1.0 * sPage.getSize()));
+			sPage.setTotalPage(totalPage);
+			if (totalPage == sPage.getPage()) {
+				sPage.setLast(true);
+			}
+
+			List<Document> documents = new ArrayList<>();
+			for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
+				documents.add(indexSearcher.doc(scoreDoc.doc));
+			}
+
+			sPage.setContent(documents);
+		} catch (IOException e) {
+			throw new SearchException(e).setDetailedMessage(e);
+		} finally {
+			releaseIndexSearcher(indexSearcher);
+		}
+		return sPage;
+	}
+
+	/**
+	 * 获取各表索引的id字段名
+	 * 
+	 * @param tableName
+	 * @return
+	 */
+	public static String getIdField(String tableName) {
+		if (tableName == null) {
+			return null;
+		}
+		if (tableName.equals(SearchConstants.KIND_TABLE_NAME)) {
+			return SearchConstants.KIND_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.BRAND_TABLE_NAME)) {
+			return SearchConstants.BRAND_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.COMPONENT_TABLE_NAME)) {
+			return SearchConstants.COMPONENT_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_TABLE_NAME)) {
+			return SearchConstants.ORDER_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.ORDER_INVOICE_TABLE_NAME)) {
+			return SearchConstants.ORDER_INVOICE_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_ID_FIELD;
+		} else if (tableName.equals(SearchConstants.PURCHASE_INVOICE_TABLE_NAME)) {
+			return SearchConstants.PURCHASE_INVOICE_ID_FIELD;
+		}
+		throw new SearchException("不支持的表:" + tableName);
+	}
+
+	/**
+	 * 获取所有需要建立索引的表名
+	 * 
+	 * @return
+	 */
+	public static List<String> getTableNames() {
+		List<String> tableNames = new ArrayList<>();
+		tableNames.add(SearchConstants.KIND_TABLE_NAME);
+		tableNames.add(SearchConstants.BRAND_TABLE_NAME);
+		tableNames.add(SearchConstants.COMPONENT_TABLE_NAME);
+		tableNames.add(SearchConstants.GOODS_TABLE_NAME);
+		tableNames.add(SearchConstants.ORDER_TABLE_NAME);
+		tableNames.add(SearchConstants.ORDER_INVOICE_TABLE_NAME);
+		tableNames.add(SearchConstants.PURCHASE_TABLE_NAME);
+		tableNames.add(SearchConstants.PURCHASE_INVOICE_TABLE_NAME);
+		return tableNames;
+	}
+
+	/**
+	 * 获取指定的实体类对应的需要建立索引的表名
+	 * 
+	 * @param clazz
+	 * @return
+	 */
+	public static String getTableName(Class<?> clazz) {
+		if (clazz == null) {
+			return null;
+		}
+		if (clazz == Kind.class) {
+			return SearchConstants.KIND_TABLE_NAME;
+		} else if (clazz == Brand.class) {
+			return SearchConstants.BRAND_TABLE_NAME;
+		} else if (clazz == Component.class) {
+			return SearchConstants.COMPONENT_TABLE_NAME;
+		} else if (clazz == Goods.class) {
+			return SearchConstants.GOODS_TABLE_NAME;
+		} else if (clazz == Order.class) {
+			return SearchConstants.ORDER_TABLE_NAME;
+		} else if (clazz == OrderInvoice.class) {
+			return SearchConstants.ORDER_INVOICE_TABLE_NAME;
+		} else if (clazz == Purchase.class) {
+			return SearchConstants.PURCHASE_TABLE_NAME;
+		} else if (clazz == PurchaseInvoice.class) {
+			return SearchConstants.PURCHASE_INVOICE_TABLE_NAME;
+		} else {
+			throw new SearchException("该实体类没有对应的需要建立索引的表:" + clazz);
+		}
+	}
+
+	/**
+	 * 获取指定表的索引路径
+	 * 
+	 * @param tableName
+	 *            指定表
+	 * @return 索引路径
+	 */
+	public static String getIndexPath(String tableName) {
+		if (tableName == null) {
+			return null;
+		}
+		return luceneProperties.getIndexesDir() + "/" + tableName;
+	}
+
+	/**
+	 * 获取指定表的数据路径
+	 * 
+	 * @param tableName
+	 *            指定表
+	 * @return 数据路径
+	 */
+	public static String getDataPath(String tableName) {
+		if (tableName == null) {
+			return null;
+		}
+		return luceneProperties.getDataDir() + "/" + tableName;
+	}
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно