Ver código fonte

first commit

zxj 10 meses atrás
commit
648f504771
100 arquivos alterados com 13959 adições e 0 exclusões
  1. 63 0
      .gitignore
  2. 661 0
      LICENSE
  3. 174 0
      README.md
  4. 11 0
      app/.gitignore
  5. BIN
      app/assets/OpenSans-Light.ttf
  6. BIN
      app/assets/OpenSans-Regular.ttf
  7. BIN
      app/assets/bd_etts_common_speech_as_mand_eng_high_am_v3.0.0_20170516.dat
  8. BIN
      app/assets/bd_etts_common_speech_f7_mand_eng_high_am-mix_v3.0.0_20170512.dat
  9. BIN
      app/assets/bd_etts_common_speech_m15_mand_eng_high_am-mix_v3.0.0_20170505.dat
  10. BIN
      app/assets/bd_etts_common_speech_yyjw_mand_eng_high_am-mix_v3.0.0_20170512.dat
  11. BIN
      app/assets/bd_etts_text.dat
  12. BIN
      app/assets/flutter_shared/icudtl.dat
  13. BIN
      app/assets/fonts/fontawesome-webfont.ttf
  14. BIN
      app/assets/goodgoodstudy.zip
  15. 83 0
      app/assets/html/huawei.html
  16. 78 0
      app/assets/html/xiaomi.html
  17. BIN
      app/assets/j_qinqin.png
  18. 27 0
      app/assets/kaomoji
  19. 4070 0
      app/assets/province_data.xml
  20. 8 0
      app/assets/server.json
  21. 328 0
      app/build.gradle
  22. BIN
      app/debug.keystore
  23. BIN
      app/libs/BaiduLBS_Android.jar
  24. BIN
      app/libs/arm64-v8a/libBaiduMapSDK_base_v7_5_2.so
  25. BIN
      app/libs/arm64-v8a/libBaiduMapSDK_map_v7_5_2.so
  26. BIN
      app/libs/arm64-v8a/libgnustl_shared.so
  27. BIN
      app/libs/arm64-v8a/liblocSDK8b.so
  28. BIN
      app/libs/armeabi-v7a/libBaiduMapSDK_base_v7_5_2.so
  29. BIN
      app/libs/armeabi-v7a/libBaiduMapSDK_map_v7_5_2.so
  30. BIN
      app/libs/armeabi-v7a/libgnustl_shared.so
  31. BIN
      app/libs/armeabi-v7a/liblocSDK8b.so
  32. BIN
      app/libs/bdasr_V3_20180320_9066860.jar
  33. BIN
      app/libs/changeskin-1.3.0.aar
  34. BIN
      app/libs/com.baidu.tts_2.3.1.20170808_e39ea89.jar
  35. BIN
      app/libs/material-calendarview-fancy-1.1.aar
  36. BIN
      app/libs/pinyin4j-2.5.0.jar
  37. BIN
      app/libs/universal-image-loader-1.9.5.jar
  38. 0 0
      app/multidex_keep_file.pro
  39. 367 0
      app/proguard-rules.pro
  40. 496 0
      app/src/main/AndroidManifest.xml
  41. 373 0
      app/src/main/java/com/bigkoo/convenientbanner/ConvenientBanner.java
  42. 72 0
      app/src/main/java/com/bigkoo/convenientbanner/OvalViewOutlineProvider.java
  43. 46 0
      app/src/main/java/com/bigkoo/convenientbanner/RoundViewOutlineProvider.java
  44. 49 0
      app/src/main/java/com/bigkoo/convenientbanner/ViewPagerScroller.java
  45. 53 0
      app/src/main/java/com/bigkoo/convenientbanner/ViewStyleSetter.java
  46. 107 0
      app/src/main/java/com/bigkoo/convenientbanner/adapter/CBPageAdapter.java
  47. 10 0
      app/src/main/java/com/bigkoo/convenientbanner/holder/CBViewHolderCreator.java
  48. 14 0
      app/src/main/java/com/bigkoo/convenientbanner/holder/Holder.java
  49. 45 0
      app/src/main/java/com/bigkoo/convenientbanner/listener/CBPageChangeListener.java
  50. 8 0
      app/src/main/java/com/bigkoo/convenientbanner/listener/OnItemClickListener.java
  51. 26 0
      app/src/main/java/com/bigkoo/convenientbanner/transformer/ScaleYTransformer.java
  52. 168 0
      app/src/main/java/com/bigkoo/convenientbanner/view/CBLoopViewPager.java
  53. 179 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/O2App.kt
  54. 207 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/O2AppUpdateManager.kt
  55. 132 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/VideoPlayerActivity.kt
  56. 447 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/FastCheckInManager.kt
  57. 307 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceAppealActivity.kt
  58. 19 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceAppealContract.kt
  59. 66 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceAppealPresenter.kt
  60. 233 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceV2AppealActivity.kt
  61. 27 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceV2AppealContract.kt
  62. 127 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceV2AppealPresenter.kt
  63. 236 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/approval/AttendanceAppealApprovalActivity.kt
  64. 18 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/approval/AttendanceAppealApprovalContract.kt
  65. 70 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/approval/AttendanceAppealApprovalPresenter.kt
  66. 145 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/list/AttendanceListActivity.kt
  67. 18 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/list/AttendanceListContract.kt
  68. 64 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/list/AttendanceListPresenter.kt
  69. 16 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceChartContract.kt
  70. 133 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceChartFragment.kt
  71. 41 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceChartPresenter.kt
  72. 27 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInContract.kt
  73. 331 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInFragment.kt
  74. 521 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInNewFragment.kt
  75. 144 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInPresenter.kt
  76. 18 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInV2Contract.kt
  77. 450 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInV2NewFragment.kt
  78. 54 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInV2Presenter.kt
  79. 175 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceMainActivity.kt
  80. 15 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceMainContract.kt
  81. 33 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceMainPresenter.kt
  82. 25 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticContract.kt
  83. 141 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticFragment.kt
  84. 160 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticPresenter.kt
  85. 24 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticV2Contract.kt
  86. 86 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticV2Fragment.kt
  87. 35 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticV2Presenter.kt
  88. 250 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/setting/AttendanceLocationSettingActivity.kt
  89. 20 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/setting/AttendanceLocationSettingContract.kt
  90. 82 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/setting/AttendanceLocationSettingPresenter.kt
  91. 250 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPActivity.kt
  92. 53 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPFragment.kt
  93. 128 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPViewPagerFragment.kt
  94. 94 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseO2Activity.kt
  95. 94 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseO2BindActivity.kt
  96. 234 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseO2ViewModel.kt
  97. 22 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BasePresenter.kt
  98. 572 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BasePresenterImpl.kt
  99. 14 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseView.kt
  100. 85 0
      app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/bbs/main/BBSMainActivity.kt

+ 63 - 0
.gitignore

@@ -0,0 +1,63 @@
+/PushSDK/build/
+.DS_Store
+/captures
+.svn/
+
+autopacking.sh
+
+reports/
+
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# Intellij
+*.iml
+.idea/
+
+# VsCode
+.vscode/
+
+# Keystore files
+*.jks
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json

+ 661 - 0
LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    o2oa-android
+    Copyright (C) 2020  o2oa
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.

+ 174 - 0
README.md

@@ -0,0 +1,174 @@
+# O2OA 企业信息化办公平台 Android端源码
+
+
+
+O2OA 平台Android客户端,最低支持Android版本5.0 Android L,编程语言主要使用kotlin。
+
+
+
+## 环境安装
+
+请使用最新版本的`Android Studio`进行导入编译,编译的Android SDK版本是 30。 `Android Studio`下载地址https://developer.android.google.cn/studio/
+
+
+
+安装Android Studio完成后,打开设置里面的SDK Manager工具。
+
+![image](./img/1577344557429-5f5242d4-89b0-426e-bf59-057a2e52686f.png)
+
+选择**Android 12.0 (S)** ,安装SDK。
+
+![image.png](./img/20221125-sdk.png)
+
+然后选择SDK Tools 选项卡,勾选右下角的Show Package Details,然后选择Android SDK Build-Tools 下面的**30.0.3**版本进行安装。
+
+![image.png](./img/2022-03-31_10.17.07.png)
+
+
+
+
+
+## 导入项目
+
+打开`Android Studio` 点击`Open an existing Android Studio project`
+
+
+
+![image](./img/1577344557509-a21b55b1-4241-4b5a-b642-aa6c74c8e549.png)
+
+
+
+等它加载完成后可能会弹出这样一个窗口,让你更新gradle版本,这个不需要更新,点击 `Donot remind me again for this project`
+
+
+
+![image](./img/1577344558957-4a1ed875-17de-4d35-9a53-1fc14de8caff.png)
+
+
+
+
+
+## 项目基本信息修改
+
+### 项目名称
+
+项目名称就是Android手机桌面上显示的名称,这个名称修改是在strings资源文件中:
+
+具体位置:`./app/src/main/res/values/strings.xml`
+
+
+
+![image](./img/1577344557451-0d30193a-801c-43bd-94a5-b60ac07773a5.png)
+
+
+
+### 项目桌面LOGO
+
+还有就是项目的LOGO,这个LOGO是在Android手机桌面上显示那个图标,这个需要替换图片: 图片位置: `./app/src/main/res` 这个资源目录下有4个 `mipmap` 文件夹,把4个文件夹中的`logo.png`和`logo_round.png`都替换了。
+
+
+
+|                                                              |                                                              |
+| ------------------------------------------------------------ | ------------------------------------------------------------ |
+| ![image](./img/1577344557413-0780e7c6-8597-4239-a47a-b2aef0b879dc.png) | ![image](./img/1577344557674-0bc05c23-9c26-409c-b2e4-5f16f92e7f8b.png) |
+
+
+
+### 唯一应用ID
+
+
+
+Android应用都有一个唯一的应用ID `applicationId` 。这个id代表这个Android App所以不能重复,一般都用公司域名组成的一串字符串,如: `com.baidu.app.xxx` 。 修改的文件是在`./app/build.gradle` 文件中,android -> defaultConfig -> applicationId 。
+
+![image](./img/1577344557438-dee08395-93fd-427a-86be-871cc8701316.png)
+
+### 可配置的应用内图标
+App内看到的一些O2OA相关的logo图标,可以不编译打包进App,我们服务端可以进行动态配置。用管理员进入我们O2OA的服务端,找到系统设置->移动办公配置->样式配置,就可以修改图标了:
+
+![image](./img/20221125-image-style.png)
+
+## 第三方SDK配置
+
+找到项目根目录下的 gradle.properties 文件,里面有一些第三方SDK的key需要配置。
+#下面是第三方SDK 需要的key
+#极光推送
+JPUSH_APPKEY_DEBUG=极光推送AppKey
+
+#百度地图
+BAIDU_MAP_APPKEY=百度地图Appkey
+
+
+
+在`local.properties`文件中还有打包签名证书的信息需要配置,如果用Android studio打包,这几个参数可为空:
+
+```
+signingConfig.keyAlias=密钥别名
+signingConfig.keyPassword=密码
+signingConfig.storeFilePath=密钥文件
+signingConfig.storePassword=存储密码
+```
+
+
+## 直连版本配置 
+
+如果你不希望连接到我们O2云,通过修改配置编译打包,生成的Android应用,就会直接连接到你们自己的O2OA服务器。 
+
+直连版本的极光消息推送需要额外配置:[《O2OA移动端APP直连版本如何开启消息推送》](https://www.yuque.com/o2oa/cfst8l/mws5lw)
+
+修改方式如下: 在app目录下的build.gradle文件中,找到 android -> buildTypes ,把`InnerServer` 改成 `true` 。这里应用有两个 一个debug下的 一个是release下的
+
+buildConfigField "Boolean", "InnerServer", "true"
+
+![image](./img/1577344559312-11600328-394a-4b1a-8798-b85c1af59219.png)
+
+然后找到app->assets 目录下找到server.json文件,把里面的centerHost 、 centerPort、httpProtocol改成你们自己的O2OA中心服务器地址信息。
+
+![image](./img/1577344559030-1735ff15-8980-42cf-a8c8-220d9d8ff200.png)
+
+
+
+## 打包Android apk
+
+打包还是通过Android Studio工具。上面提到的生成签名文件的时候一样,找到菜单 Build -> Generate Signed Bundle or APK 。
+
+
+
+![image](./img/1577344557829-5b7d13c2-9b8c-4743-abb6-b7df21af84f9.png)
+
+
+
+然后勾选APK ,下一步。 
+
+
+
+![image](./img/1577344558176-f5c3d53c-4483-4070-9164-49de977ef72e.png)
+
+
+
+这次不用新建了因为刚才已经创建好签名文件了,选择你刚才生成的签名文件,输入密码,然后继续。 
+
+
+
+![image](./img/1577344558020-ea1b6356-e766-4b1f-a7f6-eff15dae770f.png)
+
+这里的签名版本V1和V2都勾选。点击 `Finish` 就开始打包了。 打包完成后,APK文件在 `./app/O2PLATFORM/release/` 目录下 。
+
+
+
+
+
+# 协议
+
+[AGPL-3.0 开源协议。](./LICENSE)
+
+
+
+# 关于
+
+[![img](./img/O2OA-logo.jpg)](./img/O2OA-logo.jpg)
+
+
+
+O2OA开发平台是由 **浙江兰德纵横网路技术股份有限公司** 建立和维护的。O2OA 的名字和标志是属于 **浙江兰德纵横网路技术股份有限公司** 的注册商标。
+
+我们 ❤️ 开源软件!看一下[我们的其他开源项目](https://github.com/o2oa),瞅一眼[我们的博客](https://my.oschina.net/o2oa)。

+ 11 - 0
app/.gitignore

@@ -0,0 +1,11 @@
+/build
+.project
+.classpath
+.settings/
+# VsCode
+.vscode/
+
+# RELEASE
+O2PLATFORM/
+huawei/
+xiaomi/

BIN
app/assets/OpenSans-Light.ttf


BIN
app/assets/OpenSans-Regular.ttf


BIN
app/assets/bd_etts_common_speech_as_mand_eng_high_am_v3.0.0_20170516.dat


BIN
app/assets/bd_etts_common_speech_f7_mand_eng_high_am-mix_v3.0.0_20170512.dat


BIN
app/assets/bd_etts_common_speech_m15_mand_eng_high_am-mix_v3.0.0_20170505.dat


BIN
app/assets/bd_etts_common_speech_yyjw_mand_eng_high_am-mix_v3.0.0_20170512.dat


BIN
app/assets/bd_etts_text.dat


BIN
app/assets/flutter_shared/icudtl.dat


BIN
app/assets/fonts/fontawesome-webfont.ttf


BIN
app/assets/goodgoodstudy.zip


+ 83 - 0
app/assets/html/huawei.html

@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>O2-帮助</title>
+    <style type="text/css">
+        body {
+            padding: 10px;
+        }
+    </style>
+    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
+    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
+          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
+    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"
+            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
+            crossorigin="anonymous"></script>
+
+</head>
+<body>
+<div class="panel panel-default">
+    <div class="panel-heading">
+        <h3 class="panel-title"> 华为消息提醒设置说明 </h3>
+    </div>
+    <div class="panel-body">
+        <p class="p1">
+            华为手机需要开启O2自动启动、关闭锁屏清理应用和开启O2通知,主要是解决使用O2无法及时收到新的消息通知(需要重新打开O2才能收到新的消息提醒)。
+        </p>
+        <p>
+            <span style="color: rgb(255, 0, 0);">PS:如果你能接收消息通知,请忽略</span>
+            <br><br>
+            <strong>导致原因:</strong>
+            <br>
+            因为华为手机系统在黑屏待机后自动清理后台运行的软件,这样影响了我们正常接收新的消息,需要将O2设置为非清理应用。
+        </p>
+        <p class="p1">
+            <strong>如何设置:</strong>
+        </p>
+        <p class="p1">
+            <span class="s1">1.</span>关闭锁屏清理应用
+        </p>
+        <p class="p3">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">设置-电池-锁屏清理应用,找到O2,关闭锁屏清理开关,参见下面的GIF动画:</span>
+            <br>
+            <img alt="" src="https://app.o2oa.net/o2/20170920/%E9%94%81%E5%B1%8F%E6%B8%85%E7%90%86.gif"
+                 style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+        <p class="p1">
+            <span class="s1">2.</span>开启自动运行
+        </p>
+        <p class="p3">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">找到手机管家-自启管理,找到O2并允许自启动,参见下面的GIF动画:</span>
+            <br>
+            <img alt="" src="https://app.o2oa.net/o2/20170920/%E5%BC%80%E5%90%AF%E8%87%AA%E5%90%AF%E5%8A%A8.gif"
+                 style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+        <p class="p1"><span class="s1">3.</span>通知设置</p>
+        <p class="p1">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">设置-通知和状态栏-通知管理,找到O2-开启允许通知及其他开关。参见下面的GIF动画:</span>
+            <br><img alt="" src="https://app.o2oa.net/o2/20170920/%E9%80%9A%E7%9F%A5%E8%AE%BE%E7%BD%AE.gif"
+                     style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+        <p class="p1"><span class="s1">4.</span>多任务界面锁定O2</p>
+        <p class="p1">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">点击手机右下角的功能键-进入多任务页面-找到O2,点击O2右上角锁图标锁定。参见下面的GIF动画:</span>
+            <br><img alt="" src="https://app.o2oa.net/o2/20170920/%E5%A4%9A%E4%BB%BB%E5%8A%A1.gif"
+                     style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+
+    </div>
+</div>
+</body>
+</html>

+ 78 - 0
app/assets/html/xiaomi.html

@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>O2-帮助</title>
+    <style type="text/css">
+        body {
+            padding: 10px;
+        }
+    </style>
+    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
+    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css"
+          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
+    <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
+    <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"
+            integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
+            crossorigin="anonymous"></script>
+
+</head>
+<body>
+<div class="panel panel-default">
+    <div class="panel-heading">
+        <h3 class="panel-title"> 小米手机消息提醒设置说明 </h3>
+    </div>
+    <div class="panel-body">
+        <p>小米手机需要开启O2自动运行应用程序,主要是解决使用O2无法及时收到新的消息通知(需要重新打开O2才能收到新的消息提醒)。</p>
+        <p>
+            <span style="color: rgb(255, 0, 0);">PS:如果你能接收消息通知,请忽略</span>
+            <br><br>
+            <strong>导致原因:</strong>
+            <br>
+            因为小米手机系统在黑屏待机后自动清理后台运行的软件,这样影响了我们正常接收新的消息,需要将O2设置为自动运行应用程序。
+        </p>
+        <p class="p1">
+            <strong>如何设置:</strong>
+        </p>
+        <p class="p1">
+            <span class="s1">1.</span>开启自动启动
+        </p>
+        <p class="p3">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">手机找到并点击 安全中心-授权管理-自启动管理-找到O2开启开关。参见下面的GIF动画:</span>
+            <br>
+            <img alt="" src="https://app.o2oa.net/o2/20170921/%E5%B0%8F%E7%B1%B3%E8%87%AA%E5%90%AF%E5%8A%A8.gif"
+                 style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+        <p class="p1"><span class="s1">2.</span>神隐模式</p>
+        <p class="p1">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">手机设置-电量和性能-神隐模式-应用配置-找到O2-点击无限制并允许定位。参见下面的GIF动画:</span>
+            <br><img alt="" src="https://app.o2oa.net/o2/20170921/%E5%B0%8F%E7%B1%B3%E7%A5%9E%E9%9A%90%E6%A8%A1%E5%BC%8F.gif"
+                     style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+        <p class="p1"><span class="s1">3.</span>通知设置</p>
+        <p class="p1">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">手机设置-通知和状态栏-通知管理-找到O2-开启允许通知和其他选项。参见下面的GIF动画:</span>
+            <br><img alt="" src="https://app.o2oa.net/o2/20170921/%E5%B0%8F%E7%B1%B3%E9%80%9A%E7%9F%A5%E8%AE%BE%E7%BD%AE.gif"
+                     style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+        <p class="p1"><span class="s1">4.</span>多任务界面锁定O2</p>
+        <p class="p1">
+            <span class="s2">操作步骤:</span>
+            <span class="s3">点击手机左下角的功能键-进入多任务页面-找到O2-拖曳O2下滑-点击锁定任务。参见下面的GIF动画:</span>
+            <br><img alt="" src="https://app.o2oa.net/o2/20170921/%E5%B0%8F%E7%B1%B3%E5%A4%9A%E4%BB%BB%E5%8A%A1.gif"
+                     style="width: 100%;">
+        </p>
+        <p class="p2">&nbsp;</p>
+
+    </div>
+</div>
+</body>
+</html>

BIN
app/assets/j_qinqin.png


+ 27 - 0
app/assets/kaomoji

@@ -0,0 +1,27 @@
+(๑‾ ꇴ ‾๑)好哒
+๑乛◡乛๑嘿嘿
+(/ω·\*)捂脸
+(๑⁼̴̀д⁼̴́๑)可恶
+(๑•॒̀ ູ॒•́๑)啦啦啦
+(づ ̄³ ̄)づ抱抱
+("▔□▔)汗
+<(ˉ^ˉ)>哼
+( ꒪Д꒪)吓尿
+( ・᷄д・᷅ )委屈
+(  ˃᷄˶˶̫˶˂᷅  )羞
+눈_눈
+Ծ‸Ծ
+(๑˙ー˙๑)
+( ´・ᴗ・` )
+٩(˃̶͈̀௰˂̶͈́)و加油
+(⺣◡⺣)♡喜欢哒
+(= ̄ω ̄=)喵
+(σ゚∀゚)σ呦呦
+ԅ( ¯་། ¯ԅ)
+◝( ˙ ꒳ ˙ )◜
+(っ˘̩╭╮˘̩)っ
+(⁍̥̥̥᷄д⁍̥̥̥᷅ ू )伤心
+( ・᷄ ᵌ・᷅ )我要嘛
+(ಥ_ಥ)爱不爱我
+(◕‿◕✿)
+╮( ̄⊿ ̄”)╭无奈

+ 4070 - 0
app/assets/province_data.xml

@@ -0,0 +1,4070 @@
+<root>
+    <province name="安徽省">
+        <city name="安庆市">
+            <district name="枞阳县" zipcode="246000" />
+            <district name="大观区" zipcode="246000" />
+            <district name="怀宁县" zipcode="246000" />
+            <district name="潜山县" zipcode="246000" />
+            <district name="宿松县" zipcode="246000" />
+            <district name="太湖县" zipcode="246000" />
+            <district name="桐城市" zipcode="246000" />
+            <district name="望江县" zipcode="246000" />
+            <district name="宜秀区" zipcode="246000" />
+            <district name="迎江区" zipcode="246000" />
+            <district name="岳西县" zipcode="246000" />
+            <district name="其他" zipcode="246000" />
+        </city>
+        <city name="蚌埠市">
+            <district name="蚌山区" zipcode="233000" />
+            <district name="固镇县" zipcode="233000" />
+            <district name="怀远县" zipcode="233000" />
+            <district name="淮上区" zipcode="233000" />
+            <district name="龙子湖区" zipcode="233000" />
+            <district name="五河县" zipcode="233000" />
+            <district name="禹会区" zipcode="233000" />
+            <district name="其他" zipcode="233000" />
+        </city>
+        <city name="亳州市">
+            <district name="利辛县" zipcode="236000" />
+            <district name="蒙城县" zipcode="236000" />
+            <district name="谯城区" zipcode="236000" />
+            <district name="涡阳县" zipcode="236000" />
+            <district name="其他" zipcode="236000" />
+        </city>
+        <city name="池州市">
+            <district name="东至县" zipcode="247100" />
+            <district name="贵池区" zipcode="247100" />
+            <district name="青阳县" zipcode="247100" />
+            <district name="石台县" zipcode="247100" />
+            <district name="其他" zipcode="247100" />
+        </city>
+        <city name="滁州市">
+            <district name="定远县" zipcode="239000" />
+            <district name="凤阳县" zipcode="239000" />
+            <district name="来安县" zipcode="239000" />
+            <district name="琅琊区" zipcode="239000" />
+            <district name="明光市" zipcode="239000" />
+            <district name="南谯区" zipcode="239000" />
+            <district name="全椒县" zipcode="239000" />
+            <district name="天长市" zipcode="239000" />
+            <district name="其他" zipcode="239000" />
+        </city>
+        <city name="阜阳市">
+            <district name="阜南县" zipcode="236100" />
+            <district name="界首市" zipcode="236100" />
+            <district name="临泉县" zipcode="236100" />
+            <district name="太和县" zipcode="236100" />
+            <district name="颍东区" zipcode="236100" />
+            <district name="颍泉区" zipcode="236100" />
+            <district name="颍上县" zipcode="236100" />
+            <district name="颍州区" zipcode="236100" />
+            <district name="其他" zipcode="236100" />
+        </city>
+        <city name="合肥市">
+            <district name="包河区" zipcode="230000" />
+            <district name="长丰县" zipcode="230000" />
+            <district name="肥东县" zipcode="230000" />
+            <district name="肥西县" zipcode="230000" />
+            <district name="庐江县" zipcode="230000" />
+            <district name="蜀山区" zipcode="230000" />
+            <district name="瑶海区" zipcode="230000" />
+            <district name="巢湖市" zipcode="230000" />
+            <district name="其他" zipcode="230000" />
+        </city>
+        <city name="淮北市">
+            <district name="杜集区" zipcode="235000" />
+            <district name="烈山区" zipcode="235000" />
+            <district name="濉溪县" zipcode="235000" />
+            <district name="相山区" zipcode="235000" />
+            <district name="其他" zipcode="235000" />
+        </city>
+        <city name="淮南市">
+            <district name="八公山区" zipcode="232000" />
+            <district name="大通区" zipcode="232000" />
+            <district name="凤台县" zipcode="232000" />
+            <district name="潘集区" zipcode="232000" />
+            <district name="田家庵区" zipcode="232000" />
+            <district name="谢家集区" zipcode="232000" />
+            <district name="寿县" zipcode="232000" />
+            <district name="其他" zipcode="232000" />
+        </city>
+        <city name="黄山市">
+            <district name="黄山区" zipcode="242700" />
+            <district name="徽州区" zipcode="242700" />
+            <district name="祁门县" zipcode="242700" />
+            <district name="屯溪区" zipcode="242700" />
+            <district name="歙县" zipcode="242700" />
+            <district name="休宁县" zipcode="242700" />
+            <district name="黟县" zipcode="242700" />
+            <district name="其他" zipcode="242700" />
+        </city>
+        <city name="六安市">
+            <district name="霍邱县" zipcode="237000" />
+            <district name="霍山县" zipcode="237000" />
+            <district name="金安区" zipcode="237000" />
+            <district name="金寨县" zipcode="237000" />
+            <district name="寿县" zipcode="237000" />
+            <district name="舒城县" zipcode="237000" />
+            <district name="裕安区" zipcode="237000" />
+            <district name="其他" zipcode="237000" />
+        </city>
+        <city name="马鞍山市">
+            <district name="当涂县" zipcode="243000" />
+            <district name="花山区" zipcode="243000" />
+            <district name="含山县" zipcode="243000" />
+            <district name="雨山区" zipcode="243000" />
+            <district name="博望区" zipcode="243000" />
+            <district name="和县" zipcode="243000" />
+            <district name="其他" zipcode="243000" />
+        </city>
+        <city name="宿州市">
+            <district name="砀山县" zipcode="234100" />
+            <district name="灵璧县" zipcode="234100" />
+            <district name="泗县" zipcode="234100" />
+            <district name="萧县" zipcode="234100" />
+            <district name="墉桥区" zipcode="234100" />
+            <district name="其他" zipcode="234100" />
+        </city>
+        <city name="铜陵市">
+            <district name="郊区" zipcode="244000" />
+            <district name="铜官山区" zipcode="244000" />
+            <district name="义安区" zipcode="244000" />
+            <district name="枞阳县" zipcode="244000" />
+            <district name="其他" zipcode="244000" />
+        </city>
+        <city name="芜湖市">
+            <district name="繁昌县" zipcode="241000" />
+            <district name="镜湖区" zipcode="241000" />
+            <district name="鸠江区" zipcode="241000" />
+            <district name="南陵县" zipcode="241000" />
+            <district name="三山区" zipcode="241000" />
+            <district name="芜湖县" zipcode="241000" />
+            <district name="弋江区" zipcode="241000" />
+            <district name="无为县" zipcode="241000" />
+            <district name="其他" zipcode="241000" />
+        </city>
+        <city name="宣城市">
+            <district name="广德县" zipcode="366000" />
+            <district name="绩溪县" zipcode="366000" />
+            <district name="泾县" zipcode="366000" />
+            <district name="旌德县" zipcode="366000" />
+            <district name="郎溪县" zipcode="366000" />
+            <district name="宁国市" zipcode="366000" />
+            <district name="宣州区" zipcode="366000" />
+            <district name="其他" zipcode="366000" />
+        </city>
+    </province>
+    <province name="北京市">
+        <city name="北京市">
+            <district name="昌平区" zipcode="100000" />
+            <district name="朝阳区" zipcode="100000" />
+            <district name="崇文区" zipcode="100000" />
+            <district name="大兴区" zipcode="100000" />
+            <district name="东城区" zipcode="100000" />
+            <district name="房山区" zipcode="100000" />
+            <district name="丰台区" zipcode="100000" />
+            <district name="海淀区" zipcode="100000" />
+            <district name="怀柔区" zipcode="100000" />
+            <district name="门头沟区" zipcode="100000" />
+            <district name="密云县" zipcode="100000" />
+            <district name="平谷区" zipcode="100000" />
+            <district name="石景山区" zipcode="100000" />
+            <district name="顺义区" zipcode="100000" />
+            <district name="通州区" zipcode="100000" />
+            <district name="西城区" zipcode="100000" />
+            <district name="宣武区" zipcode="100000" />
+            <district name="延庆县" zipcode="100000" />
+        </city>
+    </province>
+    <province name="福建省">
+        <city name="福州市">
+            <district name="仓山区" zipcode="350000" />
+            <district name="长乐市" zipcode="350000" />
+            <district name="福清市" zipcode="350000" />
+            <district name="鼓楼区" zipcode="350000" />
+            <district name="晋安区" zipcode="350000" />
+            <district name="连江县" zipcode="350000" />
+            <district name="罗源县" zipcode="350000" />
+            <district name="马尾区" zipcode="350000" />
+            <district name="闽侯县" zipcode="350000" />
+            <district name="闽清县" zipcode="350000" />
+            <district name="平潭县" zipcode="350000" />
+            <district name="台江区" zipcode="350000" />
+            <district name="永泰县" zipcode="350000" />
+            <district name="其他" zipcode="350000" />
+        </city>
+        <city name="龙岩市">
+            <district name="长汀县" zipcode="364000" />
+            <district name="连城县" zipcode="364000" />
+            <district name="上杭县" zipcode="364000" />
+            <district name="武平县" zipcode="364000" />
+            <district name="新罗区" zipcode="364000" />
+            <district name="永定县" zipcode="364000" />
+            <district name="漳平市" zipcode="364000" />
+            <district name="其他" zipcode="364000" />
+        </city>
+        <city name="南平市">
+            <district name="光泽县" zipcode="353000" />
+            <district name="建瓯市" zipcode="353000" />
+            <district name="建阳市" zipcode="353000" />
+            <district name="浦城县" zipcode="353000" />
+            <district name="邵武市" zipcode="353000" />
+            <district name="顺昌县" zipcode="353000" />
+            <district name="松溪县" zipcode="353000" />
+            <district name="武夷山市" zipcode="353000" />
+            <district name="延平区" zipcode="353000" />
+            <district name="政和县" zipcode="353000" />
+            <district name="其他" zipcode="353000" />
+        </city>
+        <city name="宁德市">
+            <district name="福安市" zipcode="352100" />
+            <district name="福鼎市" zipcode="352100" />
+            <district name="古田县" zipcode="352100" />
+            <district name="蕉城区" zipcode="352100" />
+            <district name="屏南县" zipcode="352100" />
+            <district name="寿宁县" zipcode="352100" />
+            <district name="霞浦县" zipcode="352100" />
+            <district name="柘荣县" zipcode="352100" />
+            <district name="周宁县" zipcode="352100" />
+            <district name="其他" zipcode="352100" />
+        </city>
+        <city name="莆田市">
+            <district name="城厢区" zipcode="362000" />
+            <district name="涵江区" zipcode="362000" />
+            <district name="荔城区" zipcode="362000" />
+            <district name="仙游县" zipcode="362000" />
+            <district name="秀屿区" zipcode="362000" />
+            <district name="其他" zipcode="362000" />
+        </city>
+        <city name="泉州市">
+            <district name="安溪县" zipcode="362000" />
+            <district name="德化县" zipcode="362000" />
+            <district name="丰泽区" zipcode="362000" />
+            <district name="惠安县" zipcode="362000" />
+            <district name="晋江市" zipcode="362000" />
+            <district name="鲤城区" zipcode="362000" />
+            <district name="洛江区" zipcode="362000" />
+            <district name="南安市" zipcode="362000" />
+            <district name="建宁县" zipcode="362000" />
+            <district name="泉港区" zipcode="362000" />
+            <district name="石狮市" zipcode="362000" />
+            <district name="永春县" zipcode="362000" />
+            <district name="其他" zipcode="362000" />
+        </city>
+        <city name="三明市">
+            <district name="大田县" zipcode="363000" />
+            <district name="建宁县" zipcode="363000" />
+            <district name="将乐县" zipcode="363000" />
+            <district name="梅列区" zipcode="363000" />
+            <district name="明溪县" zipcode="363000" />
+            <district name="宁化县" zipcode="363000" />
+            <district name="清流县" zipcode="743000" />
+            <district name="三元区" zipcode="743000" />
+            <district name="沙县" zipcode="743000" />
+            <district name="泰宁县" zipcode="743000" />
+            <district name="永安市" zipcode="743000" />
+            <district name="尤溪县" zipcode="743000" />
+            <district name="其他" zipcode="743000" />
+        </city>
+        <city name="厦门市">
+            <district name="海沧区" zipcode="743000" />
+            <district name="湖里区" zipcode="361000" />
+            <district name="集美区" zipcode="361000" />
+            <district name="思明区" zipcode="361000" />
+            <district name="同安区" zipcode="361000" />
+            <district name="翔安区" zipcode="361000" />
+            <district name="其他" zipcode="361000" />
+        </city>
+        <city name="漳州市">
+            <district name="长泰县" zipcode="363000" />
+            <district name="东山县" zipcode="363000" />
+            <district name="华安县" zipcode="363000" />
+            <district name="龙海市" zipcode="363000" />
+            <district name="龙文区" zipcode="363000" />
+            <district name="南靖县" zipcode="363000" />
+            <district name="平和县" zipcode="363000" />
+            <district name="芗城区" zipcode="363000" />
+            <district name="云霄县" zipcode="363000" />
+            <district name="漳浦县" zipcode="363000" />
+            <district name="诏安县" zipcode="363000" />
+            <district name="其他" zipcode="363000" />
+        </city>
+    </province>
+    <province name="甘肃省">
+        <city name="白银市">
+            <district name="白银区" zipcode="730900" />
+            <district name="会宁县" zipcode="730900" />
+            <district name="景泰县" zipcode="730900" />
+            <district name="靖远县" zipcode="730900" />
+            <district name="平川区" zipcode="730900" />
+            <district name="其他" zipcode="730900" />
+        </city>
+        <city name="定西市">
+            <district name="安定区" zipcode="743000" />
+            <district name="临洮县" zipcode="743000" />
+            <district name="陇西县" zipcode="743000" />
+            <district name="岷县" zipcode="743000" />
+            <district name="通渭县" zipcode="743000" />
+            <district name="渭源县" zipcode="743000" />
+            <district name="漳县" zipcode="743000" />
+            <district name="其他" zipcode="743000" />
+        </city>
+        <city name="甘南藏族自治州">
+            <district name="迭部县" zipcode="747000" />
+            <district name="合作市" zipcode="747000" />
+            <district name="临潭县" zipcode="747000" />
+            <district name="碌曲县" zipcode="747000" />
+            <district name="玛曲县" zipcode="747000" />
+            <district name="夏河县" zipcode="747000" />
+            <district name="舟曲县" zipcode="747000" />
+            <district name="卓尼县" zipcode="747000" />
+            <district name="其他" zipcode="747000" />
+        </city>
+        <city name="金昌市">
+            <district name="金川区" zipcode="737100" />
+            <district name="永昌县" zipcode="737100" />
+            <district name="其他" zipcode="737100" />
+        </city>
+        <city name="酒泉市">
+            <district name="阿克塞哈萨克族自治县" zipcode="735000" />
+            <district name="敦煌市" zipcode="735000" />
+            <district name="瓜州县" zipcode="735000" />
+            <district name="金塔县" zipcode="735000" />
+            <district name="肃北蒙古族自治县" zipcode="735000" />
+            <district name="肃州区" zipcode="735000" />
+            <district name="玉门市" zipcode="735000" />
+            <district name="其他" zipcode="735000" />
+        </city>
+        <city name="兰州市">
+            <district name="安宁区" zipcode="730000" />
+            <district name="城关区" zipcode="730000" />
+            <district name="皋兰县" zipcode="730000" />
+            <district name="红古区" zipcode="730000" />
+            <district name="七里河区" zipcode="730000" />
+            <district name="西固区" zipcode="730000" />
+            <district name="永登县" zipcode="730000" />
+            <district name="榆中县" zipcode="730000" />
+            <district name="其他" zipcode="730000" />
+        </city>
+        <city name="临夏回族自治州">
+            <district name="东乡族自治县" zipcode="731100" />
+            <district name="广河县" zipcode="731100" />
+            <district name="和政县" zipcode="731100" />
+            <district name="积石山保安族东乡族撒拉族自治县" zipcode="731100" />
+            <district name="康乐县" zipcode="731100" />
+            <district name="临夏市" zipcode="731100" />
+            <district name="临夏县" zipcode="731100" />
+            <district name="永靖县" zipcode="731100" />
+            <district name="其他" zipcode="731100" />
+        </city>
+        <city name="陇南市">
+            <district name="成县" zipcode="742100" />
+            <district name="宕昌县" zipcode="742100" />
+            <district name="徽县" zipcode="742100" />
+            <district name="康县" zipcode="742100" />
+            <district name="礼县" zipcode="742100" />
+            <district name="两当县" zipcode="742100" />
+            <district name="文县" zipcode="742100" />
+            <district name="武都区" zipcode="742100" />
+            <district name="西和县" zipcode="742100" />
+            <district name="其他" zipcode="742100" />
+        </city>
+        <city name="平凉市">
+            <district name="崇信县" zipcode="744000" />
+            <district name="华亭县" zipcode="744000" />
+            <district name="泾川县" zipcode="744000" />
+            <district name="静宁县" zipcode="744000" />
+            <district name="崆峒区" zipcode="744000" />
+            <district name="灵台县" zipcode="744000" />
+            <district name="庄浪县" zipcode="744000" />
+            <district name="其他" zipcode="744000" />
+        </city>
+        <city name="庆阳市">
+            <district name="合水县" zipcode="744500" />
+            <district name="华池县" zipcode="744500" />
+            <district name="环县" zipcode="744500" />
+            <district name="宁县" zipcode="744500" />
+            <district name="庆城县" zipcode="744500" />
+            <district name="西峰区" zipcode="744500" />
+            <district name="镇原县" zipcode="744500" />
+            <district name="正宁县" zipcode="744500" />
+            <district name="其他" zipcode="744500" />
+        </city>
+        <city name="天水市">
+            <district name="甘谷县" zipcode="741000" />
+            <district name="麦积区" zipcode="741000" />
+            <district name="秦安县" zipcode="741000" />
+            <district name="秦州区" zipcode="741000" />
+            <district name="清水县" zipcode="741000" />
+            <district name="武山县" zipcode="741000" />
+            <district name="张家川回族自治县" zipcode="741000" />
+            <district name="其他" zipcode="741000" />
+        </city>
+        <city name="武威市">
+            <district name="古浪县" zipcode="733000" />
+            <district name="凉州区" zipcode="733000" />
+            <district name="民勤县" zipcode="733000" />
+            <district name="天祝藏族自治县" zipcode="733000" />
+            <district name="其他" zipcode="733000" />
+        </city>
+        <city name="张掖市">
+            <district name="甘州区" zipcode="734000" />
+            <district name="高台县" zipcode="734000" />
+            <district name="临泽县" zipcode="734000" />
+            <district name="民乐县" zipcode="734000" />
+            <district name="山丹县" zipcode="734000" />
+            <district name="肃南裕固族自治县" zipcode="734000" />
+            <district name="其他" zipcode="734000" />
+        </city>
+        <city name="嘉峪关市">
+            <district name="嘉峪关市" zipcode="734000" />
+        </city>
+    </province>
+    <province name="广东省">
+        <city name="潮州市">
+            <district name="潮安县" zipcode="515600" />
+            <district name="饶平县" zipcode="515600" />
+            <district name="湘桥区" zipcode="515600" />
+            <district name="其他" zipcode="515600" />
+        </city>
+        <city name="佛山市">
+            <district name="禅城区" zipcode="517000" />
+            <district name="高明区" zipcode="517000" />
+            <district name="南海区" zipcode="517000" />
+            <district name="三水区" zipcode="517000" />
+            <district name="顺德区" zipcode="517000" />
+            <district name="其他" zipcode="517000" />
+        </city>
+        <city name="广州市">
+            <district name="白云区" zipcode="510000" />
+            <district name="从化区" zipcode="510000" />
+            <district name="番禺区" zipcode="510000" />
+            <district name="海珠区" zipcode="510000" />
+            <district name="花都区" zipcode="510000" />
+            <district name="黄埔区" zipcode="510000" />
+            <district name="荔湾区" zipcode="510000" />
+            <district name="天河区" zipcode="510000" />
+            <district name="越秀区" zipcode="510000" />
+            <district name="增城区" zipcode="510000" />
+            <district name="南沙区" zipcode="510000" />
+            <district name="其他" zipcode="510000" />
+        </city>
+        <city name="河源市">
+            <district name="东源县" zipcode="517000" />
+            <district name="和平县" zipcode="517000" />
+            <district name="连平县" zipcode="517000" />
+            <district name="龙川县" zipcode="517000" />
+            <district name="源城区" zipcode="517000" />
+            <district name="紫金县" zipcode="517000" />
+            <district name="其他" zipcode="517000" />
+        </city>
+        <city name="惠州市">
+            <district name="博罗县" zipcode="516000" />
+            <district name="惠城区" zipcode="516000" />
+            <district name="惠东县" zipcode="516000" />
+            <district name="惠阳区" zipcode="516000" />
+            <district name="龙门县" zipcode="516000" />
+            <district name="其他" zipcode="516000" />
+        </city>
+        <city name="江门市">
+            <district name="恩平市" zipcode="529000" />
+            <district name="鹤山市" zipcode="529000" />
+            <district name="江海区" zipcode="529000" />
+            <district name="开平市" zipcode="529000" />
+            <district name="蓬江区" zipcode="529000" />
+            <district name="台山市" zipcode="529000" />
+            <district name="新会区" zipcode="529000" />
+            <district name="其他" zipcode="529000" />
+        </city>
+        <city name="揭阳市">
+            <district name="东山区" zipcode="522000" />
+            <district name="惠来县" zipcode="522000" />
+            <district name="揭东县" zipcode="522000" />
+            <district name="揭西县" zipcode="522000" />
+            <district name="经济开发试验区" zipcode="522000" />
+            <district name="普宁市" zipcode="522000" />
+            <district name="榕城区" zipcode="522000" />
+            <district name="其他" zipcode="522000" />
+        </city>
+        <city name="茂名市">
+            <district name="电白县" zipcode="525000" />
+            <district name="高州市" zipcode="525000" />
+            <district name="化州市" zipcode="525000" />
+            <district name="茂港区" zipcode="525000" />
+            <district name="茂南区" zipcode="525000" />
+            <district name="信宜市" zipcode="525000" />
+            <district name="其他" zipcode="525000" />
+        </city>
+        <city name="梅州市">
+            <district name="大埔县" zipcode="514000" />
+            <district name="丰顺县" zipcode="514000" />
+            <district name="蕉岭县" zipcode="514000" />
+            <district name="梅江区" zipcode="514000" />
+            <district name="梅县" zipcode="514000" />
+            <district name="平远县" zipcode="514000" />
+            <district name="五华县" zipcode="514000" />
+            <district name="兴宁市" zipcode="514000" />
+            <district name="其他" zipcode="514000" />
+        </city>
+        <city name="清远市">
+            <district name="佛冈县" zipcode="511500" />
+            <district name="连南瑶族自治县" zipcode="511500" />
+            <district name="连山壮族瑶族自治县" zipcode="511500" />
+            <district name="连州市" zipcode="511500" />
+            <district name="清城区" zipcode="511500" />
+            <district name="清新县" zipcode="511500" />
+            <district name="阳山县" zipcode="511500" />
+            <district name="英德市" zipcode="511500" />
+            <district name="其他" zipcode="511500" />
+        </city>
+        <city name="汕头市">
+            <district name="潮南区" zipcode="515000" />
+            <district name="潮阳区" zipcode="515000" />
+            <district name="澄海区" zipcode="515000" />
+            <district name="濠江区" zipcode="515000" />
+            <district name="金平区" zipcode="515000" />
+            <district name="龙湖区" zipcode="515000" />
+            <district name="南澳县" zipcode="515000" />
+            <district name="其他" zipcode="515000" />
+        </city>
+        <city name="汕尾市">
+            <district name="城区" zipcode="516600" />
+            <district name="海丰县" zipcode="516600" />
+            <district name="红海湾经济开发试验区" zipcode="516600" />
+            <district name="陆丰市" zipcode="516600" />
+            <district name="陆河县" zipcode="516600" />
+            <district name="其他" zipcode="516600" />
+        </city>
+        <city name="韶关市">
+            <district name="乐昌市" zipcode="512000" />
+            <district name="南雄市" zipcode="512000" />
+            <district name="曲江区" zipcode="512000" />
+            <district name="仁化县" zipcode="512000" />
+            <district name="乳源瑶族自治县" zipcode="512000" />
+            <district name="始兴县" zipcode="512000" />
+            <district name="翁源县" zipcode="512000" />
+            <district name="武江区" zipcode="512000" />
+            <district name="新丰县" zipcode="512000" />
+            <district name="浈江区" zipcode="512000" />
+            <district name="其他" zipcode="512000" />
+        </city>
+        <city name="深圳市">
+            <district name="宝安区" zipcode="518000" />
+            <district name="福田区" zipcode="518000" />
+            <district name="龙岗区" zipcode="518000" />
+            <district name="罗湖区" zipcode="518000" />
+            <district name="南山区" zipcode="518000" />
+            <district name="盐田区" zipcode="518000" />
+            <district name="其他" zipcode="518000" />
+        </city>
+        <city name="阳江市">
+            <district name="江城区" zipcode="529500" />
+            <district name="阳春市" zipcode="529500" />
+            <district name="阳东县" zipcode="529500" />
+            <district name="阳西县" zipcode="529500" />
+            <district name="其他" zipcode="529500" />
+        </city>
+        <city name="云浮市">
+            <district name="罗定市" zipcode="527300" />
+            <district name="新兴县" zipcode="527300" />
+            <district name="郁南县" zipcode="527300" />
+            <district name="云安县" zipcode="527300" />
+            <district name="云城区" zipcode="527300" />
+            <district name="其他" zipcode="527300" />
+        </city>
+        <city name="湛江市">
+            <district name="赤坎区" zipcode="528400" />
+            <district name="东海岛" zipcode="519000" />
+            <district name="开发区" zipcode="519000" />
+            <district name="雷州市" zipcode="519000" />
+            <district name="廉江市" zipcode="533000" />
+            <district name="麻章区" zipcode="533000" />
+            <district name="坡头区" zipcode="533000" />
+            <district name="遂溪县" zipcode="533000" />
+            <district name="吴川市" zipcode="533000" />
+            <district name="霞山区" zipcode="533000" />
+            <district name="徐闻县" zipcode="533000" />
+            <district name="其他" zipcode="533000" />
+        </city>
+        <city name="肇庆市">
+            <district name="德庆县" zipcode="533000" />
+            <district name="鼎湖区" zipcode="533000" />
+            <district name="端州区" zipcode="533000" />
+            <district name="封开县" zipcode="533000" />
+            <district name="高要市" zipcode="533000" />
+            <district name="广宁县" zipcode="536000" />
+            <district name="怀集县" zipcode="536000" />
+            <district name="四会市" zipcode="536000" />
+            <district name="其他" zipcode="536000" />
+        </city>
+        <city name="珠海市">
+            <district name="斗门区" zipcode="519000" />
+            <district name="金湾区" zipcode="519000" />
+            <district name="香洲区" zipcode="519000" />
+            <district name="其他" zipcode="519000" />
+        </city>
+        <city name="东莞市">
+            <district name="东莞市" zipcode="536000" />
+        </city>
+        <city name="中山市">
+            <district name="中山市" zipcode="519000" />
+        </city>
+    </province>
+    <province name="广西壮族自治区">
+        <city name="百色市">
+            <district name="德保县" zipcode="533000" />
+            <district name="靖西县" zipcode="533000" />
+            <district name="乐业县" zipcode="533000" />
+            <district name="凌云县" zipcode="533000" />
+            <district name="隆林各族自治县" zipcode="533000" />
+            <district name="那坡县" zipcode="533000" />
+            <district name="平果县" zipcode="533000" />
+            <district name="田东县" zipcode="533000" />
+            <district name="田林县" zipcode="533000" />
+            <district name="田阳县" zipcode="533000" />
+            <district name="西林县" zipcode="533000" />
+            <district name="右江区" zipcode="533000" />
+            <district name="其他" zipcode="533000" />
+        </city>
+        <city name="北海市">
+            <district name="海城区" zipcode="536000" />
+            <district name="合浦县" zipcode="536000" />
+            <district name="铁山港区" zipcode="536000" />
+            <district name="银海区" zipcode="536000" />
+            <district name="其他" zipcode="536000" />
+        </city>
+        <city name="崇左市">
+            <district name="大新县" zipcode="532200" />
+            <district name="扶绥县" zipcode="532200" />
+            <district name="江洲区" zipcode="532200" />
+            <district name="龙州县" zipcode="532200" />
+            <district name="宁明县" zipcode="532200" />
+            <district name="凭祥市" zipcode="532200" />
+            <district name="天等县" zipcode="532200" />
+            <district name="其他" zipcode="532200" />
+        </city>
+        <city name="防城港市">
+            <district name="东兴市" zipcode="538000" />
+            <district name="防城区" zipcode="538000" />
+            <district name="港口区" zipcode="538000" />
+            <district name="上思县" zipcode="538000" />
+            <district name="其他" zipcode="538000" />
+        </city>
+        <city name="贵港市">
+            <district name="港北区" zipcode="537100" />
+            <district name="港南区" zipcode="537100" />
+            <district name="桂平市" zipcode="537100" />
+            <district name="平南县" zipcode="537100" />
+            <district name="覃塘区" zipcode="537100" />
+            <district name="其他" zipcode="537100" />
+        </city>
+        <city name="桂林市">
+            <district name="叠彩区" zipcode="541000" />
+            <district name="恭城瑶族自治县" zipcode="541000" />
+            <district name="灌阳县" zipcode="541000" />
+            <district name="荔蒲县" zipcode="541000" />
+            <district name="临桂县" zipcode="541000" />
+            <district name="灵川县" zipcode="541000" />
+            <district name="龙胜各族自治县" zipcode="541000" />
+            <district name="平乐县" zipcode="541000" />
+            <district name="七星区" zipcode="541000" />
+            <district name="全州县" zipcode="541000" />
+            <district name="象山区" zipcode="541000" />
+            <district name="兴安县" zipcode="541000" />
+            <district name="秀峰区" zipcode="541000" />
+            <district name="雁山区" zipcode="541000" />
+            <district name="阳朔县" zipcode="541000" />
+            <district name="永福县" zipcode="541000" />
+            <district name="资源县" zipcode="541000" />
+            <district name="其他" zipcode="541000" />
+        </city>
+        <city name="河池市">
+            <district name="巴马瑶族自治县" zipcode="547000" />
+            <district name="大化瑶族自治县" zipcode="547000" />
+            <district name="东兰县" zipcode="547000" />
+            <district name="都安瑶族自治县" zipcode="547000" />
+            <district name="凤山县" zipcode="547000" />
+            <district name="环江毛南族自治县" zipcode="547000" />
+            <district name="金城江区" zipcode="547000" />
+            <district name="罗城仫佬族自治县" zipcode="547000" />
+            <district name="南丹县" zipcode="547000" />
+            <district name="天峨县" zipcode="547000" />
+            <district name="宜州市" zipcode="547000" />
+            <district name="其他" zipcode="547000" />
+        </city>
+        <city name="贺州市">
+            <district name="八步区" zipcode="542800" />
+            <district name="富川瑶族自治县" zipcode="542800" />
+            <district name="昭平县" zipcode="542800" />
+            <district name="钟山县" zipcode="542800" />
+            <district name="其他" zipcode="542800" />
+        </city>
+        <city name="来宾市">
+            <district name="合山市" zipcode="546100" />
+            <district name="金秀瑶族自治县" zipcode="546100" />
+            <district name="武宣县" zipcode="546100" />
+            <district name="象州县" zipcode="546100" />
+            <district name="忻城县" zipcode="546100" />
+            <district name="兴宾区" zipcode="546100" />
+            <district name="其他" zipcode="546100" />
+        </city>
+        <city name="柳州市">
+            <district name="城中区" zipcode="545000" />
+            <district name="柳北区" zipcode="545000" />
+            <district name="柳城县" zipcode="545000" />
+            <district name="柳江县" zipcode="545000" />
+            <district name="柳南区" zipcode="545000" />
+            <district name="鹿寨县" zipcode="545000" />
+            <district name="融安县" zipcode="545000" />
+            <district name="融水苗族自治县" zipcode="545000" />
+            <district name="三江侗族自治县" zipcode="545000" />
+            <district name="鱼峰区" zipcode="545000" />
+            <district name="其他" zipcode="545000" />
+        </city>
+        <city name="南宁市">
+            <district name="宾阳县" zipcode="530000" />
+            <district name="横县" zipcode="530000" />
+            <district name="江南区" zipcode="530000" />
+            <district name="良庆区" zipcode="530000" />
+            <district name="隆安县" zipcode="530000" />
+            <district name="马山县" zipcode="530000" />
+            <district name="青秀区" zipcode="530000" />
+            <district name="上林县" zipcode="530000" />
+            <district name="武鸣县" zipcode="530000" />
+            <district name="西乡塘区" zipcode="530000" />
+            <district name="兴宁区" zipcode="530000" />
+            <district name="邕宁区" zipcode="530000" />
+            <district name="其他" zipcode="530000" />
+        </city>
+        <city name="钦州市">
+            <district name="灵山县" zipcode="535000" />
+            <district name="浦北县" zipcode="535000" />
+            <district name="钦北区" zipcode="535000" />
+            <district name="钦南区" zipcode="535000" />
+            <district name="其他" zipcode="535000" />
+        </city>
+        <city name="梧州市">
+            <district name="苍梧县" zipcode="543000" />
+            <district name="岑溪市" zipcode="543000" />
+            <district name="长洲区" zipcode="543000" />
+            <district name="蝶山区" zipcode="543000" />
+            <district name="蒙山县" zipcode="543000" />
+            <district name="藤县" zipcode="543000" />
+            <district name="万秀区" zipcode="543000" />
+            <district name="其他" zipcode="543000" />
+        </city>
+        <city name="玉林市">
+            <district name="北流市" zipcode="537000" />
+            <district name="博白县" zipcode="537000" />
+            <district name="陆川县" zipcode="537000" />
+            <district name="容县" zipcode="537000" />
+            <district name="兴业县" zipcode="537000" />
+            <district name="玉州区" zipcode="537000" />
+            <district name="其他" zipcode="537000" />
+        </city>
+    </province>
+    <province name="贵州省">
+        <city name="安顺市">
+            <district name="关岭布依族苗族自治县" zipcode="561000" />
+            <district name="平坝县" zipcode="561000" />
+            <district name="普定县" zipcode="561000" />
+            <district name="西秀区" zipcode="561000" />
+            <district name="镇宁布依族苗族自治县" zipcode="561000" />
+            <district name="紫云苗族布依族自治县" zipcode="561000" />
+            <district name="其他" zipcode="561000" />
+        </city>
+        <city name="毕节地区">
+            <district name="毕节市" zipcode="551700" />
+            <district name="大方县" zipcode="551700" />
+            <district name="赫章县" zipcode="551700" />
+            <district name="金沙县" zipcode="551700" />
+            <district name="纳雍县" zipcode="551700" />
+            <district name="黔西县" zipcode="551700" />
+            <district name="威宁彝族回族苗族自治县" zipcode="551700" />
+            <district name="织金县" zipcode="551700" />
+            <district name="其他" zipcode="551700" />
+        </city>
+        <city name="贵阳市">
+            <district name="白云区" zipcode="550000" />
+            <district name="花溪区" zipcode="550000" />
+            <district name="开阳县" zipcode="550000" />
+            <district name="南明区" zipcode="550000" />
+            <district name="清镇市" zipcode="550000" />
+            <district name="乌当区" zipcode="550000" />
+            <district name="息烽县" zipcode="550000" />
+            <district name="小河区" zipcode="550000" />
+            <district name="修文县" zipcode="550000" />
+            <district name="云岩区" zipcode="550000" />
+            <district name="其他" zipcode="550000" />
+        </city>
+        <city name="六盘水市">
+            <district name="六枝特区" zipcode="553000" />
+            <district name="盘县" zipcode="553000" />
+            <district name="水城县" zipcode="553000" />
+            <district name="钟山区" zipcode="553000" />
+            <district name="其他" zipcode="553000" />
+        </city>
+        <city name="黔东南苗族侗族自治州">
+            <district name="岑巩县" zipcode="556000" />
+            <district name="从江县" zipcode="556000" />
+            <district name="丹寨县" zipcode="556000" />
+            <district name="黄平县" zipcode="556000" />
+            <district name="剑河县" zipcode="556000" />
+            <district name="锦屏县" zipcode="556000" />
+            <district name="凯里市" zipcode="556000" />
+            <district name="雷山县" zipcode="556000" />
+            <district name="黎平县" zipcode="556000" />
+            <district name="麻江县" zipcode="556000" />
+            <district name="榕江县" zipcode="556000" />
+            <district name="三穗县" zipcode="556000" />
+            <district name="施秉县" zipcode="556000" />
+            <district name="台江县" zipcode="556000" />
+            <district name="天柱县" zipcode="556000" />
+            <district name="镇远县" zipcode="556000" />
+            <district name="其他" zipcode="556000" />
+        </city>
+        <city name="黔南布依族苗族自治州">
+            <district name="长顺县" zipcode="558000" />
+            <district name="都匀市" zipcode="558000" />
+            <district name="独山县" zipcode="558000" />
+            <district name="福泉市" zipcode="558000" />
+            <district name="贵定县" zipcode="558000" />
+            <district name="惠水县" zipcode="558000" />
+            <district name="荔波县" zipcode="558000" />
+            <district name="龙里县" zipcode="558000" />
+            <district name="罗甸县" zipcode="558000" />
+            <district name="平塘县" zipcode="558000" />
+            <district name="三都水族自治县" zipcode="558000" />
+            <district name="瓮安县" zipcode="558000" />
+            <district name="其他" zipcode="558000" />
+        </city>
+        <city name="黔西南布依族苗族自治州">
+            <district name="安龙县" zipcode="562400" />
+            <district name="册亨县" zipcode="562400" />
+            <district name="普安县" zipcode="562400" />
+            <district name="晴隆县" zipcode="562400" />
+            <district name="望谟县" zipcode="562400" />
+            <district name="兴仁县" zipcode="562400" />
+            <district name="兴义市" zipcode="562400" />
+            <district name="贞丰县" zipcode="562400" />
+            <district name="其他" zipcode="562400" />
+        </city>
+        <city name="铜仁地区">
+            <district name="德江县" zipcode="554300" />
+            <district name="江口县" zipcode="554300" />
+            <district name="石阡县" zipcode="554300" />
+            <district name="思南县" zipcode="554300" />
+            <district name="松桃苗族自治县" zipcode="554300" />
+            <district name="铜仁市" zipcode="554300" />
+            <district name="万山特区" zipcode="554300" />
+            <district name="沿河土家族自治县" zipcode="554300" />
+            <district name="印江土家族苗族自治县" zipcode="554300" />
+            <district name="玉屏侗族自治县" zipcode="554300" />
+            <district name="其他" zipcode="554300" />
+        </city>
+        <city name="遵义市">
+            <district name="赤水市" zipcode="563000" />
+            <district name="道真仡佬族苗族自治县" zipcode="563000" />
+            <district name="凤冈县" zipcode="563000" />
+            <district name="红花岗区" zipcode="563000" />
+            <district name="汇川区" zipcode="563000" />
+            <district name="湄潭县" zipcode="563000" />
+            <district name="仁怀市" zipcode="563000" />
+            <district name="绥阳县" zipcode="563000" />
+            <district name="桐梓县" zipcode="563000" />
+            <district name="务川仡佬族苗族自治县" zipcode="563000" />
+            <district name="习水县" zipcode="563000" />
+            <district name="余庆县" zipcode="563000" />
+            <district name="正安县" zipcode="563000" />
+            <district name="遵义县" zipcode="563000" />
+            <district name="其他" zipcode="563000" />
+        </city>
+    </province>
+    <province name="海南省">
+        <city name="三亚市">
+            <district name="海棠区" zipcode="572000" />
+            <district name="吉阳区" zipcode="572000" />
+            <district name="天涯区" zipcode="572000" />
+            <district name="崖州区" zipcode="572000" />
+            <district name="其他" zipcode="572000" />
+        </city>
+        <city name="海口市">
+            <district name="龙华区" zipcode="570000" />
+            <district name="美兰区" zipcode="570000" />
+            <district name="琼山区" zipcode="570000" />
+            <district name="秀英区" zipcode="570000" />
+            <district name="其他" zipcode="570000" />
+        </city>
+        <city name="三沙市">
+            <district name="三沙市" zipcode="572000" />
+        </city>
+        <city name="儋州市">
+            <district name="儋州市" zipcode="570000" />
+        </city>
+        <city name="省直辖行政单位">
+            <district name="白沙黎族自治县" zipcode="572000" />
+            <district name="保亭黎族苗族自治县" zipcode="572000" />
+            <district name="昌江黎族自治县" zipcode="572000" />
+            <district name="澄迈县" zipcode="572000" />
+            <district name="儋州市" zipcode="572000" />
+            <district name="定安县" zipcode="572000" />
+            <district name="东方市" zipcode="572000" />
+            <district name="乐东黎族自治县" zipcode="572000" />
+            <district name="临高县" zipcode="572000" />
+            <district name="陵水黎族自治县" zipcode="572000" />
+            <district name="琼海市" zipcode="572000" />
+            <district name="琼中黎族苗族自治县" zipcode="572000" />
+            <district name="屯昌县" zipcode="572000" />
+            <district name="万宁市" zipcode="572000" />
+            <district name="文昌市" zipcode="572000" />
+            <district name="五指山市" zipcode="572000" />
+            <district name="西沙群岛" zipcode="572000" />
+            <district name="其他" zipcode="572000" />
+        </city>
+
+    </province>
+    <province name="河北省">
+        <city name="保定市">
+            <district name="安国市" zipcode="071000" />
+            <district name="安新县" zipcode="071000" />
+            <district name="北市区" zipcode="071000" />
+            <district name="博野县" zipcode="071000" />
+            <district name="定兴县" zipcode="071000" />
+            <district name="定州市" zipcode="071000" />
+            <district name="阜平县" zipcode="071000" />
+            <district name="高碑店市" zipcode="071000" />
+            <district name="高阳县" zipcode="071000" />
+            <district name="涞水县" zipcode="071000" />
+            <district name="涞源县" zipcode="071000" />
+            <district name="蠡县" zipcode="071000" />
+            <district name="满城县" zipcode="071000" />
+            <district name="南市区" zipcode="071000" />
+            <district name="清苑县" zipcode="071000" />
+            <district name="曲阳县" zipcode="071000" />
+            <district name="容城县" zipcode="071000" />
+            <district name="顺平县" zipcode="071000" />
+            <district name="唐县" zipcode="071000" />
+            <district name="望都县" zipcode="071000" />
+            <district name="新市区" zipcode="071000" />
+            <district name="雄县" zipcode="071000" />
+            <district name="徐水县" zipcode="071000" />
+            <district name="易县" zipcode="071000" />
+            <district name="涿州市" zipcode="071000" />
+            <district name="其他" zipcode="71000" />
+        </city>
+        <city name="沧州市">
+            <district name="泊头市" zipcode="061000" />
+            <district name="沧县" zipcode="061000" />
+            <district name="东光县" zipcode="061000" />
+            <district name="海兴县" zipcode="061000" />
+            <district name="河间市" zipcode="061000" />
+            <district name="黄骅市" zipcode="061000" />
+            <district name="孟村回族自治县" zipcode="061000" />
+            <district name="南皮县" zipcode="061000" />
+            <district name="青县" zipcode="061000" />
+            <district name="任丘市" zipcode="061000" />
+            <district name="肃宁县" zipcode="061000" />
+            <district name="吴桥县" zipcode="061000" />
+            <district name="献县" zipcode="061000" />
+            <district name="新华区" zipcode="061000" />
+            <district name="盐山县" zipcode="061000" />
+            <district name="运河区" zipcode="061000" />
+            <district name="其他" zipcode="61000" />
+        </city>
+        <city name="承德市">
+            <district name="承德县" zipcode="067000" />
+            <district name="丰宁满族自治县" zipcode="067000" />
+            <district name="宽城满族自治县" zipcode="067000" />
+            <district name="隆化县" zipcode="067000" />
+            <district name="滦平县" zipcode="067000" />
+            <district name="平泉县" zipcode="067000" />
+            <district name="双滦区" zipcode="067000" />
+            <district name="双桥区" zipcode="067000" />
+            <district name="围场满族蒙古族自治县" zipcode="067000" />
+            <district name="兴隆县" zipcode="067000" />
+            <district name="鹰手营子矿区" zipcode="067000" />
+            <district name="其他" zipcode="67000" />
+        </city>
+        <city name="邯郸市">
+            <district name="成安县" zipcode="056000" />
+            <district name="磁县" zipcode="056000" />
+            <district name="丛台区" zipcode="056000" />
+            <district name="大名县" zipcode="056000" />
+            <district name="肥乡县" zipcode="056000" />
+            <district name="峰峰矿区" zipcode="056000" />
+            <district name="复兴区" zipcode="056000" />
+            <district name="馆陶县" zipcode="056000" />
+            <district name="广平县" zipcode="056000" />
+            <district name="邯郸县" zipcode="056000" />
+            <district name="邯山区" zipcode="056000" />
+            <district name="鸡泽县" zipcode="056000" />
+            <district name="临漳县" zipcode="056000" />
+            <district name="邱县" zipcode="056000" />
+            <district name="曲周县" zipcode="056000" />
+            <district name="涉县" zipcode="056000" />
+            <district name="魏县" zipcode="056000" />
+            <district name="武安市" zipcode="056000" />
+            <district name="永年县" zipcode="056000" />
+            <district name="其他" zipcode="56000" />
+        </city>
+        <city name="衡水市">
+            <district name="安平县" zipcode="053000" />
+            <district name="阜城县" zipcode="053000" />
+            <district name="故城县" zipcode="053000" />
+            <district name="冀州市" zipcode="053000" />
+            <district name="景县" zipcode="053000" />
+            <district name="饶阳县" zipcode="053000" />
+            <district name="深州市" zipcode="053000" />
+            <district name="桃城区" zipcode="053000" />
+            <district name="武强县" zipcode="053000" />
+            <district name="武邑县" zipcode="053000" />
+            <district name="枣强县" zipcode="053000" />
+            <district name="其他" zipcode="53000" />
+        </city>
+        <city name="廊坊市">
+            <district name="安次区" zipcode="065000" />
+            <district name="霸州市" zipcode="065000" />
+            <district name="大厂回族自治县" zipcode="065000" />
+            <district name="大城县" zipcode="065000" />
+            <district name="固安县" zipcode="065000" />
+            <district name="广阳区" zipcode="065000" />
+            <district name="三河市" zipcode="065000" />
+            <district name="文安县" zipcode="065000" />
+            <district name="香河县" zipcode="065000" />
+            <district name="永清县" zipcode="065000" />
+            <district name="其他" zipcode="65000" />
+        </city>
+        <city name="秦皇岛市">
+            <district name="北戴河区" zipcode="066000" />
+            <district name="昌黎县" zipcode="066000" />
+            <district name="抚宁县" zipcode="066000" />
+            <district name="海港区" zipcode="066000" />
+            <district name="卢龙县" zipcode="066000" />
+            <district name="青龙满族自治县" zipcode="066000" />
+            <district name="山海关区" zipcode="066000" />
+            <district name="其他" zipcode="66000" />
+        </city>
+        <city name="石家庄市">
+            <district name="长安区" zipcode="050000" />
+            <district name="高邑县" zipcode="050000" />
+            <district name="藁城市" zipcode="050000" />
+            <district name="晋州市" zipcode="050000" />
+            <district name="井陉矿区" zipcode="050000" />
+            <district name="井陉县" zipcode="050000" />
+            <district name="灵寿县" zipcode="050000" />
+            <district name="鹿泉市" zipcode="050000" />
+            <district name="栾城县" zipcode="050000" />
+            <district name="平山县" zipcode="050000" />
+            <district name="桥东区" zipcode="050000" />
+            <district name="桥西区" zipcode="050000" />
+            <district name="深泽县" zipcode="050000" />
+            <district name="无极县" zipcode="050000" />
+            <district name="辛集市" zipcode="050000" />
+            <district name="新华区" zipcode="050000" />
+            <district name="新乐市" zipcode="050000" />
+            <district name="行唐县" zipcode="050000" />
+            <district name="裕华区" zipcode="050000" />
+            <district name="元氏县" zipcode="050000" />
+            <district name="赞皇县" zipcode="050000" />
+            <district name="赵县" zipcode="050000" />
+            <district name="正定县" zipcode="050000" />
+            <district name="其他" zipcode="50000" />
+        </city>
+        <city name="唐山市">
+            <district name="丰南区" zipcode="063000" />
+            <district name="丰润区" zipcode="063000" />
+            <district name="古冶区" zipcode="063000" />
+            <district name="开平区" zipcode="063000" />
+            <district name="乐亭县" zipcode="063000" />
+            <district name="路北区" zipcode="063000" />
+            <district name="路南区" zipcode="063000" />
+            <district name="滦南县" zipcode="063000" />
+            <district name="滦县" zipcode="063000" />
+            <district name="迁安市" zipcode="063000" />
+            <district name="迁西县" zipcode="063000" />
+            <district name="唐海县" zipcode="063000" />
+            <district name="玉田县" zipcode="063000" />
+            <district name="遵化市" zipcode="063000" />
+            <district name="其他" zipcode="63000" />
+        </city>
+        <city name="邢台市">
+            <district name="柏乡县" zipcode="054000" />
+            <district name="广宗县" zipcode="054000" />
+            <district name="巨鹿县" zipcode="054000" />
+            <district name="临城县" zipcode="054000" />
+            <district name="临西县" zipcode="054000" />
+            <district name="隆尧县" zipcode="054000" />
+            <district name="内丘县" zipcode="054000" />
+            <district name="南宫市" zipcode="054000" />
+            <district name="南和县" zipcode="054000" />
+            <district name="宁晋县" zipcode="054000" />
+            <district name="平乡县" zipcode="054000" />
+            <district name="桥东区" zipcode="054000" />
+            <district name="桥西区" zipcode="054000" />
+            <district name="清河县" zipcode="054000" />
+            <district name="任县" zipcode="054000" />
+            <district name="沙河市" zipcode="054000" />
+            <district name="威县" zipcode="054000" />
+            <district name="新河县" zipcode="054000" />
+            <district name="邢台县" zipcode="054000" />
+            <district name="其他" zipcode="54000" />
+        </city>
+        <city name="张家口市">
+            <district name="赤城县" zipcode="075000" />
+            <district name="崇礼县" zipcode="075000" />
+            <district name="沽源县" zipcode="075000" />
+            <district name="怀安县" zipcode="075000" />
+            <district name="怀来县" zipcode="075000" />
+            <district name="康保县" zipcode="075000" />
+            <district name="桥东区" zipcode="075000" />
+            <district name="桥西区" zipcode="075000" />
+            <district name="尚义县" zipcode="075000" />
+            <district name="万全县" zipcode="075000" />
+            <district name="蔚县" zipcode="075000" />
+            <district name="下花园区" zipcode="075000" />
+            <district name="宣化区" zipcode="075000" />
+            <district name="宣化县" zipcode="075000" />
+            <district name="阳原县" zipcode="075000" />
+            <district name="张北县" zipcode="075000" />
+            <district name="涿鹿县" zipcode="075000" />
+            <district name="其他" zipcode="75000" />
+        </city>
+    </province>
+    <province name="河南省">
+        <city name="安阳市">
+            <district name="安阳县" zipcode="454900" />
+            <district name="北关区" zipcode="454900" />
+            <district name="滑县" zipcode="454900" />
+            <district name="林州市" zipcode="454900" />
+            <district name="龙安区" zipcode="454900" />
+            <district name="内黄县" zipcode="454900" />
+            <district name="汤阴县" zipcode="454900" />
+            <district name="文峰区" zipcode="454900" />
+            <district name="殷都区" zipcode="454900" />
+            <district name="其他" zipcode="454900" />
+        </city>
+        <city name="鹤壁市">
+            <district name="鹤山区" zipcode="458000" />
+            <district name="浚县" zipcode="458000" />
+            <district name="淇滨区" zipcode="458000" />
+            <district name="淇县" zipcode="458000" />
+            <district name="山城区" zipcode="458000" />
+            <district name="其他" zipcode="458000" />
+        </city>
+        <city name="焦作市">
+            <district name="博爱县" zipcode="454100" />
+            <district name="解放区" zipcode="454100" />
+            <district name="孟州市" zipcode="454100" />
+            <district name="沁阳市" zipcode="454100" />
+            <district name="山阳区" zipcode="454100" />
+            <district name="温县" zipcode="454100" />
+            <district name="武陟县" zipcode="454100" />
+            <district name="修武县" zipcode="454100" />
+            <district name="中站区" zipcode="454100" />
+            <district name="马村区" zipcode="454100" />
+            <district name="其他" zipcode="454100" />
+        </city>
+        <city name="开封市">
+            <district name="鼓楼区" zipcode="475000" />
+            <district name="金明区" zipcode="475000" />
+            <district name="开封县" zipcode="475000" />
+            <district name="兰考县" zipcode="475000" />
+            <district name="龙亭区" zipcode="475000" />
+            <district name="杞县" zipcode="475000" />
+            <district name="顺河回族区" zipcode="475000" />
+            <district name="通许县" zipcode="475000" />
+            <district name="尉氏县" zipcode="475000" />
+            <district name="禹王台区" zipcode="475000" />
+            <district name="其他" zipcode="475000" />
+        </city>
+        <city name="洛阳市">
+            <district name="瀍河回族区" zipcode="471000" />
+            <district name="吉利区" zipcode="471000" />
+            <district name="涧西区" zipcode="471000" />
+            <district name="老城区" zipcode="471000" />
+            <district name="栾川县" zipcode="471000" />
+            <district name="洛龙区" zipcode="471000" />
+            <district name="洛宁县" zipcode="471000" />
+            <district name="孟津县" zipcode="471000" />
+            <district name="汝阳县" zipcode="471000" />
+            <district name="嵩县" zipcode="471000" />
+            <district name="西工区" zipcode="471000" />
+            <district name="新安县" zipcode="471000" />
+            <district name="偃师市" zipcode="471000" />
+            <district name="伊川县" zipcode="471000" />
+            <district name="宜阳县" zipcode="471000" />
+            <district name="其他" zipcode="471000" />
+        </city>
+        <city name="漯河市">
+            <district name="临颍县" zipcode="462000" />
+            <district name="舞阳县" zipcode="462000" />
+            <district name="郾城区" zipcode="462000" />
+            <district name="源汇区" zipcode="462000" />
+            <district name="召陵区" zipcode="462000" />
+            <district name="其他" zipcode="462000" />
+        </city>
+        <city name="南阳市">
+            <district name="邓州市" zipcode="473000" />
+            <district name="方城县" zipcode="473000" />
+            <district name="内乡县" zipcode="473000" />
+            <district name="南召县" zipcode="473000" />
+            <district name="社旗县" zipcode="473000" />
+            <district name="唐河县" zipcode="473000" />
+            <district name="桐柏县" zipcode="473000" />
+            <district name="宛城区" zipcode="473000" />
+            <district name="卧龙区" zipcode="473000" />
+            <district name="西峡县" zipcode="473000" />
+            <district name="淅川县" zipcode="473000" />
+            <district name="新野县" zipcode="473000" />
+            <district name="镇平县" zipcode="473000" />
+            <district name="其他" zipcode="473000" />
+        </city>
+        <city name="平顶山市">
+            <district name="宝丰县" zipcode="467000" />
+            <district name="郏县" zipcode="467000" />
+            <district name="鲁山县" zipcode="467000" />
+            <district name="汝州市" zipcode="467000" />
+            <district name="石龙区" zipcode="467000" />
+            <district name="卫东区" zipcode="467000" />
+            <district name="舞钢市" zipcode="467000" />
+            <district name="新华区" zipcode="467000" />
+            <district name="叶县" zipcode="467000" />
+            <district name="湛河区" zipcode="467000" />
+            <district name="其他" zipcode="467000" />
+        </city>
+        <city name="濮阳市">
+            <district name="范县" zipcode="457000" />
+            <district name="华龙区" zipcode="457000" />
+            <district name="南乐县" zipcode="457000" />
+            <district name="濮阳县" zipcode="457000" />
+            <district name="清丰县" zipcode="457000" />
+            <district name="台前县" zipcode="457000" />
+            <district name="其他" zipcode="457000" />
+        </city>
+        <city name="三门峡市">
+            <district name="湖滨区" zipcode="472000" />
+            <district name="灵宝市" zipcode="472000" />
+            <district name="卢氏县" zipcode="472000" />
+            <district name="陕县" zipcode="472000" />
+            <district name="渑池县" zipcode="472000" />
+            <district name="义马市" zipcode="472000" />
+            <district name="其他" zipcode="472000" />
+        </city>
+        <city name="商丘市">
+            <district name="梁园区" zipcode="476000" />
+            <district name="民权县" zipcode="476000" />
+            <district name="宁陵县" zipcode="476000" />
+            <district name="睢县" zipcode="476000" />
+            <district name="睢阳区" zipcode="476000" />
+            <district name="夏邑县" zipcode="476000" />
+            <district name="永城市" zipcode="476000" />
+            <district name="虞城县" zipcode="476000" />
+            <district name="柘城县" zipcode="476000" />
+            <district name="其他" zipcode="476000" />
+        </city>
+        <city name="新乡市">
+            <district name="长垣县" zipcode="453000" />
+            <district name="封丘县" zipcode="453000" />
+            <district name="凤泉区" zipcode="453000" />
+            <district name="红旗区" zipcode="453000" />
+            <district name="辉县市" zipcode="453000" />
+            <district name="获嘉县" zipcode="453000" />
+            <district name="牧野区" zipcode="453000" />
+            <district name="卫滨区" zipcode="453000" />
+            <district name="卫辉市" zipcode="453000" />
+            <district name="新乡县" zipcode="453000" />
+            <district name="延津县" zipcode="453000" />
+            <district name="原阳县" zipcode="453000" />
+            <district name="其他" zipcode="453000" />
+        </city>
+        <city name="信阳市">
+            <district name="固始县" zipcode="464000" />
+            <district name="光山县" zipcode="464000" />
+            <district name="淮滨县" zipcode="464000" />
+            <district name="潢川县" zipcode="464000" />
+            <district name="罗山县" zipcode="464000" />
+            <district name="平桥区" zipcode="464000" />
+            <district name="商城县" zipcode="464000" />
+            <district name="浉河区" zipcode="464000" />
+            <district name="息县" zipcode="464000" />
+            <district name="新县" zipcode="464000" />
+            <district name="其他" zipcode="464000" />
+        </city>
+        <city name="许昌市">
+            <district name="长葛市" zipcode="461000" />
+            <district name="魏都区" zipcode="461000" />
+            <district name="襄城县" zipcode="461000" />
+            <district name="许昌县" zipcode="461000" />
+            <district name="鄢陵县" zipcode="461000" />
+            <district name="禹州市" zipcode="461000" />
+            <district name="其他" zipcode="461000" />
+        </city>
+        <city name="郑州市">
+            <district name="登封市" zipcode="450000" />
+            <district name="二七区" zipcode="450000" />
+            <district name="巩义市" zipcode="450000" />
+            <district name="管城回族区" zipcode="450000" />
+            <district name="惠济区" zipcode="450000" />
+            <district name="金水区" zipcode="450000" />
+            <district name="上街区" zipcode="450000" />
+            <district name="新密市" zipcode="450000" />
+            <district name="新郑市" zipcode="450000" />
+            <district name="荥阳市" zipcode="450000" />
+            <district name="中牟县" zipcode="450000" />
+            <district name="中原区" zipcode="450000" />
+            <district name="其他" zipcode="450000" />
+        </city>
+        <city name="周口市">
+            <district name="川汇区" zipcode="466000" />
+            <district name="郸城县" zipcode="466000" />
+            <district name="扶沟县" zipcode="466000" />
+            <district name="淮阳县" zipcode="466000" />
+            <district name="鹿邑县" zipcode="466000" />
+            <district name="商水县" zipcode="466000" />
+            <district name="沈丘县" zipcode="466000" />
+            <district name="太康县" zipcode="466000" />
+            <district name="西华县" zipcode="466000" />
+            <district name="项城市" zipcode="466000" />
+            <district name="其他" zipcode="466000" />
+        </city>
+        <city name="驻马店市">
+            <district name="泌阳县" zipcode="463000" />
+            <district name="平舆县" zipcode="463000" />
+            <district name="确山县" zipcode="463000" />
+            <district name="汝南县" zipcode="463000" />
+            <district name="上蔡县" zipcode="463000" />
+            <district name="遂平县" zipcode="463000" />
+            <district name="西平县" zipcode="463000" />
+            <district name="新蔡县" zipcode="463000" />
+            <district name="驿城区" zipcode="463000" />
+            <district name="正阳县" zipcode="463000" />
+            <district name="其他" zipcode="463000" />
+        </city>
+        <city name="省直辖行政区">
+            <district name="济源市" zipcode="463000" />
+            <district name="其他" zipcode="463000" />
+        </city>
+    </province>
+    <province name="黑龙江省">
+        <city name="大庆市">
+            <district name="大同区" zipcode="163000" />
+            <district name="杜尔伯特蒙古族自治县" zipcode="163000" />
+            <district name="红岗区" zipcode="163000" />
+            <district name="林甸县" zipcode="163000" />
+            <district name="龙凤区" zipcode="163000" />
+            <district name="让胡路区" zipcode="163000" />
+            <district name="萨尔图区" zipcode="163000" />
+            <district name="肇源县" zipcode="163000" />
+            <district name="肇州县" zipcode="163000" />
+            <district name="其他" zipcode="163000" />
+        </city>
+        <city name="大兴安岭地区">
+            <district name="呼玛县" zipcode="165000" />
+            <district name="漠河县" zipcode="165000" />
+            <district name="塔河县" zipcode="165000" />
+            <district name="其他" zipcode="165000" />
+        </city>
+        <city name="哈尔滨市">
+            <district name="阿城区" zipcode="150000" />
+            <district name="巴彦县" zipcode="150000" />
+            <district name="宾县" zipcode="150000" />
+            <district name="道里区" zipcode="150000" />
+            <district name="道外区" zipcode="150000" />
+            <district name="方正县" zipcode="150000" />
+            <district name="呼兰区" zipcode="150000" />
+            <district name="木兰县" zipcode="150000" />
+            <district name="南岗区" zipcode="150000" />
+            <district name="平房区" zipcode="150000" />
+            <district name="尚志市" zipcode="150000" />
+            <district name="双城市" zipcode="150000" />
+            <district name="松北区" zipcode="150000" />
+            <district name="通河县" zipcode="150000" />
+            <district name="五常市" zipcode="150000" />
+            <district name="香坊区" zipcode="150000" />
+            <district name="延寿县" zipcode="150000" />
+            <district name="依兰县" zipcode="150000" />
+            <district name="其他" zipcode="150000" />
+        </city>
+        <city name="鹤岗市">
+            <district name="东山区" zipcode="154100" />
+            <district name="工农区" zipcode="154100" />
+            <district name="萝北县" zipcode="154100" />
+            <district name="南山区" zipcode="154100" />
+            <district name="绥滨县" zipcode="154100" />
+            <district name="向阳区" zipcode="154100" />
+            <district name="兴安区" zipcode="154100" />
+            <district name="兴山区" zipcode="154100" />
+            <district name="其他" zipcode="154100" />
+        </city>
+        <city name="黑河市">
+            <district name="爱辉区" zipcode="164300" />
+            <district name="北安市" zipcode="164300" />
+            <district name="嫩江县" zipcode="164300" />
+            <district name="孙吴县" zipcode="164300" />
+            <district name="五大连池市" zipcode="164300" />
+            <district name="逊克县" zipcode="164300" />
+            <district name="其他" zipcode="164300" />
+        </city>
+        <city name="鸡西市">
+            <district name="城子河区" zipcode="158100" />
+            <district name="滴道区" zipcode="158100" />
+            <district name="恒山区" zipcode="158100" />
+            <district name="虎林市" zipcode="158100" />
+            <district name="鸡东县" zipcode="158100" />
+            <district name="鸡冠区" zipcode="158100" />
+            <district name="梨树区" zipcode="158100" />
+            <district name="麻山区" zipcode="158100" />
+            <district name="密山市" zipcode="158100" />
+            <district name="其他" zipcode="158100" />
+        </city>
+        <city name="佳木斯市">
+            <district name="东风区" zipcode="154000" />
+            <district name="抚远县" zipcode="154000" />
+            <district name="富锦市" zipcode="154000" />
+            <district name="桦川县" zipcode="154000" />
+            <district name="桦南县" zipcode="154000" />
+            <district name="郊区" zipcode="154000" />
+            <district name="前进区" zipcode="154000" />
+            <district name="汤原县" zipcode="154000" />
+            <district name="同江市" zipcode="154000" />
+            <district name="向阳区" zipcode="154000" />
+            <district name="其他" zipcode="154000" />
+        </city>
+        <city name="牡丹江市">
+            <district name="爱民区" zipcode="157000" />
+            <district name="东安区" zipcode="157000" />
+            <district name="东宁县" zipcode="157000" />
+            <district name="海林市" zipcode="157000" />
+            <district name="林口县" zipcode="157000" />
+            <district name="穆棱市" zipcode="157000" />
+            <district name="宁安市" zipcode="157000" />
+            <district name="绥芬河市" zipcode="157000" />
+            <district name="西安区" zipcode="157000" />
+            <district name="阳明区" zipcode="157000" />
+            <district name="其他" zipcode="157000" />
+        </city>
+        <city name="七台河市">
+            <district name="勃利县" zipcode="154600" />
+            <district name="茄子河区" zipcode="154600" />
+            <district name="桃山区" zipcode="154600" />
+            <district name="新兴区" zipcode="154600" />
+            <district name="其他" zipcode="154600" />
+        </city>
+        <city name="齐齐哈尔市">
+            <district name="昂昂溪区" zipcode="161000" />
+            <district name="拜泉县" zipcode="161000" />
+            <district name="富拉尔基区" zipcode="161000" />
+            <district name="富裕县" zipcode="161000" />
+            <district name="甘南县" zipcode="161000" />
+            <district name="建华区" zipcode="161000" />
+            <district name="克东县" zipcode="161000" />
+            <district name="克山县" zipcode="161000" />
+            <district name="龙江县" zipcode="161000" />
+            <district name="龙沙区" zipcode="161000" />
+            <district name="梅里斯达斡尔族区" zipcode="161000" />
+            <district name="讷河市" zipcode="161000" />
+            <district name="碾子山区" zipcode="161000" />
+            <district name="泰来县" zipcode="161000" />
+            <district name="铁锋区" zipcode="161000" />
+            <district name="依安县" zipcode="161000" />
+            <district name="其他" zipcode="161000" />
+        </city>
+        <city name="双鸭山市">
+            <district name="宝清县" zipcode="155100" />
+            <district name="宝山区" zipcode="155100" />
+            <district name="集贤县" zipcode="155100" />
+            <district name="尖山区" zipcode="155100" />
+            <district name="岭东区" zipcode="155100" />
+            <district name="饶河县" zipcode="155100" />
+            <district name="四方台区" zipcode="155100" />
+            <district name="友谊县" zipcode="155100" />
+            <district name="其他" zipcode="155100" />
+        </city>
+        <city name="绥化市">
+            <district name="安达市" zipcode="152000" />
+            <district name="北林区" zipcode="152000" />
+            <district name="海伦市" zipcode="152000" />
+            <district name="兰西县" zipcode="152000" />
+            <district name="明水县" zipcode="152000" />
+            <district name="青冈县" zipcode="152000" />
+            <district name="庆安县" zipcode="152000" />
+            <district name="绥棱县" zipcode="152000" />
+            <district name="望奎县" zipcode="152000" />
+            <district name="肇东市" zipcode="152000" />
+            <district name="其他" zipcode="152000" />
+        </city>
+        <city name="伊春市">
+            <district name="翠峦区" zipcode="152300" />
+            <district name="带岭区" zipcode="152300" />
+            <district name="红星区" zipcode="152300" />
+            <district name="嘉荫县" zipcode="152300" />
+            <district name="金山屯区" zipcode="152300" />
+            <district name="美溪区" zipcode="152300" />
+            <district name="南岔区" zipcode="152300" />
+            <district name="上甘岭区" zipcode="152300" />
+            <district name="汤旺河区" zipcode="152300" />
+            <district name="铁力市" zipcode="152300" />
+            <district name="乌马河区" zipcode="152300" />
+            <district name="乌伊岭区" zipcode="152300" />
+            <district name="五营区" zipcode="152300" />
+            <district name="西林区" zipcode="152300" />
+            <district name="新青区" zipcode="152300" />
+            <district name="伊春区" zipcode="152300" />
+            <district name="友好区" zipcode="152300" />
+            <district name="其他" zipcode="152300" />
+        </city>
+    </province>
+    <province name="湖北省">
+        <city name="鄂州市">
+            <district name="鄂城区" zipcode="436000" />
+            <district name="华容区" zipcode="436000" />
+            <district name="梁子湖区" zipcode="436000" />
+            <district name="其他" zipcode="436000" />
+        </city>
+        <city name="恩施土家族苗族自治州">
+            <district name="巴东县" zipcode="445000" />
+            <district name="恩施市" zipcode="445000" />
+            <district name="鹤峰县" zipcode="445000" />
+            <district name="建始县" zipcode="445000" />
+            <district name="来凤县" zipcode="445000" />
+            <district name="利川市" zipcode="445000" />
+            <district name="咸丰县" zipcode="445000" />
+            <district name="宣恩县" zipcode="445000" />
+            <district name="其他" zipcode="445000" />
+        </city>
+        <city name="黄冈市">
+            <district name="红安县" zipcode="438000" />
+            <district name="黄梅县" zipcode="438000" />
+            <district name="黄州区" zipcode="438000" />
+            <district name="罗田县" zipcode="438000" />
+            <district name="麻城市" zipcode="438000" />
+            <district name="蕲春县" zipcode="438000" />
+            <district name="团风县" zipcode="438000" />
+            <district name="武穴市" zipcode="438000" />
+            <district name="浠水县" zipcode="438000" />
+            <district name="英山县" zipcode="438000" />
+            <district name="其他" zipcode="438000" />
+        </city>
+        <city name="黄石市">
+            <district name="大冶市" zipcode="435000" />
+            <district name="黄石港区" zipcode="435000" />
+            <district name="铁山区" zipcode="435000" />
+            <district name="西塞山区" zipcode="435000" />
+            <district name="下陆区" zipcode="435000" />
+            <district name="阳新县" zipcode="435000" />
+            <district name="其他" zipcode="435000" />
+        </city>
+        <city name="荆门市">
+            <district name="东宝区" zipcode="448000" />
+            <district name="掇刀区" zipcode="448000" />
+            <district name="京山县" zipcode="448000" />
+            <district name="沙洋县" zipcode="448000" />
+            <district name="钟祥市" zipcode="448000" />
+            <district name="其他" zipcode="448000" />
+        </city>
+        <city name="荆州市">
+            <district name="公安县" zipcode="434000" />
+            <district name="洪湖市" zipcode="434000" />
+            <district name="监利县" zipcode="434000" />
+            <district name="江陵县" zipcode="434000" />
+            <district name="荆州区" zipcode="434000" />
+            <district name="沙市区" zipcode="434000" />
+            <district name="石首市" zipcode="434000" />
+            <district name="松滋市" zipcode="434000" />
+            <district name="其他" zipcode="434000" />
+        </city>
+        <city name="十堰市">
+            <district name="丹江口市" zipcode="442000" />
+            <district name="房县" zipcode="442000" />
+            <district name="茅箭区" zipcode="442000" />
+            <district name="郧西县" zipcode="442000" />
+            <district name="郧县" zipcode="442000" />
+            <district name="张湾区" zipcode="442000" />
+            <district name="竹山县" zipcode="442000" />
+            <district name="竹溪县" zipcode="442000" />
+            <district name="其他" zipcode="442000" />
+        </city>
+        <city name="随州市">
+            <district name="广水市" zipcode="441300" />
+            <district name="曾都区" zipcode="441300" />
+            <district name="随县" zipcode="441300" />
+            <district name="其他" zipcode="441300" />
+        </city>
+        <city name="武汉市">
+            <district name="蔡甸区" zipcode="430000" />
+            <district name="东西湖区" zipcode="430000" />
+            <district name="汉南区" zipcode="430000" />
+            <district name="汉阳区" zipcode="430000" />
+            <district name="洪山区" zipcode="430000" />
+            <district name="黄陂区" zipcode="430000" />
+            <district name="江岸区" zipcode="430000" />
+            <district name="江汉区" zipcode="430000" />
+            <district name="江夏区" zipcode="430000" />
+            <district name="硚口区" zipcode="430000" />
+            <district name="青山区" zipcode="430000" />
+            <district name="武昌区" zipcode="430000" />
+            <district name="新洲区" zipcode="430000" />
+            <district name="其他" zipcode="430000" />
+        </city>
+        <city name="咸宁市">
+            <district name="赤壁市" zipcode="437000" />
+            <district name="崇阳县" zipcode="437000" />
+            <district name="嘉鱼县" zipcode="437000" />
+            <district name="通城县" zipcode="437000" />
+            <district name="通山县" zipcode="437000" />
+            <district name="咸安区" zipcode="437000" />
+            <district name="其他" zipcode="437000" />
+        </city>
+        <city name="襄樊市">
+            <district name="保康县" zipcode="441000" />
+            <district name="樊城区" zipcode="441000" />
+            <district name="谷城县" zipcode="441000" />
+            <district name="老河口市" zipcode="441000" />
+            <district name="南漳县" zipcode="441000" />
+            <district name="襄城区" zipcode="441000" />
+            <district name="襄阳区" zipcode="441000" />
+            <district name="宜城市" zipcode="441000" />
+            <district name="枣阳市" zipcode="441000" />
+            <district name="其他" zipcode="441000" />
+        </city>
+        <city name="孝感市">
+            <district name="安陆市" zipcode="432100" />
+            <district name="大悟县" zipcode="432100" />
+            <district name="汉川市" zipcode="432100" />
+            <district name="孝昌县" zipcode="432100" />
+            <district name="孝南区" zipcode="432100" />
+            <district name="应城市" zipcode="432100" />
+            <district name="云梦县" zipcode="432100" />
+            <district name="其他" zipcode="432100" />
+        </city>
+        <city name="宜昌市">
+            <district name="长阳土家族自治县" zipcode="443000" />
+            <district name="当阳市" zipcode="443000" />
+            <district name="点军区" zipcode="443000" />
+            <district name="五峰土家族自治县" zipcode="443000" />
+            <district name="伍家岗区" zipcode="443000" />
+            <district name="西陵区" zipcode="443000" />
+            <district name="猇亭区" zipcode="443000" />
+            <district name="兴山县" zipcode="443000" />
+            <district name="夷陵区" zipcode="443000" />
+            <district name="宜都市" zipcode="443000" />
+            <district name="远安县" zipcode="443000" />
+            <district name="枝江市" zipcode="443000" />
+            <district name="秭归县" zipcode="443000" />
+            <district name="其他" zipcode="443000" />
+        </city>
+        <city name="省直辖">
+            <district name="仙桃市" zipcode="443000" />
+            <district name="潜江市" zipcode="443000" />
+            <district name="天门市" zipcode="443000" />
+            <district name="神农架林区" zipcode="443000" />
+            <district name="其他" zipcode="443000" />
+        </city>
+    </province>
+    <province name="湖南省">
+        <city name="长沙市">
+            <district name="长沙县" zipcode="410000" />
+            <district name="芙蓉区" zipcode="410000" />
+            <district name="开福区" zipcode="410000" />
+            <district name="浏阳市" zipcode="410000" />
+            <district name="宁乡县" zipcode="410000" />
+            <district name="天心区" zipcode="410000" />
+            <district name="望城县" zipcode="410000" />
+            <district name="雨花区" zipcode="410000" />
+            <district name="岳麓区" zipcode="410000" />
+            <district name="其他" zipcode="410000" />
+        </city>
+        <city name="常德市">
+            <district name="安乡县" zipcode="415000" />
+            <district name="鼎城区" zipcode="415000" />
+            <district name="汉寿县" zipcode="415000" />
+            <district name="津市市" zipcode="415000" />
+            <district name="澧县" zipcode="415000" />
+            <district name="临澧县" zipcode="415000" />
+            <district name="石门县" zipcode="415000" />
+            <district name="桃源县" zipcode="415000" />
+            <district name="武陵区" zipcode="415000" />
+            <district name="其他" zipcode="415000" />
+        </city>
+        <city name="郴州市">
+            <district name="安仁县" zipcode="423000" />
+            <district name="北湖区" zipcode="423000" />
+            <district name="桂东县" zipcode="423000" />
+            <district name="桂阳县" zipcode="423000" />
+            <district name="嘉禾县" zipcode="423000" />
+            <district name="临武县" zipcode="423000" />
+            <district name="汝城县" zipcode="423000" />
+            <district name="苏仙区" zipcode="423000" />
+            <district name="宜章县" zipcode="423000" />
+            <district name="永兴县" zipcode="423000" />
+            <district name="资兴市" zipcode="423000" />
+            <district name="其他" zipcode="423000" />
+        </city>
+        <city name="衡阳市">
+            <district name="常宁市" zipcode="421000" />
+            <district name="衡东县" zipcode="421000" />
+            <district name="衡南县" zipcode="421000" />
+            <district name="衡山县" zipcode="421000" />
+            <district name="衡阳县" zipcode="421000" />
+            <district name="耒阳市" zipcode="421000" />
+            <district name="南岳区" zipcode="421000" />
+            <district name="祁东县" zipcode="421000" />
+            <district name="石鼓区" zipcode="421000" />
+            <district name="雁峰区" zipcode="421000" />
+            <district name="蒸湘区" zipcode="421000" />
+            <district name="珠晖区" zipcode="421000" />
+            <district name="其他" zipcode="421000" />
+        </city>
+        <city name="怀化市">
+            <district name="辰溪县" zipcode="418000" />
+            <district name="鹤城区" zipcode="418000" />
+            <district name="洪江市" zipcode="418000" />
+            <district name="会同县" zipcode="418000" />
+            <district name="靖州苗族侗族自治县" zipcode="418000" />
+            <district name="麻阳苗族自治县" zipcode="418000" />
+            <district name="通道侗族自治县" zipcode="418000" />
+            <district name="新晃侗族自治县" zipcode="418000" />
+            <district name="溆浦县" zipcode="418000" />
+            <district name="沅陵县" zipcode="418000" />
+            <district name="芷江侗族自治县" zipcode="418000" />
+            <district name="中方县" zipcode="418000" />
+            <district name="其他" zipcode="418000" />
+        </city>
+        <city name="娄底市">
+            <district name="冷水江市" zipcode="417000" />
+            <district name="涟源市" zipcode="417000" />
+            <district name="娄星区" zipcode="417000" />
+            <district name="双峰县" zipcode="417000" />
+            <district name="新化县" zipcode="417000" />
+            <district name="其他" zipcode="417000" />
+        </city>
+        <city name="邵阳市">
+            <district name="北塔区" zipcode="422000" />
+            <district name="城步苗族自治县" zipcode="422000" />
+            <district name="大祥区" zipcode="422000" />
+            <district name="洞口县" zipcode="422000" />
+            <district name="隆回县" zipcode="422000" />
+            <district name="邵东县" zipcode="422000" />
+            <district name="邵阳县" zipcode="422000" />
+            <district name="双清区" zipcode="422000" />
+            <district name="绥宁县" zipcode="422000" />
+            <district name="武冈市" zipcode="422000" />
+            <district name="新宁县" zipcode="422000" />
+            <district name="新邵县" zipcode="422000" />
+            <district name="其他" zipcode="422000" />
+        </city>
+        <city name="湘潭市">
+            <district name="韶山市" zipcode="411100" />
+            <district name="湘潭县" zipcode="411100" />
+            <district name="湘乡市" zipcode="411100" />
+            <district name="雨湖区" zipcode="411100" />
+            <district name="岳塘区" zipcode="411100" />
+            <district name="其他" zipcode="411100" />
+        </city>
+        <city name="湘西土家族苗族自治州">
+            <district name="保靖县" zipcode="416000" />
+            <district name="凤凰县" zipcode="416000" />
+            <district name="古丈县" zipcode="416000" />
+            <district name="花垣县" zipcode="416000" />
+            <district name="吉首市" zipcode="416000" />
+            <district name="龙山县" zipcode="416000" />
+            <district name="泸溪县" zipcode="416000" />
+            <district name="永顺县" zipcode="416000" />
+            <district name="其他" zipcode="416000" />
+        </city>
+        <city name="益阳市">
+            <district name="安化县" zipcode="413000" />
+            <district name="赫山区" zipcode="413000" />
+            <district name="南县" zipcode="413000" />
+            <district name="桃江县" zipcode="413000" />
+            <district name="沅江市" zipcode="413000" />
+            <district name="资阳区" zipcode="413000" />
+            <district name="其他" zipcode="413000" />
+        </city>
+        <city name="永州市">
+            <district name="道县" zipcode="425000" />
+            <district name="东安县" zipcode="425000" />
+            <district name="江华瑶族自治县" zipcode="425000" />
+            <district name="江永县" zipcode="425000" />
+            <district name="蓝山县" zipcode="425000" />
+            <district name="冷水滩区" zipcode="425000" />
+            <district name="零陵区" zipcode="425000" />
+            <district name="宁远县" zipcode="425000" />
+            <district name="祁阳县" zipcode="425000" />
+            <district name="双牌县" zipcode="425000" />
+            <district name="新田县" zipcode="425000" />
+            <district name="其他" zipcode="425000" />
+        </city>
+        <city name="岳阳市">
+            <district name="华容县" zipcode="414000" />
+            <district name="君山区" zipcode="414000" />
+            <district name="临湘市" zipcode="414000" />
+            <district name="汨罗市" zipcode="414000" />
+            <district name="平江县" zipcode="414000" />
+            <district name="湘阴县" zipcode="414000" />
+            <district name="岳阳楼区" zipcode="414000" />
+            <district name="岳阳县" zipcode="414000" />
+            <district name="云溪区" zipcode="414000" />
+            <district name="其他" zipcode="414000" />
+        </city>
+        <city name="张家界市">
+            <district name="慈利县" zipcode="427000" />
+            <district name="桑植县" zipcode="427000" />
+            <district name="武陵源区" zipcode="427000" />
+            <district name="永定区" zipcode="427000" />
+            <district name="其他" zipcode="427000" />
+        </city>
+        <city name="株洲市">
+            <district name="茶陵县" zipcode="412000" />
+            <district name="荷塘区" zipcode="412000" />
+            <district name="醴陵市" zipcode="412000" />
+            <district name="芦淞区" zipcode="412000" />
+            <district name="石峰区" zipcode="412000" />
+            <district name="天元区" zipcode="412000" />
+            <district name="炎陵县" zipcode="412000" />
+            <district name="攸县" zipcode="412000" />
+            <district name="株洲县" zipcode="412000" />
+            <district name="其他" zipcode="412000" />
+        </city>
+    </province>
+    <province name="吉林省">
+        <city name="白城市">
+            <district name="大安市" zipcode="137000" />
+            <district name="洮北区" zipcode="137000" />
+            <district name="洮南市" zipcode="137000" />
+            <district name="通榆县" zipcode="137000" />
+            <district name="镇赉县" zipcode="137000" />
+            <district name="其他" zipcode="137000" />
+        </city>
+        <city name="白山市">
+            <district name="八道江区" zipcode="134300" />
+            <district name="长白朝鲜族自治县" zipcode="134300" />
+            <district name="抚松县" zipcode="134300" />
+            <district name="江源区" zipcode="134300" />
+            <district name="靖宇县" zipcode="134300" />
+            <district name="临江市" zipcode="134300" />
+            <district name="其他" zipcode="134300" />
+        </city>
+        <city name="长春市">
+            <district name="朝阳区" zipcode="130000" />
+            <district name="德惠市" zipcode="130000" />
+            <district name="二道区" zipcode="130000" />
+            <district name="九台市" zipcode="130000" />
+            <district name="宽城区" zipcode="130000" />
+            <district name="绿园区" zipcode="130000" />
+            <district name="南关区" zipcode="130000" />
+            <district name="农安县" zipcode="130000" />
+            <district name="双阳区" zipcode="130000" />
+            <district name="榆树市" zipcode="130000" />
+            <district name="其他" zipcode="130000" />
+        </city>
+        <city name="吉林市">
+            <district name="昌邑区" zipcode="132000" />
+            <district name="船营区" zipcode="132000" />
+            <district name="丰满区" zipcode="132000" />
+            <district name="桦甸市" zipcode="132000" />
+            <district name="蛟河市" zipcode="132000" />
+            <district name="龙潭区" zipcode="132000" />
+            <district name="磐石市" zipcode="132000" />
+            <district name="舒兰市" zipcode="132000" />
+            <district name="永吉县" zipcode="132000" />
+            <district name="其他" zipcode="132000" />
+        </city>
+        <city name="辽源市">
+            <district name="东丰县" zipcode="136200" />
+            <district name="东辽县" zipcode="136200" />
+            <district name="龙山区" zipcode="136200" />
+            <district name="西安区" zipcode="136200" />
+            <district name="其他" zipcode="136200" />
+        </city>
+        <city name="四平市">
+            <district name="公主岭市" zipcode="136000" />
+            <district name="梨树县" zipcode="136000" />
+            <district name="双辽市" zipcode="136000" />
+            <district name="铁东区" zipcode="136000" />
+            <district name="铁西区" zipcode="136000" />
+            <district name="伊通满族自治县" zipcode="136000" />
+            <district name="其他" zipcode="136000" />
+        </city>
+        <city name="松原市">
+            <district name="长岭县" zipcode="131100" />
+            <district name="扶余县" zipcode="131100" />
+            <district name="宁江区" zipcode="131100" />
+            <district name="前郭尔罗斯蒙古族自治县" zipcode="131100" />
+            <district name="乾安县" zipcode="131100" />
+            <district name="其他" zipcode="131100" />
+        </city>
+        <city name="通化市">
+            <district name="东昌区" zipcode="134000" />
+            <district name="二道江区" zipcode="134000" />
+            <district name="辉南县" zipcode="134000" />
+            <district name="集安市" zipcode="134000" />
+            <district name="柳河县" zipcode="134000" />
+            <district name="梅河口市" zipcode="134000" />
+            <district name="通化县" zipcode="134000" />
+            <district name="其他" zipcode="134000" />
+        </city>
+        <city name="延边朝鲜族自治州">
+            <district name="安图县" zipcode="133000" />
+            <district name="敦化市" zipcode="133000" />
+            <district name="和龙市" zipcode="133000" />
+            <district name="珲春市" zipcode="133000" />
+            <district name="龙井市" zipcode="133000" />
+            <district name="图们市" zipcode="133000" />
+            <district name="汪清县" zipcode="133000" />
+            <district name="延吉市" zipcode="133000" />
+            <district name="其他" zipcode="133000" />
+        </city>
+    </province>
+    <province name="江苏省">
+        <city name="常州市">
+            <district name="金坛市" zipcode="213000" />
+            <district name="溧阳市" zipcode="213000" />
+            <district name="戚墅堰区" zipcode="213000" />
+            <district name="天宁区" zipcode="213000" />
+            <district name="武进区" zipcode="213000" />
+            <district name="新北区" zipcode="213000" />
+            <district name="钟楼区" zipcode="213000" />
+            <district name="其他" zipcode="213000" />
+        </city>
+        <city name="淮安市">
+            <district name="楚州区" zipcode="223200" />
+            <district name="洪泽县" zipcode="223200" />
+            <district name="淮阴区" zipcode="223200" />
+            <district name="金湖县" zipcode="223200" />
+            <district name="涟水县" zipcode="223200" />
+            <district name="清河区" zipcode="223200" />
+            <district name="清浦区" zipcode="223200" />
+            <district name="盱眙县" zipcode="223200" />
+            <district name="其他" zipcode="223200" />
+        </city>
+        <city name="连云港市">
+            <district name="东海县" zipcode="222000" />
+            <district name="赣榆县" zipcode="222000" />
+            <district name="灌南县" zipcode="222000" />
+            <district name="灌云县" zipcode="222000" />
+            <district name="海州区" zipcode="222000" />
+            <district name="连云区" zipcode="222000" />
+            <district name="新浦区" zipcode="222000" />
+            <district name="其他" zipcode="222000" />
+        </city>
+        <city name="南京市">
+            <district name="白下区" zipcode="210000" />
+            <district name="高淳县" zipcode="210000" />
+            <district name="鼓楼区" zipcode="210000" />
+            <district name="建邺区" zipcode="210000" />
+            <district name="江宁区" zipcode="210000" />
+            <district name="溧水县" zipcode="210000" />
+            <district name="六合区" zipcode="210000" />
+            <district name="浦口区" zipcode="210000" />
+            <district name="栖霞区" zipcode="210000" />
+            <district name="秦淮区" zipcode="210000" />
+            <district name="下关区" zipcode="210000" />
+            <district name="玄武区" zipcode="210000" />
+            <district name="雨花台区" zipcode="210000" />
+            <district name="其他" zipcode="210000" />
+        </city>
+        <city name="南通市">
+            <district name="崇川区" zipcode="226000" />
+            <district name="港闸区" zipcode="226000" />
+            <district name="海安县" zipcode="226000" />
+            <district name="海门市" zipcode="226000" />
+            <district name="启东市" zipcode="226000" />
+            <district name="如东县" zipcode="226000" />
+            <district name="如皋市" zipcode="226000" />
+            <district name="通州市" zipcode="226000" />
+            <district name="其他" zipcode="226000" />
+        </city>
+        <city name="苏州市">
+            <district name="沧浪区" zipcode="215000" />
+            <district name="常熟市" zipcode="215000" />
+            <district name="虎丘区" zipcode="215000" />
+            <district name="金阊区" zipcode="215000" />
+            <district name="昆山市" zipcode="215000" />
+            <district name="平江区" zipcode="215000" />
+            <district name="太仓市" zipcode="215000" />
+            <district name="吴江市" zipcode="215000" />
+            <district name="吴中区" zipcode="215000" />
+            <district name="相城区" zipcode="215000" />
+            <district name="张家港市" zipcode="215000" />
+            <district name="其他" zipcode="215000" />
+        </city>
+        <city name="宿迁市">
+            <district name="沭阳县" zipcode="223800" />
+            <district name="泗洪县" zipcode="223800" />
+            <district name="泗阳县" zipcode="223800" />
+            <district name="宿城区" zipcode="223800" />
+            <district name="宿豫区" zipcode="223800" />
+            <district name="其他" zipcode="223800" />
+        </city>
+        <city name="泰州市">
+            <district name="高港区" zipcode="225300" />
+            <district name="海陵区" zipcode="225300" />
+            <district name="姜堰市" zipcode="225300" />
+            <district name="靖江市" zipcode="225300" />
+            <district name="泰兴市" zipcode="225300" />
+            <district name="兴化市" zipcode="225300" />
+            <district name="其他" zipcode="225300" />
+        </city>
+        <city name="无锡市">
+            <district name="北塘区" zipcode="214000" />
+            <district name="滨湖区" zipcode="214000" />
+            <district name="崇安区" zipcode="214000" />
+            <district name="惠山区" zipcode="214000" />
+            <district name="江阴市" zipcode="214000" />
+            <district name="南长区" zipcode="214000" />
+            <district name="锡山区" zipcode="214000" />
+            <district name="宜兴市" zipcode="214000" />
+            <district name="其他" zipcode="214000" />
+        </city>
+        <city name="徐州市">
+            <district name="丰县" zipcode="221000" />
+            <district name="鼓楼区" zipcode="221000" />
+            <district name="贾汪区" zipcode="221000" />
+            <district name="九里区" zipcode="221000" />
+            <district name="沛县" zipcode="221000" />
+            <district name="邳州市" zipcode="221000" />
+            <district name="泉山区" zipcode="221000" />
+            <district name="睢宁县" zipcode="221000" />
+            <district name="铜山县" zipcode="221000" />
+            <district name="新沂市" zipcode="221000" />
+            <district name="云龙区" zipcode="221000" />
+            <district name="其他" zipcode="221000" />
+        </city>
+        <city name="盐城市">
+            <district name="滨海县" zipcode="224000" />
+            <district name="大丰市" zipcode="224000" />
+            <district name="东台市" zipcode="224000" />
+            <district name="阜宁县" zipcode="224000" />
+            <district name="建湖县" zipcode="224000" />
+            <district name="射阳县" zipcode="224000" />
+            <district name="亭湖区" zipcode="224000" />
+            <district name="响水县" zipcode="224000" />
+            <district name="盐都区" zipcode="224000" />
+            <district name="其他" zipcode="224000" />
+        </city>
+        <city name="扬州市">
+            <district name="宝应县" zipcode="225000" />
+            <district name="高邮市" zipcode="225000" />
+            <district name="广陵区" zipcode="225000" />
+            <district name="邗江区" zipcode="225000" />
+            <district name="江都市" zipcode="225000" />
+            <district name="维扬区" zipcode="225000" />
+            <district name="仪征市" zipcode="225000" />
+            <district name="其他" zipcode="225000" />
+        </city>
+        <city name="镇江市">
+            <district name="丹徒区" zipcode="212000" />
+            <district name="丹阳市" zipcode="212000" />
+            <district name="京口区" zipcode="212000" />
+            <district name="句容市" zipcode="212000" />
+            <district name="润州区" zipcode="212000" />
+            <district name="扬中市" zipcode="212000" />
+            <district name="其他" zipcode="212000" />
+        </city>
+    </province>
+    <province name="江西省">
+        <city name="抚州市">
+            <district name="崇仁县" zipcode="332900" />
+            <district name="东乡县" zipcode="332900" />
+            <district name="广昌县" zipcode="332900" />
+            <district name="金溪县" zipcode="332900" />
+            <district name="乐安县" zipcode="332900" />
+            <district name="黎川县" zipcode="332900" />
+            <district name="临川区" zipcode="332900" />
+            <district name="南城县" zipcode="332900" />
+            <district name="南丰县" zipcode="332900" />
+            <district name="宜黄县" zipcode="332900" />
+            <district name="资溪县" zipcode="332900" />
+            <district name="其他" zipcode="332900" />
+        </city>
+        <city name="赣州市">
+            <district name="安远县" zipcode="341000" />
+            <district name="崇义县" zipcode="341000" />
+            <district name="大余县" zipcode="341000" />
+            <district name="定南县" zipcode="341000" />
+            <district name="赣县" zipcode="341000" />
+            <district name="会昌县" zipcode="341000" />
+            <district name="龙南县" zipcode="341000" />
+            <district name="南康市" zipcode="341000" />
+            <district name="宁都县" zipcode="341000" />
+            <district name="全南县" zipcode="341000" />
+            <district name="瑞金市" zipcode="341000" />
+            <district name="上犹县" zipcode="341000" />
+            <district name="石城县" zipcode="341000" />
+            <district name="信丰县" zipcode="341000" />
+            <district name="兴国县" zipcode="341000" />
+            <district name="寻乌县" zipcode="341000" />
+            <district name="于都县" zipcode="341000" />
+            <district name="章贡区" zipcode="341000" />
+            <district name="其他" zipcode="341000" />
+        </city>
+        <city name="吉安市">
+            <district name="安福县" zipcode="343000" />
+            <district name="吉安县" zipcode="343000" />
+            <district name="吉水县" zipcode="343000" />
+            <district name="吉州区" zipcode="343000" />
+            <district name="井冈山市" zipcode="343000" />
+            <district name="青原区" zipcode="343000" />
+            <district name="遂川县" zipcode="343000" />
+            <district name="泰和县" zipcode="343000" />
+            <district name="万安县" zipcode="343000" />
+            <district name="峡江县" zipcode="343000" />
+            <district name="新干县" zipcode="343000" />
+            <district name="永丰县" zipcode="343000" />
+            <district name="永新县" zipcode="343000" />
+            <district name="其他" zipcode="343000" />
+        </city>
+        <city name="景德镇市">
+            <district name="昌江区" zipcode="333000" />
+            <district name="浮梁县" zipcode="333000" />
+            <district name="乐平市" zipcode="333000" />
+            <district name="珠山区" zipcode="333000" />
+            <district name="其他" zipcode="333000" />
+        </city>
+        <city name="九江市">
+            <district name="德安县" zipcode="332000" />
+            <district name="都昌县" zipcode="332000" />
+            <district name="共青城" zipcode="332000" />
+            <district name="湖口县" zipcode="332000" />
+            <district name="九江县" zipcode="332000" />
+            <district name="庐山区" zipcode="332000" />
+            <district name="彭泽县" zipcode="332000" />
+            <district name="瑞昌市" zipcode="332000" />
+            <district name="武宁县" zipcode="332000" />
+            <district name="星子县" zipcode="332000" />
+            <district name="修水县" zipcode="332000" />
+            <district name="浔阳区" zipcode="332000" />
+            <district name="永修县" zipcode="332000" />
+            <district name="其他" zipcode="332000" />
+        </city>
+        <city name="南昌市">
+            <district name="安义县" zipcode="330000" />
+            <district name="东湖区" zipcode="330000" />
+            <district name="进贤县" zipcode="330000" />
+            <district name="南昌县" zipcode="330000" />
+            <district name="青山湖区" zipcode="330000" />
+            <district name="青云谱区" zipcode="330000" />
+            <district name="湾里区" zipcode="330000" />
+            <district name="西湖区" zipcode="330000" />
+            <district name="新建县" zipcode="330000" />
+            <district name="其他" zipcode="330000" />
+        </city>
+        <city name="萍乡市">
+            <district name="安源区" zipcode="337000" />
+            <district name="莲花县" zipcode="337000" />
+            <district name="芦溪县" zipcode="337000" />
+            <district name="上栗县" zipcode="337000" />
+            <district name="湘东区" zipcode="337000" />
+            <district name="其他" zipcode="337000" />
+        </city>
+        <city name="上饶市">
+            <district name="德兴市" zipcode="334000" />
+            <district name="广丰县" zipcode="334000" />
+            <district name="横峰县" zipcode="334000" />
+            <district name="鄱阳县" zipcode="334000" />
+            <district name="铅山县" zipcode="334000" />
+            <district name="上饶县" zipcode="334000" />
+            <district name="万年县" zipcode="334000" />
+            <district name="婺源县" zipcode="334000" />
+            <district name="信州区" zipcode="334000" />
+            <district name="弋阳县" zipcode="334000" />
+            <district name="余干县" zipcode="334000" />
+            <district name="玉山县" zipcode="334000" />
+            <district name="其他" zipcode="334000" />
+        </city>
+        <city name="新余市">
+            <district name="分宜县" zipcode="338000" />
+            <district name="渝水区" zipcode="338000" />
+            <district name="其他" zipcode="338000" />
+        </city>
+        <city name="宜春市">
+            <district name="丰城市" zipcode="336000" />
+            <district name="奉新县" zipcode="336000" />
+            <district name="高安市" zipcode="336000" />
+            <district name="靖安县" zipcode="336000" />
+            <district name="上高县" zipcode="336000" />
+            <district name="铜鼓县" zipcode="336000" />
+            <district name="万载县" zipcode="336000" />
+            <district name="宜丰县" zipcode="336000" />
+            <district name="袁州区" zipcode="336000" />
+            <district name="樟树市" zipcode="336000" />
+            <district name="其他" zipcode="336000" />
+        </city>
+        <city name="鹰潭市">
+            <district name="贵溪市" zipcode="335000" />
+            <district name="余江县" zipcode="335000" />
+            <district name="月湖区" zipcode="335000" />
+            <district name="其他" zipcode="335000" />
+        </city>
+    </province>
+    <province name="辽宁省">
+        <city name="鞍山市">
+            <district name="海城市" zipcode="114000" />
+            <district name="立山区" zipcode="114000" />
+            <district name="千山区" zipcode="114000" />
+            <district name="台安县" zipcode="114000" />
+            <district name="铁东区" zipcode="114000" />
+            <district name="铁西区" zipcode="114000" />
+            <district name="岫岩满族自治县" zipcode="114000" />
+            <district name="其他" zipcode="114000" />
+        </city>
+        <city name="本溪市">
+            <district name="本溪满族自治县" zipcode="117000" />
+            <district name="桓仁满族自治县" zipcode="117000" />
+            <district name="明山区" zipcode="117000" />
+            <district name="南芬区" zipcode="117000" />
+            <district name="平山区" zipcode="117000" />
+            <district name="溪湖区" zipcode="117000" />
+            <district name="其他" zipcode="117000" />
+        </city>
+        <city name="朝阳市">
+            <district name="北票市" zipcode="122000" />
+            <district name="朝阳县" zipcode="122000" />
+            <district name="建平县" zipcode="122000" />
+            <district name="喀喇沁左翼蒙古族自治县" zipcode="122000" />
+            <district name="凌源市" zipcode="122000" />
+            <district name="龙城区" zipcode="122000" />
+            <district name="双塔区" zipcode="122000" />
+            <district name="其他" zipcode="122000" />
+        </city>
+        <city name="大连市">
+            <district name="长海县" zipcode="116000" />
+            <district name="甘井子区" zipcode="116000" />
+            <district name="金州区" zipcode="116000" />
+            <district name="旅顺口区" zipcode="116000" />
+            <district name="普兰店市" zipcode="116000" />
+            <district name="沙河口区" zipcode="116000" />
+            <district name="瓦房店市" zipcode="116000" />
+            <district name="西岗区" zipcode="116000" />
+            <district name="中山区" zipcode="116000" />
+            <district name="庄河市" zipcode="116000" />
+            <district name="其他" zipcode="116000" />
+        </city>
+        <city name="丹东市">
+            <district name="东港市" zipcode="118000" />
+            <district name="凤城市" zipcode="118000" />
+            <district name="宽甸满族自治县" zipcode="118000" />
+            <district name="元宝区" zipcode="118000" />
+            <district name="振安区" zipcode="118000" />
+            <district name="振兴区" zipcode="118000" />
+            <district name="其他" zipcode="118000" />
+        </city>
+        <city name="抚顺市">
+            <district name="东洲区" zipcode="113000" />
+            <district name="抚顺县" zipcode="113000" />
+            <district name="清原满族自治县" zipcode="113000" />
+            <district name="顺城区" zipcode="113000" />
+            <district name="望花区" zipcode="113000" />
+            <district name="新宾满族自治县" zipcode="113000" />
+            <district name="新抚区" zipcode="113000" />
+            <district name="其他" zipcode="113000" />
+        </city>
+        <city name="阜新市">
+            <district name="阜新蒙古族自治县" zipcode="123000" />
+            <district name="海州区" zipcode="123000" />
+            <district name="清河门区" zipcode="123000" />
+            <district name="太平区" zipcode="123000" />
+            <district name="细河区" zipcode="123000" />
+            <district name="新邱区" zipcode="123000" />
+            <district name="彰武县" zipcode="123000" />
+            <district name="其他" zipcode="123000" />
+        </city>
+        <city name="葫芦岛市">
+            <district name="建昌县" zipcode="125000" />
+            <district name="连山区" zipcode="125000" />
+            <district name="龙港区" zipcode="125000" />
+            <district name="南票区" zipcode="125000" />
+            <district name="绥中县" zipcode="125000" />
+            <district name="兴城市" zipcode="125000" />
+            <district name="其他" zipcode="125000" />
+        </city>
+        <city name="锦州市">
+            <district name="北镇市" zipcode="121000" />
+            <district name="古塔区" zipcode="121000" />
+            <district name="黑山县" zipcode="121000" />
+            <district name="凌海市" zipcode="121000" />
+            <district name="凌河区" zipcode="121000" />
+            <district name="太和区" zipcode="121000" />
+            <district name="义县" zipcode="121000" />
+            <district name="其他" zipcode="121000" />
+        </city>
+        <city name="辽阳市">
+            <district name="白塔区" zipcode="111000" />
+            <district name="灯塔市" zipcode="111000" />
+            <district name="弓长岭区" zipcode="111000" />
+            <district name="宏伟区" zipcode="111000" />
+            <district name="辽阳县" zipcode="111000" />
+            <district name="太子河区" zipcode="111000" />
+            <district name="文圣区" zipcode="111000" />
+            <district name="其他" zipcode="111000" />
+        </city>
+        <city name="盘锦市">
+            <district name="大洼县" zipcode="124000" />
+            <district name="盘山县" zipcode="124000" />
+            <district name="双台子区" zipcode="124000" />
+            <district name="兴隆台区" zipcode="124000" />
+            <district name="其他" zipcode="124000" />
+        </city>
+        <city name="沈阳市">
+            <district name="大东区" zipcode="110000" />
+            <district name="东陵区" zipcode="110000" />
+            <district name="法库县" zipcode="110000" />
+            <district name="和平区" zipcode="110000" />
+            <district name="皇姑区" zipcode="110000" />
+            <district name="康平县" zipcode="110000" />
+            <district name="辽中县" zipcode="110000" />
+            <district name="沈北新区" zipcode="110000" />
+            <district name="沈河区" zipcode="110000" />
+            <district name="苏家屯区" zipcode="110000" />
+            <district name="铁西区" zipcode="110000" />
+            <district name="新民市" zipcode="110000" />
+            <district name="于洪区" zipcode="110000" />
+            <district name="其他" zipcode="110000" />
+        </city>
+        <city name="铁岭市">
+            <district name="昌图县" zipcode="112000" />
+            <district name="调兵山市" zipcode="112000" />
+            <district name="开原市" zipcode="112000" />
+            <district name="清河区" zipcode="112000" />
+            <district name="铁岭县" zipcode="112000" />
+            <district name="西丰县" zipcode="112000" />
+            <district name="银州区" zipcode="112000" />
+            <district name="其他" zipcode="112000" />
+        </city>
+        <city name="营口市">
+            <district name="鲅鱼圈区" zipcode="115000" />
+            <district name="大石桥市" zipcode="115000" />
+            <district name="盖州市" zipcode="115000" />
+            <district name="老边区" zipcode="115000" />
+            <district name="西市区" zipcode="115000" />
+            <district name="站前区" zipcode="115000" />
+            <district name="其他" zipcode="115000" />
+        </city>
+    </province>
+    <province name="内蒙古自治区">
+        <city name="阿拉善盟">
+            <district name="阿拉善右旗" zipcode="750300" />
+            <district name="阿拉善左旗" zipcode="750300" />
+            <district name="额济纳旗" zipcode="750300" />
+            <district name="其他" zipcode="750300" />
+        </city>
+        <city name="巴彦淖尔市">
+            <district name="磴口县" zipcode="014400" />
+            <district name="杭锦后旗" zipcode="014400" />
+            <district name="临河区" zipcode="014400" />
+            <district name="乌拉特后旗" zipcode="014400" />
+            <district name="乌拉特前旗" zipcode="014400" />
+            <district name="乌拉特中旗" zipcode="014400" />
+            <district name="五原县" zipcode="014400" />
+            <district name="其他" zipcode="14400" />
+        </city>
+        <city name="包头市">
+            <district name="白云鄂博矿区" zipcode="014000" />
+            <district name="达尔罕茂明安联合旗" zipcode="014000" />
+            <district name="东河区" zipcode="014000" />
+            <district name="固阳县" zipcode="014000" />
+            <district name="九原区" zipcode="014000" />
+            <district name="昆都仑区" zipcode="014000" />
+            <district name="青山区" zipcode="014000" />
+            <district name="石拐区" zipcode="014000" />
+            <district name="土默特右旗" zipcode="014000" />
+            <district name="其他" zipcode="14000" />
+        </city>
+        <city name="赤峰市">
+            <district name="阿鲁科尔沁旗" zipcode="024000" />
+            <district name="敖汉旗" zipcode="024000" />
+            <district name="巴林右旗" zipcode="024000" />
+            <district name="巴林左旗" zipcode="024000" />
+            <district name="红山区" zipcode="024000" />
+            <district name="喀喇沁旗" zipcode="024000" />
+            <district name="克什克腾旗" zipcode="024000" />
+            <district name="林西县" zipcode="024000" />
+            <district name="宁城县" zipcode="024000" />
+            <district name="松山区" zipcode="024000" />
+            <district name="翁牛特旗" zipcode="024000" />
+            <district name="元宝山区" zipcode="024000" />
+            <district name="其他" zipcode="24000" />
+        </city>
+        <city name="鄂尔多斯市">
+            <district name="达拉特旗" zipcode="010300" />
+            <district name="东胜区" zipcode="010300" />
+            <district name="鄂托克旗" zipcode="010300" />
+            <district name="鄂托克前旗" zipcode="010300" />
+            <district name="杭锦旗" zipcode="010300" />
+            <district name="乌审旗" zipcode="010300" />
+            <district name="伊金霍洛旗" zipcode="010300" />
+            <district name="准格尔旗" zipcode="010300" />
+            <district name="其他" zipcode="10300" />
+        </city>
+        <city name="呼和浩特市">
+            <district name="和林格尔县" zipcode="010000" />
+            <district name="回民区" zipcode="010000" />
+            <district name="清水河县" zipcode="010000" />
+            <district name="赛罕区" zipcode="010000" />
+            <district name="土默特左旗" zipcode="010000" />
+            <district name="托克托县" zipcode="010000" />
+            <district name="武川县" zipcode="010000" />
+            <district name="新城区" zipcode="010000" />
+            <district name="玉泉区" zipcode="010000" />
+            <district name="其他" zipcode="10000" />
+        </city>
+        <city name="呼伦贝尔市">
+            <district name="阿荣旗" zipcode="021000" />
+            <district name="陈巴尔虎旗" zipcode="021000" />
+            <district name="额尔古纳市" zipcode="021000" />
+            <district name="鄂伦春自治旗" zipcode="021000" />
+            <district name="鄂温克族自治旗" zipcode="021000" />
+            <district name="根河市" zipcode="021000" />
+            <district name="海拉尔区" zipcode="021000" />
+            <district name="满洲里市" zipcode="021000" />
+            <district name="莫力达瓦达斡尔族自治旗" zipcode="021000" />
+            <district name="新巴尔虎右旗" zipcode="021000" />
+            <district name="新巴尔虎左旗" zipcode="021000" />
+            <district name="牙克石市" zipcode="021000" />
+            <district name="扎兰屯市" zipcode="021000" />
+            <district name="其他" zipcode="21000" />
+        </city>
+        <city name="通辽市">
+            <district name="霍林郭勒市" zipcode="028000" />
+            <district name="开鲁县" zipcode="028000" />
+            <district name="科尔沁区" zipcode="028000" />
+            <district name="科尔沁左翼后旗" zipcode="028000" />
+            <district name="科尔沁左翼中旗" zipcode="028000" />
+            <district name="库伦旗" zipcode="028000" />
+            <district name="奈曼旗" zipcode="028000" />
+            <district name="扎鲁特旗" zipcode="028000" />
+            <district name="其他" zipcode="28000" />
+        </city>
+        <city name="乌海市">
+            <district name="海勃湾区" zipcode="016000" />
+            <district name="海南区" zipcode="016000" />
+            <district name="乌达区" zipcode="016000" />
+            <district name="其他" zipcode="16000" />
+        </city>
+        <city name="乌兰察布市">
+            <district name="察哈尔右翼后旗" zipcode="012000" />
+            <district name="察哈尔右翼前旗" zipcode="012000" />
+            <district name="察哈尔右翼中旗" zipcode="012000" />
+            <district name="丰镇市" zipcode="012000" />
+            <district name="化德县" zipcode="012000" />
+            <district name="集宁区" zipcode="012000" />
+            <district name="凉城县" zipcode="012000" />
+            <district name="商都县" zipcode="012000" />
+            <district name="四子王旗" zipcode="012000" />
+            <district name="兴和县" zipcode="012000" />
+            <district name="卓资县" zipcode="012000" />
+            <district name="其他" zipcode="12000" />
+        </city>
+        <city name="锡林郭勒盟">
+            <district name="阿巴嘎旗" zipcode="011100" />
+            <district name="东乌珠穆沁旗" zipcode="011100" />
+            <district name="多伦县" zipcode="011100" />
+            <district name="二连浩特市" zipcode="011100" />
+            <district name="苏尼特右旗" zipcode="011100" />
+            <district name="苏尼特左旗" zipcode="011100" />
+            <district name="太仆寺旗" zipcode="011100" />
+            <district name="西乌珠穆沁旗" zipcode="011100" />
+            <district name="锡林浩特市" zipcode="011100" />
+            <district name="镶黄旗" zipcode="011100" />
+            <district name="正蓝旗" zipcode="011100" />
+            <district name="正镶白旗" zipcode="011100" />
+            <district name="其他" zipcode="11100" />
+        </city>
+        <city name="兴安盟">
+            <district name="阿尔山市" zipcode="137400" />
+            <district name="科尔沁右翼前旗" zipcode="137400" />
+            <district name="科尔沁右翼中旗" zipcode="137400" />
+            <district name="突泉县" zipcode="137400" />
+            <district name="乌兰浩特市" zipcode="137400" />
+            <district name="扎赉特旗" zipcode="137400" />
+            <district name="其他" zipcode="137400" />
+        </city>
+    </province>
+    <province name="宁夏回族自治区">
+        <city name="固原市">
+            <district name="泾源县" zipcode="756000" />
+            <district name="隆德县" zipcode="756000" />
+            <district name="彭阳县" zipcode="756000" />
+            <district name="西吉县" zipcode="756000" />
+            <district name="原州区" zipcode="756000" />
+            <district name="其他" zipcode="756000" />
+        </city>
+        <city name="石嘴山市">
+            <district name="大武口区" zipcode="753000" />
+            <district name="惠农区" zipcode="753000" />
+            <district name="平罗县" zipcode="753000" />
+            <district name="其他" zipcode="753000" />
+        </city>
+        <city name="吴忠市">
+            <district name="利通区" zipcode="751100" />
+            <district name="青铜峡市" zipcode="751100" />
+            <district name="同心县" zipcode="751100" />
+            <district name="盐池县" zipcode="751100" />
+            <district name="其他" zipcode="751100" />
+        </city>
+        <city name="银川市">
+            <district name="贺兰县" zipcode="750000" />
+            <district name="金凤区" zipcode="750000" />
+            <district name="灵武市" zipcode="750000" />
+            <district name="西夏区" zipcode="750000" />
+            <district name="兴庆区" zipcode="750000" />
+            <district name="永宁县" zipcode="750000" />
+            <district name="其他" zipcode="750000" />
+        </city>
+        <city name="中卫市">
+            <district name="海原县" zipcode="751700" />
+            <district name="沙坡头区" zipcode="751700" />
+            <district name="中宁县" zipcode="751700" />
+            <district name="其他" zipcode="751700" />
+        </city>
+    </province>
+    <province name="青海省">
+        <city name="果洛藏族自治州">
+            <district name="班玛县" zipcode="814000" />
+            <district name="达日县" zipcode="814000" />
+            <district name="甘德县" zipcode="814000" />
+            <district name="久治县" zipcode="814000" />
+            <district name="玛多县" zipcode="814000" />
+            <district name="玛沁县" zipcode="814000" />
+            <district name="其他" zipcode="814000" />
+        </city>
+        <city name="海北藏族自治州">
+            <district name="刚察县" zipcode="812200" />
+            <district name="海晏县" zipcode="812200" />
+            <district name="门源回族自治县" zipcode="812200" />
+            <district name="祁连县" zipcode="812200" />
+            <district name="其他" zipcode="812200" />
+        </city>
+        <city name="海东地区">
+            <district name="互助土族自治县" zipcode="810600" />
+            <district name="化隆回族自治县" zipcode="810600" />
+            <district name="乐都县" zipcode="810600" />
+            <district name="民和回族土族自治县" zipcode="810600" />
+            <district name="平安县" zipcode="810600" />
+            <district name="循化撒拉族自治县" zipcode="810600" />
+            <district name="其他" zipcode="810600" />
+        </city>
+        <city name="海南藏族自治州">
+            <district name="共和县" zipcode="813000" />
+            <district name="贵德县" zipcode="813000" />
+            <district name="贵南县" zipcode="813000" />
+            <district name="同德县" zipcode="813000" />
+            <district name="兴海县" zipcode="813000" />
+            <district name="其他" zipcode="813000" />
+        </city>
+        <city name="海西蒙古族藏族自治州">
+            <district name="大柴旦" zipcode="817000" />
+            <district name="德令哈市" zipcode="817000" />
+            <district name="都兰县" zipcode="817000" />
+            <district name="格尔木市" zipcode="817000" />
+            <district name="冷湖" zipcode="817000" />
+            <district name="茫崖" zipcode="817000" />
+            <district name="天峻县" zipcode="817000" />
+            <district name="乌兰县" zipcode="817000" />
+            <district name="其他" zipcode="817000" />
+        </city>
+        <city name="黄南藏族自治州">
+            <district name="河南蒙古族自治县" zipcode="811300" />
+            <district name="尖扎县" zipcode="811300" />
+            <district name="同仁县" zipcode="811300" />
+            <district name="泽库县" zipcode="811300" />
+            <district name="其他" zipcode="811300" />
+        </city>
+        <city name="西宁市">
+            <district name="城北区" zipcode="810000" />
+            <district name="城东区" zipcode="810000" />
+            <district name="城西区" zipcode="810000" />
+            <district name="城中区" zipcode="810000" />
+            <district name="大通回族土族自治县" zipcode="810000" />
+            <district name="湟源县" zipcode="810000" />
+            <district name="湟中县" zipcode="810000" />
+            <district name="其他" zipcode="810000" />
+        </city>
+        <city name="玉树藏族自治州">
+            <district name="称多县" zipcode="815000" />
+            <district name="囊谦县" zipcode="815000" />
+            <district name="曲麻莱县" zipcode="815000" />
+            <district name="玉树县" zipcode="815000" />
+            <district name="杂多县" zipcode="815000" />
+            <district name="治多县" zipcode="815000" />
+            <district name="其他" zipcode="815000" />
+        </city>
+    </province>
+    <province name="山东省">
+        <city name="滨州市">
+            <district name="滨城区" zipcode="256600" />
+            <district name="博兴县" zipcode="256600" />
+            <district name="惠民县" zipcode="256600" />
+            <district name="无棣县" zipcode="256600" />
+            <district name="阳信县" zipcode="256600" />
+            <district name="沾化县" zipcode="256600" />
+            <district name="邹平县" zipcode="256600" />
+            <district name="其他" zipcode="256600" />
+        </city>
+        <city name="德州市">
+            <district name="德城区" zipcode="253000" />
+            <district name="乐陵市" zipcode="253000" />
+            <district name="临邑县" zipcode="253000" />
+            <district name="陵县" zipcode="253000" />
+            <district name="宁津县" zipcode="253000" />
+            <district name="平原县" zipcode="253000" />
+            <district name="齐河县" zipcode="253000" />
+            <district name="庆云县" zipcode="253000" />
+            <district name="武城县" zipcode="253000" />
+            <district name="夏津县" zipcode="253000" />
+            <district name="禹城市" zipcode="253000" />
+            <district name="其他" zipcode="253000" />
+        </city>
+        <city name="东营市">
+            <district name="东营区" zipcode="257000" />
+            <district name="广饶县" zipcode="257000" />
+            <district name="河口区" zipcode="257000" />
+            <district name="垦利县" zipcode="257000" />
+            <district name="利津县" zipcode="257000" />
+            <district name="其他" zipcode="257000" />
+        </city>
+        <city name="菏泽市">
+            <district name="曹县" zipcode="274000" />
+            <district name="成武县" zipcode="274000" />
+            <district name="单县" zipcode="274000" />
+            <district name="定陶县" zipcode="274000" />
+            <district name="东明县" zipcode="274000" />
+            <district name="巨野县" zipcode="274000" />
+            <district name="鄄城县" zipcode="274000" />
+            <district name="牡丹区" zipcode="274000" />
+            <district name="郓城县" zipcode="274000" />
+            <district name="其他" zipcode="274000" />
+        </city>
+        <city name="济南市">
+            <district name="长清区" zipcode="250000" />
+            <district name="槐荫区" zipcode="250000" />
+            <district name="济阳县" zipcode="250000" />
+            <district name="历城区" zipcode="250000" />
+            <district name="历下区" zipcode="250000" />
+            <district name="平阴县" zipcode="250000" />
+            <district name="商河县" zipcode="250000" />
+            <district name="市中区" zipcode="250000" />
+            <district name="天桥区" zipcode="250000" />
+            <district name="章丘市" zipcode="250000" />
+            <district name="其他" zipcode="250000" />
+        </city>
+        <city name="济宁市">
+            <district name="嘉祥县" zipcode="272100" />
+            <district name="金乡县" zipcode="272100" />
+            <district name="梁山县" zipcode="272100" />
+            <district name="曲阜市" zipcode="272100" />
+            <district name="任城区" zipcode="272100" />
+            <district name="市中区" zipcode="272100" />
+            <district name="泗水县" zipcode="272100" />
+            <district name="微山县" zipcode="272100" />
+            <district name="汶上县" zipcode="272100" />
+            <district name="兖州市" zipcode="272100" />
+            <district name="鱼台县" zipcode="272100" />
+            <district name="邹城市" zipcode="272100" />
+            <district name="其他" zipcode="272100" />
+        </city>
+        <city name="莱芜市">
+            <district name="钢城区" zipcode="271100" />
+            <district name="莱城区" zipcode="271100" />
+            <district name="其他" zipcode="271100" />
+        </city>
+        <city name="聊城市">
+            <district name="茌平县" zipcode="252000" />
+            <district name="东阿县" zipcode="252000" />
+            <district name="东昌府区" zipcode="252000" />
+            <district name="高唐县" zipcode="252000" />
+            <district name="冠县" zipcode="252000" />
+            <district name="临清市" zipcode="252000" />
+            <district name="莘县" zipcode="252000" />
+            <district name="阳谷县" zipcode="252000" />
+            <district name="其他" zipcode="252000" />
+        </city>
+        <city name="临沂市">
+            <district name="苍山县" zipcode="276000" />
+            <district name="费县" zipcode="276000" />
+            <district name="河东区" zipcode="276000" />
+            <district name="莒南县" zipcode="276000" />
+            <district name="兰山区" zipcode="276000" />
+            <district name="临沭县" zipcode="276000" />
+            <district name="罗庄区" zipcode="276000" />
+            <district name="蒙阴县" zipcode="276000" />
+            <district name="平邑县" zipcode="276000" />
+            <district name="郯城县" zipcode="276000" />
+            <district name="沂南县" zipcode="276000" />
+            <district name="沂水县" zipcode="276000" />
+            <district name="其他" zipcode="276000" />
+        </city>
+        <city name="青岛市">
+            <district name="城阳区" zipcode="266000" />
+            <district name="黄岛区" zipcode="266000" />
+            <district name="即墨市" zipcode="266000" />
+            <district name="胶南市" zipcode="266000" />
+            <district name="胶州市" zipcode="266000" />
+            <district name="莱西市" zipcode="266000" />
+            <district name="崂山区" zipcode="266000" />
+            <district name="李沧区" zipcode="266000" />
+            <district name="平度市" zipcode="266000" />
+            <district name="市北区" zipcode="266000" />
+            <district name="市南区" zipcode="266000" />
+            <district name="四方区" zipcode="266000" />
+            <district name="其他" zipcode="266000" />
+        </city>
+        <city name="日照市">
+            <district name="东港区" zipcode="276800" />
+            <district name="莒县" zipcode="276800" />
+            <district name="岚山区" zipcode="276800" />
+            <district name="五莲县" zipcode="276800" />
+            <district name="其他" zipcode="276800" />
+        </city>
+        <city name="泰安市">
+            <district name="岱岳区" zipcode="271000" />
+            <district name="东平县" zipcode="271000" />
+            <district name="肥城市" zipcode="271000" />
+            <district name="宁阳县" zipcode="271000" />
+            <district name="泰山区" zipcode="271000" />
+            <district name="新泰市" zipcode="271000" />
+            <district name="其他" zipcode="271000" />
+        </city>
+        <city name="威海市">
+            <district name="环翠区" zipcode="265700" />
+            <district name="荣成市" zipcode="265700" />
+            <district name="乳山市" zipcode="265700" />
+            <district name="文登市" zipcode="265700" />
+            <district name="其他" zipcode="265700" />
+        </city>
+        <city name="潍坊市">
+            <district name="安丘市" zipcode="261000" />
+            <district name="昌乐县" zipcode="261000" />
+            <district name="昌邑市" zipcode="261000" />
+            <district name="坊子区" zipcode="261000" />
+            <district name="高密市" zipcode="261000" />
+            <district name="寒亭区" zipcode="261000" />
+            <district name="奎文区" zipcode="261000" />
+            <district name="临朐县" zipcode="261000" />
+            <district name="青州市" zipcode="261000" />
+            <district name="寿光市" zipcode="261000" />
+            <district name="潍城区" zipcode="261000" />
+            <district name="诸城市" zipcode="261000" />
+            <district name="其他" zipcode="261000" />
+        </city>
+        <city name="烟台市">
+            <district name="长岛县" zipcode="264000" />
+            <district name="福山区" zipcode="264000" />
+            <district name="海阳市" zipcode="264000" />
+            <district name="开发区" zipcode="264000" />
+            <district name="莱山区" zipcode="264000" />
+            <district name="莱阳市" zipcode="264000" />
+            <district name="莱州市" zipcode="264000" />
+            <district name="龙口市" zipcode="264000" />
+            <district name="牟平区" zipcode="264000" />
+            <district name="蓬莱市" zipcode="264000" />
+            <district name="栖霞市" zipcode="264000" />
+            <district name="招远市" zipcode="264000" />
+            <district name="芝罘区" zipcode="264000" />
+            <district name="其他" zipcode="264000" />
+        </city>
+        <city name="枣庄市">
+            <district name="山亭区" zipcode="277100" />
+            <district name="市中区" zipcode="277100" />
+            <district name="台儿庄区" zipcode="277100" />
+            <district name="滕州市" zipcode="277100" />
+            <district name="薛城区" zipcode="277100" />
+            <district name="峄城区" zipcode="277100" />
+            <district name="其他" zipcode="277100" />
+        </city>
+        <city name="淄博市">
+            <district name="博山区" zipcode="255000" />
+            <district name="高青县" zipcode="255000" />
+            <district name="桓台县" zipcode="255000" />
+            <district name="临淄区" zipcode="255000" />
+            <district name="沂源县" zipcode="255000" />
+            <district name="张店区" zipcode="255000" />
+            <district name="周村区" zipcode="255000" />
+            <district name="淄川区" zipcode="255000" />
+            <district name="其他" zipcode="255000" />
+        </city>
+    </province>
+    <province name="山西省">
+        <city name="长治市">
+            <district name="长治县" zipcode="046000" />
+            <district name="长子县" zipcode="046000" />
+            <district name="城区" zipcode="046000" />
+            <district name="壶关县" zipcode="046000" />
+            <district name="郊区" zipcode="046000" />
+            <district name="黎城县" zipcode="046000" />
+            <district name="潞城市" zipcode="046000" />
+            <district name="平顺县" zipcode="046000" />
+            <district name="沁县" zipcode="046000" />
+            <district name="沁源县" zipcode="046000" />
+            <district name="屯留县" zipcode="046000" />
+            <district name="武乡县" zipcode="046000" />
+            <district name="襄垣县" zipcode="046000" />
+            <district name="其他" zipcode="46000" />
+        </city>
+        <city name="大同市">
+            <district name="城区" zipcode="037000" />
+            <district name="大同县" zipcode="037000" />
+            <district name="广灵县" zipcode="037000" />
+            <district name="浑源县" zipcode="037000" />
+            <district name="矿区" zipcode="037000" />
+            <district name="灵丘县" zipcode="037000" />
+            <district name="南郊区" zipcode="037000" />
+            <district name="天镇县" zipcode="037000" />
+            <district name="新荣区" zipcode="037000" />
+            <district name="阳高县" zipcode="037000" />
+            <district name="左云县" zipcode="037000" />
+            <district name="其他" zipcode="37000" />
+        </city>
+        <city name="晋城市">
+            <district name="城区" zipcode="048000" />
+            <district name="高平市" zipcode="048000" />
+            <district name="陵川县" zipcode="048000" />
+            <district name="沁水县" zipcode="048000" />
+            <district name="阳城县" zipcode="048000" />
+            <district name="泽州县" zipcode="048000" />
+            <district name="其他" zipcode="48000" />
+        </city>
+        <city name="晋中市">
+            <district name="和顺县" zipcode="030600" />
+            <district name="介休市" zipcode="030600" />
+            <district name="灵石县" zipcode="030600" />
+            <district name="平遥县" zipcode="030600" />
+            <district name="祁县" zipcode="030600" />
+            <district name="寿阳县" zipcode="030600" />
+            <district name="太谷县" zipcode="030600" />
+            <district name="昔阳县" zipcode="030600" />
+            <district name="榆次区" zipcode="030600" />
+            <district name="榆社县" zipcode="030600" />
+            <district name="左权县" zipcode="030600" />
+            <district name="其他" zipcode="30600" />
+        </city>
+        <city name="临汾市">
+            <district name="安泽县" zipcode="041000" />
+            <district name="大宁县" zipcode="041000" />
+            <district name="汾西县" zipcode="041000" />
+            <district name="浮山县" zipcode="041000" />
+            <district name="古县" zipcode="041000" />
+            <district name="洪洞县" zipcode="041000" />
+            <district name="侯马市" zipcode="041000" />
+            <district name="霍州市" zipcode="041000" />
+            <district name="吉县" zipcode="041000" />
+            <district name="蒲县" zipcode="041000" />
+            <district name="曲沃县" zipcode="041000" />
+            <district name="隰县" zipcode="041000" />
+            <district name="乡宁县" zipcode="041000" />
+            <district name="襄汾县" zipcode="041000" />
+            <district name="尧都区" zipcode="041000" />
+            <district name="翼城县" zipcode="041000" />
+            <district name="永和县" zipcode="041000" />
+            <district name="其他" zipcode="41000" />
+        </city>
+        <city name="吕梁市">
+            <district name="方山县" zipcode="030500" />
+            <district name="汾阳市" zipcode="030500" />
+            <district name="交城县" zipcode="030500" />
+            <district name="交口县" zipcode="030500" />
+            <district name="岚县" zipcode="030500" />
+            <district name="离石区" zipcode="030500" />
+            <district name="临县" zipcode="030500" />
+            <district name="柳林县" zipcode="030500" />
+            <district name="石楼县" zipcode="030500" />
+            <district name="文水县" zipcode="030500" />
+            <district name="孝义市" zipcode="030500" />
+            <district name="兴县" zipcode="030500" />
+            <district name="中阳县" zipcode="030500" />
+            <district name="其他" zipcode="30500" />
+        </city>
+        <city name="朔州市">
+            <district name="怀仁县" zipcode="036000" />
+            <district name="平鲁区" zipcode="036000" />
+            <district name="山阴县" zipcode="036000" />
+            <district name="朔城区" zipcode="036000" />
+            <district name="应县" zipcode="036000" />
+            <district name="右玉县" zipcode="036000" />
+            <district name="其他" zipcode="36000" />
+        </city>
+        <city name="太原市">
+            <district name="古交市" zipcode="030000" />
+            <district name="尖草坪区" zipcode="030000" />
+            <district name="晋源区" zipcode="030000" />
+            <district name="娄烦县" zipcode="030000" />
+            <district name="清徐县" zipcode="030000" />
+            <district name="万柏林区" zipcode="030000" />
+            <district name="小店区" zipcode="030000" />
+            <district name="杏花岭区" zipcode="030000" />
+            <district name="阳曲县" zipcode="030000" />
+            <district name="迎泽区" zipcode="030000" />
+            <district name="其他" zipcode="30000" />
+        </city>
+        <city name="忻州市">
+            <district name="保德县" zipcode="034000" />
+            <district name="代县" zipcode="034000" />
+            <district name="定襄县" zipcode="034000" />
+            <district name="繁峙县" zipcode="034000" />
+            <district name="河曲县" zipcode="034000" />
+            <district name="静乐县" zipcode="034000" />
+            <district name="岢岚县" zipcode="034000" />
+            <district name="宁武县" zipcode="034000" />
+            <district name="偏关县" zipcode="034000" />
+            <district name="神池县" zipcode="034000" />
+            <district name="五台县" zipcode="034000" />
+            <district name="五寨县" zipcode="034000" />
+            <district name="忻府区" zipcode="034000" />
+            <district name="原平市" zipcode="034000" />
+            <district name="其他" zipcode="34000" />
+        </city>
+        <city name="阳泉市">
+            <district name="城区" zipcode="045000" />
+            <district name="郊区" zipcode="045000" />
+            <district name="矿区" zipcode="045000" />
+            <district name="平定县" zipcode="045000" />
+            <district name="盂县" zipcode="045000" />
+            <district name="其他" zipcode="45000" />
+        </city>
+        <city name="运城市">
+            <district name="河津市" zipcode="044000" />
+            <district name="稷山县" zipcode="044000" />
+            <district name="绛县" zipcode="044000" />
+            <district name="临猗县" zipcode="044000" />
+            <district name="平陆县" zipcode="044000" />
+            <district name="芮城县" zipcode="044000" />
+            <district name="万荣县" zipcode="044000" />
+            <district name="闻喜县" zipcode="044000" />
+            <district name="夏县" zipcode="044000" />
+            <district name="新绛县" zipcode="044000" />
+            <district name="盐湖区" zipcode="044000" />
+            <district name="永济市" zipcode="044000" />
+            <district name="垣曲县" zipcode="044000" />
+            <district name="其他" zipcode="44000" />
+        </city>
+    </province>
+    <province name="陕西省">
+        <city name="安康市">
+            <district name="白河县" zipcode="725000" />
+            <district name="汉滨区" zipcode="725000" />
+            <district name="汉阴县" zipcode="725000" />
+            <district name="岚皋县" zipcode="725000" />
+            <district name="宁陕县" zipcode="725000" />
+            <district name="平利县" zipcode="725000" />
+            <district name="石泉县" zipcode="725000" />
+            <district name="旬阳县" zipcode="725000" />
+            <district name="镇坪县" zipcode="725000" />
+            <district name="紫阳县" zipcode="725000" />
+            <district name="其他" zipcode="725000" />
+        </city>
+        <city name="宝鸡市">
+            <district name="陈仓区" zipcode="721000" />
+            <district name="凤县" zipcode="721000" />
+            <district name="凤翔县" zipcode="721000" />
+            <district name="扶风县" zipcode="721000" />
+            <district name="金台区" zipcode="721000" />
+            <district name="麟游县" zipcode="721000" />
+            <district name="陇县" zipcode="721000" />
+            <district name="眉县" zipcode="721000" />
+            <district name="岐山县" zipcode="721000" />
+            <district name="千阳县" zipcode="721000" />
+            <district name="太白县" zipcode="721000" />
+            <district name="渭滨区" zipcode="721000" />
+            <district name="其他" zipcode="721000" />
+        </city>
+        <city name="汉中市">
+            <district name="城固县" zipcode="723000" />
+            <district name="佛坪县" zipcode="723000" />
+            <district name="汉台区" zipcode="723000" />
+            <district name="留坝县" zipcode="723000" />
+            <district name="略阳县" zipcode="723000" />
+            <district name="勉县" zipcode="723000" />
+            <district name="南郑县" zipcode="723000" />
+            <district name="宁强县" zipcode="723000" />
+            <district name="西乡县" zipcode="723000" />
+            <district name="洋县" zipcode="723000" />
+            <district name="镇巴县" zipcode="723000" />
+            <district name="其他" zipcode="723000" />
+        </city>
+        <city name="商洛市">
+            <district name="丹凤县" zipcode="726000" />
+            <district name="洛南县" zipcode="726000" />
+            <district name="山阳县" zipcode="726000" />
+            <district name="商南县" zipcode="726000" />
+            <district name="商州区" zipcode="726000" />
+            <district name="镇安县" zipcode="726000" />
+            <district name="柞水县" zipcode="726000" />
+            <district name="其他" zipcode="726000" />
+        </city>
+        <city name="铜川市">
+            <district name="王益区" zipcode="727000" />
+            <district name="耀州区" zipcode="727000" />
+            <district name="宜君县" zipcode="727000" />
+            <district name="印台区" zipcode="727000" />
+            <district name="其他" zipcode="727000" />
+        </city>
+        <city name="渭南市">
+            <district name="白水县" zipcode="714000" />
+            <district name="澄城县" zipcode="714000" />
+            <district name="大荔县" zipcode="714000" />
+            <district name="富平县" zipcode="714000" />
+            <district name="韩城市" zipcode="714000" />
+            <district name="合阳县" zipcode="714000" />
+            <district name="华县" zipcode="714000" />
+            <district name="华阴市" zipcode="714000" />
+            <district name="临渭区" zipcode="714000" />
+            <district name="蒲城县" zipcode="714000" />
+            <district name="潼关县" zipcode="714000" />
+            <district name="其他" zipcode="714000" />
+        </city>
+        <city name="西安市">
+            <district name="灞桥区" zipcode="710000" />
+            <district name="碑林区" zipcode="710000" />
+            <district name="长安区" zipcode="710000" />
+            <district name="高陵县" zipcode="710000" />
+            <district name="户县" zipcode="710000" />
+            <district name="蓝田县" zipcode="710000" />
+            <district name="莲湖区" zipcode="710000" />
+            <district name="临潼区" zipcode="710000" />
+            <district name="未央区" zipcode="710000" />
+            <district name="新城区" zipcode="710000" />
+            <district name="阎良区" zipcode="710000" />
+            <district name="雁塔区" zipcode="710000" />
+            <district name="周至县" zipcode="710000" />
+            <district name="其他" zipcode="710000" />
+        </city>
+        <city name="咸阳市">
+            <district name="彬县" zipcode="712000" />
+            <district name="长武县" zipcode="712000" />
+            <district name="淳化县" zipcode="712000" />
+            <district name="泾阳县" zipcode="712000" />
+            <district name="礼泉县" zipcode="712000" />
+            <district name="乾县" zipcode="712000" />
+            <district name="秦都区" zipcode="712000" />
+            <district name="三原县" zipcode="712000" />
+            <district name="渭城区" zipcode="712000" />
+            <district name="武功县" zipcode="712000" />
+            <district name="兴平市" zipcode="712000" />
+            <district name="旬邑县" zipcode="712000" />
+            <district name="杨凌区" zipcode="712000" />
+            <district name="永寿县" zipcode="712000" />
+            <district name="其他" zipcode="712000" />
+        </city>
+        <city name="延安市">
+            <district name="安塞县" zipcode="716000" />
+            <district name="宝塔区" zipcode="716000" />
+            <district name="富县" zipcode="716000" />
+            <district name="甘泉县" zipcode="716000" />
+            <district name="黄陵县" zipcode="716000" />
+            <district name="黄龙县" zipcode="716000" />
+            <district name="洛川县" zipcode="716000" />
+            <district name="延长县" zipcode="716000" />
+            <district name="延川县" zipcode="716000" />
+            <district name="宜川县" zipcode="716000" />
+            <district name="志丹县" zipcode="716000" />
+            <district name="子长县" zipcode="716000" />
+            <district name="其他" zipcode="716000" />
+        </city>
+        <city name="榆林市">
+            <district name="定边县" zipcode="719000" />
+            <district name="府谷县" zipcode="719000" />
+            <district name="横山县" zipcode="719000" />
+            <district name="佳县" zipcode="719000" />
+            <district name="靖边县" zipcode="719000" />
+            <district name="米脂县" zipcode="719000" />
+            <district name="清涧县" zipcode="719000" />
+            <district name="神木县" zipcode="719000" />
+            <district name="绥德县" zipcode="719000" />
+            <district name="吴堡县" zipcode="719000" />
+            <district name="榆阳区" zipcode="719000" />
+            <district name="子洲县" zipcode="719000" />
+            <district name="其他" zipcode="719000" />
+        </city>
+    </province>
+    <province name="上海市">
+        <city name="上海市">
+            <district name="宝山区" zipcode="200000" />
+            <district name="长宁区" zipcode="200000" />
+            <district name="崇明县" zipcode="200000" />
+            <district name="奉贤区" zipcode="200000" />
+            <district name="虹口区" zipcode="200000" />
+            <district name="黄浦区" zipcode="200000" />
+            <district name="嘉定区" zipcode="200000" />
+            <district name="金山区" zipcode="200000" />
+            <district name="静安区" zipcode="200000" />
+            <district name="卢湾区" zipcode="200000" />
+            <district name="闵行区" zipcode="200000" />
+            <district name="南汇区" zipcode="200000" />
+            <district name="浦东新区" zipcode="200000" />
+            <district name="普陀区" zipcode="200000" />
+            <district name="青浦区" zipcode="200000" />
+            <district name="松江区" zipcode="200000" />
+            <district name="徐汇区" zipcode="200000" />
+            <district name="杨浦区" zipcode="200000" />
+            <district name="闸北区" zipcode="200000" />
+            <district name="其他" zipcode="200000" />
+        </city>
+    </province>
+    <province name="四川省">
+        <city name="阿坝藏族羌族自治州">
+            <district name="阿坝县" zipcode="624000" />
+            <district name="黑水县" zipcode="624000" />
+            <district name="红原县" zipcode="624000" />
+            <district name="金川县" zipcode="624000" />
+            <district name="九寨沟县" zipcode="624000" />
+            <district name="理县" zipcode="624000" />
+            <district name="马尔康县" zipcode="624000" />
+            <district name="茂县" zipcode="624000" />
+            <district name="壤塘县" zipcode="624000" />
+            <district name="若尔盖县" zipcode="624000" />
+            <district name="松潘县" zipcode="624000" />
+            <district name="汶川县" zipcode="624000" />
+            <district name="小金县" zipcode="624000" />
+            <district name="其他" zipcode="624000" />
+        </city>
+        <city name="巴中市">
+            <district name="巴州区" zipcode="636600" />
+            <district name="南江县" zipcode="636600" />
+            <district name="平昌县" zipcode="636600" />
+            <district name="通江县" zipcode="636600" />
+            <district name="其他" zipcode="636600" />
+        </city>
+        <city name="成都市">
+            <district name="成华区" zipcode="610000" />
+            <district name="崇州市" zipcode="610000" />
+            <district name="大邑县" zipcode="610000" />
+            <district name="都江堰市" zipcode="610000" />
+            <district name="金牛区" zipcode="610000" />
+            <district name="金堂县" zipcode="610000" />
+            <district name="锦江区" zipcode="610000" />
+            <district name="龙泉驿区" zipcode="610000" />
+            <district name="彭州市" zipcode="610000" />
+            <district name="郫县" zipcode="610000" />
+            <district name="蒲江县" zipcode="610000" />
+            <district name="青白江区" zipcode="610000" />
+            <district name="青羊区" zipcode="610000" />
+            <district name="邛崃市" zipcode="610000" />
+            <district name="双流县" zipcode="610000" />
+            <district name="温江区" zipcode="610000" />
+            <district name="武侯区" zipcode="610000" />
+            <district name="新都区" zipcode="610000" />
+            <district name="新津县" zipcode="610000" />
+            <district name="其他" zipcode="610000" />
+        </city>
+        <city name="达州市">
+            <district name="达县" zipcode="635000" />
+            <district name="大竹县" zipcode="635000" />
+            <district name="开江县" zipcode="635000" />
+            <district name="渠县" zipcode="635000" />
+            <district name="通川区" zipcode="635000" />
+            <district name="万源市" zipcode="635000" />
+            <district name="宣汉县" zipcode="635000" />
+            <district name="其他" zipcode="635000" />
+        </city>
+        <city name="德阳市">
+            <district name="广汉市" zipcode="618000" />
+            <district name="旌阳区" zipcode="618000" />
+            <district name="罗江县" zipcode="618000" />
+            <district name="绵竹市" zipcode="618000" />
+            <district name="什邡市" zipcode="618000" />
+            <district name="中江县" zipcode="618000" />
+            <district name="其他" zipcode="618000" />
+        </city>
+        <city name="甘孜藏族自治州">
+            <district name="巴塘县" zipcode="626000" />
+            <district name="白玉县" zipcode="626000" />
+            <district name="丹巴县" zipcode="626000" />
+            <district name="道孚县" zipcode="626000" />
+            <district name="稻城县" zipcode="626000" />
+            <district name="得荣县" zipcode="626000" />
+            <district name="德格县" zipcode="626000" />
+            <district name="甘孜县" zipcode="626000" />
+            <district name="九龙县" zipcode="626000" />
+            <district name="康定县" zipcode="626000" />
+            <district name="理塘县" zipcode="626000" />
+            <district name="泸定县" zipcode="626000" />
+            <district name="炉霍县" zipcode="626000" />
+            <district name="色达县" zipcode="626000" />
+            <district name="石渠县" zipcode="626000" />
+            <district name="乡城县" zipcode="626000" />
+            <district name="新龙县" zipcode="626000" />
+            <district name="雅江县" zipcode="626000" />
+            <district name="其他" zipcode="626000" />
+        </city>
+        <city name="广安市">
+            <district name="广安区" zipcode="638500" />
+            <district name="华蓥市" zipcode="638500" />
+            <district name="邻水县" zipcode="638500" />
+            <district name="武胜县" zipcode="638500" />
+            <district name="岳池县" zipcode="638500" />
+            <district name="其他" zipcode="638500" />
+        </city>
+        <city name="广元市">
+            <district name="苍溪县" zipcode="628000" />
+            <district name="朝天区" zipcode="628000" />
+            <district name="剑阁县" zipcode="628000" />
+            <district name="利州区" zipcode="628000" />
+            <district name="青川县" zipcode="628000" />
+            <district name="旺苍县" zipcode="628000" />
+            <district name="元坝区" zipcode="628000" />
+            <district name="其他" zipcode="628000" />
+        </city>
+        <city name="乐山市">
+            <district name="峨边彝族自治县" zipcode="614000" />
+            <district name="峨眉山市" zipcode="614000" />
+            <district name="夹江县" zipcode="614000" />
+            <district name="犍为县" zipcode="614000" />
+            <district name="金口河区" zipcode="614000" />
+            <district name="井研县" zipcode="614000" />
+            <district name="马边彝族自治县" zipcode="614000" />
+            <district name="沐川县" zipcode="614000" />
+            <district name="沙湾区" zipcode="614000" />
+            <district name="市中区" zipcode="614000" />
+            <district name="五通桥区" zipcode="614000" />
+            <district name="其他" zipcode="614000" />
+        </city>
+        <city name="凉山彝族自治州">
+            <district name="布拖县" zipcode="615000" />
+            <district name="德昌县" zipcode="615000" />
+            <district name="甘洛县" zipcode="615000" />
+            <district name="会东县" zipcode="615000" />
+            <district name="会理县" zipcode="615000" />
+            <district name="金阳县" zipcode="615000" />
+            <district name="雷波县" zipcode="615000" />
+            <district name="美姑县" zipcode="615000" />
+            <district name="冕宁县" zipcode="615000" />
+            <district name="木里藏族自治县" zipcode="615000" />
+            <district name="宁南县" zipcode="615000" />
+            <district name="普格县" zipcode="615000" />
+            <district name="西昌市" zipcode="615000" />
+            <district name="喜德县" zipcode="615000" />
+            <district name="盐源县" zipcode="615000" />
+            <district name="越西县" zipcode="615000" />
+            <district name="昭觉县" zipcode="615000" />
+            <district name="其他" zipcode="615000" />
+        </city>
+        <city name="泸州市">
+            <district name="古蔺县" zipcode="646000" />
+            <district name="合江县" zipcode="646000" />
+            <district name="江阳区" zipcode="646000" />
+            <district name="龙马潭区" zipcode="646000" />
+            <district name="泸县" zipcode="646000" />
+            <district name="纳溪区" zipcode="646000" />
+            <district name="叙永县" zipcode="646000" />
+            <district name="其他" zipcode="646000" />
+        </city>
+        <city name="眉山市">
+            <district name="丹棱县" zipcode="620000" />
+            <district name="东坡区" zipcode="620000" />
+            <district name="洪雅县" zipcode="620000" />
+            <district name="彭山县" zipcode="620000" />
+            <district name="青神县" zipcode="620000" />
+            <district name="仁寿县" zipcode="620000" />
+            <district name="其他" zipcode="620000" />
+        </city>
+        <city name="绵阳市">
+            <district name="安县" zipcode="621000" />
+            <district name="北川羌族自治县" zipcode="621000" />
+            <district name="涪城区" zipcode="621000" />
+            <district name="江油市" zipcode="621000" />
+            <district name="平武县" zipcode="621000" />
+            <district name="三台县" zipcode="621000" />
+            <district name="盐亭县" zipcode="621000" />
+            <district name="游仙区" zipcode="621000" />
+            <district name="梓潼县" zipcode="621000" />
+            <district name="其他" zipcode="621000" />
+        </city>
+        <city name="内江市">
+            <district name="东兴区" zipcode="641000" />
+            <district name="隆昌县" zipcode="641000" />
+            <district name="市中区" zipcode="641000" />
+            <district name="威远县" zipcode="641000" />
+            <district name="资中县" zipcode="641000" />
+            <district name="其他" zipcode="641000" />
+        </city>
+        <city name="南充市">
+            <district name="高坪区" zipcode="637000" />
+            <district name="嘉陵区" zipcode="637000" />
+            <district name="阆中市" zipcode="637000" />
+            <district name="南部县" zipcode="637000" />
+            <district name="蓬安县" zipcode="637000" />
+            <district name="顺庆区" zipcode="637000" />
+            <district name="西充县" zipcode="637000" />
+            <district name="仪陇县" zipcode="637000" />
+            <district name="营山县" zipcode="637000" />
+            <district name="其他" zipcode="637000" />
+        </city>
+        <city name="攀枝花市">
+            <district name="东区" zipcode="617000" />
+            <district name="米易县" zipcode="617000" />
+            <district name="仁和区" zipcode="617000" />
+            <district name="西区" zipcode="617000" />
+            <district name="盐边县" zipcode="617000" />
+            <district name="其他" zipcode="617000" />
+        </city>
+        <city name="遂宁市">
+            <district name="安居区" zipcode="629000" />
+            <district name="船山区" zipcode="629000" />
+            <district name="大英县" zipcode="629000" />
+            <district name="蓬溪县" zipcode="629000" />
+            <district name="射洪县" zipcode="629000" />
+            <district name="其他" zipcode="629000" />
+        </city>
+        <city name="雅安市">
+            <district name="宝兴县" zipcode="625000" />
+            <district name="汉源县" zipcode="625000" />
+            <district name="芦山县" zipcode="625000" />
+            <district name="名山县" zipcode="625000" />
+            <district name="石棉县" zipcode="625000" />
+            <district name="天全县" zipcode="625000" />
+            <district name="荥经县" zipcode="625000" />
+            <district name="雨城区" zipcode="625000" />
+            <district name="其他" zipcode="625000" />
+        </city>
+        <city name="宜宾市">
+            <district name="长宁县" zipcode="644000" />
+            <district name="翠屏区" zipcode="644000" />
+            <district name="高县" zipcode="644000" />
+            <district name="珙县" zipcode="644000" />
+            <district name="江安县" zipcode="644000" />
+            <district name="筠连县" zipcode="644000" />
+            <district name="南溪县" zipcode="644000" />
+            <district name="屏山县" zipcode="644000" />
+            <district name="兴文县" zipcode="644000" />
+            <district name="宜宾县" zipcode="644000" />
+            <district name="其他" zipcode="644000" />
+        </city>
+        <city name="资阳市">
+            <district name="安岳县" zipcode="641300" />
+            <district name="简阳市" zipcode="641300" />
+            <district name="乐至县" zipcode="641300" />
+            <district name="雁江区" zipcode="641300" />
+            <district name="其他" zipcode="641300" />
+        </city>
+        <city name="自贡市">
+            <district name="大安区" zipcode="643000" />
+            <district name="富顺县" zipcode="643000" />
+            <district name="贡井区" zipcode="643000" />
+            <district name="荣县" zipcode="643000" />
+            <district name="沿滩区" zipcode="643000" />
+            <district name="自流井区" zipcode="643000" />
+            <district name="其他" zipcode="643000" />
+        </city>
+    </province>
+    <province name="天津市">
+        <city name="天津市">
+            <district name="宝坻区" zipcode="100000" />
+            <district name="北辰区" zipcode="100000" />
+            <district name="大港区" zipcode="100000" />
+            <district name="东丽区" zipcode="100000" />
+            <district name="汉沽区" zipcode="100000" />
+            <district name="和平区" zipcode="100000" />
+            <district name="河北区" zipcode="100000" />
+            <district name="河东区" zipcode="100000" />
+            <district name="河西区" zipcode="100000" />
+            <district name="红桥区" zipcode="100000" />
+            <district name="蓟县" zipcode="100000" />
+            <district name="津南区" zipcode="100000" />
+            <district name="静海县" zipcode="100000" />
+            <district name="南开区" zipcode="100000" />
+            <district name="宁河县" zipcode="100000" />
+            <district name="塘沽区" zipcode="100000" />
+            <district name="武清区" zipcode="100000" />
+            <district name="西青区" zipcode="100000" />
+            <district name="其他" zipcode="100000" />
+        </city>
+    </province>
+    <province name="西藏自治区">
+        <city name="阿里地区">
+            <district name="措勤县" zipcode="859000" />
+            <district name="噶尔县" zipcode="859000" />
+            <district name="改则县" zipcode="859000" />
+            <district name="革吉县" zipcode="859000" />
+            <district name="普兰县" zipcode="859000" />
+            <district name="日土县" zipcode="859000" />
+            <district name="札达县" zipcode="859000" />
+            <district name="其他" zipcode="859000" />
+        </city>
+        <city name="昌都地区">
+            <district name="八宿县" zipcode="854000" />
+            <district name="边坝县" zipcode="854000" />
+            <district name="察雅县" zipcode="854000" />
+            <district name="昌都县" zipcode="854000" />
+            <district name="丁青县" zipcode="854000" />
+            <district name="贡觉县" zipcode="854000" />
+            <district name="江达县" zipcode="854000" />
+            <district name="类乌齐县" zipcode="854000" />
+            <district name="洛隆县" zipcode="854000" />
+            <district name="芒康县" zipcode="854000" />
+            <district name="左贡县" zipcode="854000" />
+            <district name="其他" zipcode="854000" />
+        </city>
+        <city name="拉萨市">
+            <district name="达孜县" zipcode="850000" />
+            <district name="当雄县" zipcode="850000" />
+            <district name="堆龙德庆县" zipcode="850000" />
+            <district name="林周县" zipcode="850000" />
+            <district name="墨竹工卡县" zipcode="850000" />
+            <district name="尼木县" zipcode="850000" />
+            <district name="曲水县" zipcode="850000" />
+            <district name="其他" zipcode="850000" />
+        </city>
+        <city name="林芝地区">
+            <district name="波密县" zipcode="860100" />
+            <district name="察隅县" zipcode="860100" />
+            <district name="工布江达县" zipcode="860100" />
+            <district name="朗县" zipcode="860100" />
+            <district name="林芝县" zipcode="860100" />
+            <district name="米林县" zipcode="860100" />
+            <district name="墨脱县" zipcode="860100" />
+            <district name="其他" zipcode="860100" />
+        </city>
+        <city name="那曲地区">
+            <district name="安多县" zipcode="852000" />
+            <district name="巴青县" zipcode="852000" />
+            <district name="班戈县" zipcode="852000" />
+            <district name="比如县" zipcode="852000" />
+            <district name="嘉黎县" zipcode="852000" />
+            <district name="那曲县" zipcode="852000" />
+            <district name="尼玛县" zipcode="852000" />
+            <district name="聂荣县" zipcode="852000" />
+            <district name="申扎县" zipcode="852000" />
+            <district name="索县" zipcode="852000" />
+            <district name="其他" zipcode="852000" />
+        </city>
+        <city name="日喀则地区">
+            <district name="昂仁县" zipcode="857000" />
+            <district name="白朗县" zipcode="857000" />
+            <district name="定结县" zipcode="857000" />
+            <district name="定日县" zipcode="857000" />
+            <district name="岗巴县" zipcode="857000" />
+            <district name="吉隆县" zipcode="857000" />
+            <district name="江孜县" zipcode="857000" />
+            <district name="康马县" zipcode="857000" />
+            <district name="拉孜县" zipcode="857000" />
+            <district name="南木林县" zipcode="857000" />
+            <district name="聂拉木县" zipcode="857000" />
+            <district name="仁布县" zipcode="857000" />
+            <district name="日喀则市" zipcode="857000" />
+            <district name="萨嘎县" zipcode="857000" />
+            <district name="萨迦县" zipcode="857000" />
+            <district name="谢通门县" zipcode="857000" />
+            <district name="亚东县" zipcode="857000" />
+            <district name="仲巴县" zipcode="857000" />
+            <district name="其他" zipcode="857000" />
+        </city>
+        <city name="山南地区">
+            <district name="措美县" zipcode="856000" />
+            <district name="错那县" zipcode="856000" />
+            <district name="贡嘎县" zipcode="856000" />
+            <district name="加查县" zipcode="856000" />
+            <district name="浪卡子县" zipcode="856000" />
+            <district name="隆子县" zipcode="856000" />
+            <district name="洛扎县" zipcode="856000" />
+            <district name="乃东县" zipcode="856000" />
+            <district name="琼结县" zipcode="856000" />
+            <district name="曲松县" zipcode="856000" />
+            <district name="桑日县" zipcode="856000" />
+            <district name="扎囊县" zipcode="856000" />
+            <district name="其他" zipcode="856000" />
+        </city>
+    </province>
+    <province name="新疆维吾尔自治区">
+        <city name="阿克苏地区">
+            <district name="阿克苏市" zipcode="843000" />
+            <district name="阿瓦提县" zipcode="843000" />
+            <district name="拜城县" zipcode="843000" />
+            <district name="柯坪县" zipcode="843000" />
+            <district name="库车县" zipcode="843000" />
+            <district name="沙雅县" zipcode="843000" />
+            <district name="温宿县" zipcode="843000" />
+            <district name="乌什县" zipcode="843000" />
+            <district name="新和县" zipcode="843000" />
+            <district name="其他" zipcode="843000" />
+        </city>
+        <city name="阿勒泰地区">
+            <district name="阿勒泰市" zipcode="836500" />
+            <district name="北屯镇" zipcode="836500" />
+            <district name="布尔津县" zipcode="836500" />
+            <district name="福海县" zipcode="836500" />
+            <district name="富蕴县" zipcode="836500" />
+            <district name="哈巴河县" zipcode="836500" />
+            <district name="吉木乃县" zipcode="836500" />
+            <district name="青河县" zipcode="836500" />
+            <district name="其他" zipcode="836500" />
+        </city>
+        <city name="巴音郭楞蒙古自治州">
+            <district name="博湖县" zipcode="841000" />
+            <district name="和静县" zipcode="841000" />
+            <district name="和硕县" zipcode="841000" />
+            <district name="库尔勒市" zipcode="841000" />
+            <district name="轮台县" zipcode="841000" />
+            <district name="且末县" zipcode="841000" />
+            <district name="若羌县" zipcode="841000" />
+            <district name="尉犁县" zipcode="841000" />
+            <district name="焉耆回族自治县" zipcode="841000" />
+            <district name="其他" zipcode="841000" />
+        </city>
+        <city name="博尔塔拉蒙古自治州">
+            <district name="博乐市" zipcode="833400" />
+            <district name="精河县" zipcode="833400" />
+            <district name="温泉县" zipcode="833400" />
+            <district name="其他" zipcode="833400" />
+        </city>
+        <city name="昌吉回族自治州">
+            <district name="昌吉市" zipcode="831100" />
+            <district name="阜康市" zipcode="831100" />
+            <district name="呼图壁县" zipcode="831100" />
+            <district name="吉木萨尔县" zipcode="831100" />
+            <district name="玛纳斯县" zipcode="831100" />
+            <district name="木垒哈萨克自治县" zipcode="831100" />
+            <district name="奇台县" zipcode="831100" />
+            <district name="五家渠市" zipcode="831100" />
+            <district name="其他" zipcode="831100" />
+        </city>
+        <city name="哈密地区">
+            <district name="巴里坤哈萨克自治县" zipcode="839000" />
+            <district name="哈密市" zipcode="839000" />
+            <district name="伊吾县" zipcode="839000" />
+            <district name="其他" zipcode="839000" />
+        </city>
+        <city name="和田地区">
+            <district name="策勒县" zipcode="848000" />
+            <district name="和田市" zipcode="848000" />
+            <district name="和田县" zipcode="848000" />
+            <district name="洛浦县" zipcode="848000" />
+            <district name="民丰县" zipcode="848000" />
+            <district name="墨玉县" zipcode="848000" />
+            <district name="皮山县" zipcode="848000" />
+            <district name="于田县" zipcode="848000" />
+            <district name="其他" zipcode="848000" />
+        </city>
+        <city name="喀什地区">
+            <district name="巴楚县" zipcode="844000" />
+            <district name="伽师县" zipcode="844000" />
+            <district name="喀什市" zipcode="844000" />
+            <district name="麦盖提县" zipcode="844000" />
+            <district name="莎车县" zipcode="844000" />
+            <district name="疏附县" zipcode="844000" />
+            <district name="疏勒县" zipcode="844000" />
+            <district name="塔什库尔干塔吉克自治县" zipcode="844000" />
+            <district name="叶城县" zipcode="844000" />
+            <district name="英吉沙县" zipcode="844000" />
+            <district name="岳普湖县" zipcode="844000" />
+            <district name="泽普县" zipcode="844000" />
+            <district name="其他" zipcode="844000" />
+        </city>
+        <city name="克拉玛依市">
+            <district name="白碱滩区" zipcode="834000" />
+            <district name="独山子区" zipcode="834000" />
+            <district name="克拉玛依区" zipcode="834000" />
+            <district name="乌尔禾区" zipcode="834000" />
+            <district name="其他" zipcode="834000" />
+        </city>
+        <city name="克孜勒苏柯尔克孜自治州">
+            <district name="阿合奇县" zipcode="845350" />
+            <district name="阿克陶县" zipcode="845350" />
+            <district name="阿图什市" zipcode="845350" />
+            <district name="乌恰县" zipcode="845350" />
+            <district name="其他" zipcode="845350" />
+        </city>
+        <city name="塔城地区">
+            <district name="额敏县" zipcode="834700" />
+            <district name="和布克赛尔蒙古自治县" zipcode="834700" />
+            <district name="沙湾县" zipcode="834700" />
+            <district name="塔城市" zipcode="834700" />
+            <district name="托里县" zipcode="834700" />
+            <district name="乌苏市" zipcode="834700" />
+            <district name="裕民县" zipcode="834700" />
+            <district name="其他" zipcode="834700" />
+        </city>
+        <city name="吐鲁番地区">
+            <district name="鄯善县" zipcode="838000" />
+            <district name="吐鲁番市" zipcode="838000" />
+            <district name="托克逊县" zipcode="838000" />
+            <district name="其他" zipcode="838000" />
+        </city>
+        <city name="乌鲁木齐市">
+            <district name="达坂城区" zipcode="830000" />
+            <district name="米东区" zipcode="830000" />
+            <district name="沙依巴克区" zipcode="830000" />
+            <district name="水磨沟区" zipcode="830000" />
+            <district name="天山区" zipcode="830000" />
+            <district name="头屯河区" zipcode="830000" />
+            <district name="乌鲁木齐县" zipcode="830000" />
+            <district name="新市区" zipcode="830000" />
+            <district name="其他" zipcode="830000" />
+        </city>
+        <city name="伊犁哈萨克自治州">
+            <district name="察布查尔锡伯自治县" zipcode="833200" />
+            <district name="巩留县" zipcode="833200" />
+            <district name="霍城县" zipcode="833200" />
+            <district name="奎屯市" zipcode="833200" />
+            <district name="尼勒克县" zipcode="833200" />
+            <district name="特克斯县" zipcode="833200" />
+            <district name="新源县" zipcode="833200" />
+            <district name="伊宁市" zipcode="833200" />
+            <district name="伊宁县" zipcode="833200" />
+            <district name="昭苏县" zipcode="833200" />
+            <district name="其他" zipcode="833200" />
+        </city>
+        <city name="自治区直辖">
+            <district name="石河子市" zipcode="830000" />
+            <district name="阿拉尔市" zipcode="830000" />
+            <district name="图木舒克市" zipcode="830000" />
+            <district name="五家渠区" zipcode="830000" />
+            <district name="北屯市" zipcode="830000" />
+            <district name="铁门关市" zipcode="830000" />
+            <district name="双河市" zipcode="830000" />
+            <district name="可克达拉市" zipcode="830000" />
+            <district name="昆玉市" zipcode="830000" />
+            <district name="其他" zipcode="830000" />
+        </city>
+    </province>
+    <province name="云南省">
+        <city name="保山市">
+            <district name="伊宁市" zipcode="678000" />
+            <district name="奎屯市" zipcode="678000" />
+            <district name="霍尔果斯市" zipcode="678000" />
+            <district name="伊宁县" zipcode="678000" />
+            <district name="察布查尔县" zipcode="678000" />
+            <district name="霍城县" zipcode="678000" />
+            <district name="巩留县" zipcode="678000" />
+            <district name="新源县" zipcode="678000" />
+            <district name="昭苏县" zipcode="678000" />
+            <district name="特克斯县" zipcode="678000" />
+            <district name="尼勒克县" zipcode="678000" />
+            <district name="其他" zipcode="678000" />
+        </city>
+        <city name="楚雄彝族自治州">
+            <district name="楚雄市" zipcode="675000" />
+            <district name="大姚县" zipcode="675000" />
+            <district name="禄丰县" zipcode="675000" />
+            <district name="牟定县" zipcode="675000" />
+            <district name="南华县" zipcode="675000" />
+            <district name="双柏县" zipcode="675000" />
+            <district name="武定县" zipcode="675000" />
+            <district name="姚安县" zipcode="675000" />
+            <district name="永仁县" zipcode="675000" />
+            <district name="元谋县" zipcode="675000" />
+            <district name="其他" zipcode="675000" />
+        </city>
+        <city name="大理白族自治州">
+            <district name="宾川县" zipcode="671000" />
+            <district name="大理市" zipcode="671000" />
+            <district name="洱源县" zipcode="671000" />
+            <district name="鹤庆县" zipcode="671000" />
+            <district name="剑川县" zipcode="671000" />
+            <district name="弥渡县" zipcode="671000" />
+            <district name="南涧彝族自治县" zipcode="671000" />
+            <district name="巍山彝族回族自治县" zipcode="671000" />
+            <district name="祥云县" zipcode="671000" />
+            <district name="漾濞彝族自治县" zipcode="671000" />
+            <district name="永平县" zipcode="671000" />
+            <district name="云龙县" zipcode="671000" />
+            <district name="其他" zipcode="671000" />
+        </city>
+        <city name="德宏傣族景颇族自治州">
+            <district name="梁河县" zipcode="678400" />
+            <district name="陇川县" zipcode="678400" />
+            <district name="潞西市" zipcode="678400" />
+            <district name="瑞丽市" zipcode="678400" />
+            <district name="盈江县" zipcode="678400" />
+            <district name="其他" zipcode="678400" />
+        </city>
+        <city name="迪庆藏族自治州">
+            <district name="德钦县" zipcode="674400" />
+            <district name="维西傈僳族自治县" zipcode="674400" />
+            <district name="香格里拉县" zipcode="674400" />
+            <district name="其他" zipcode="674400" />
+        </city>
+        <city name="红河哈尼族彝族自治州">
+            <district name="个旧市" zipcode="661400" />
+            <district name="河口瑶族自治县" zipcode="661400" />
+            <district name="红河县" zipcode="661400" />
+            <district name="建水县" zipcode="661400" />
+            <district name="金平苗族瑶族傣族自治县" zipcode="661400" />
+            <district name="开远市" zipcode="661400" />
+            <district name="泸西县" zipcode="661400" />
+            <district name="绿春县" zipcode="661400" />
+            <district name="蒙自县" zipcode="661400" />
+            <district name="弥勒县" zipcode="661400" />
+            <district name="屏边苗族自治县" zipcode="661400" />
+            <district name="石屏县" zipcode="661400" />
+            <district name="元阳县" zipcode="661400" />
+            <district name="其他" zipcode="661400" />
+        </city>
+        <city name="昆明市">
+            <district name="安宁市" zipcode="650000" />
+            <district name="呈贡县" zipcode="650000" />
+            <district name="东川区" zipcode="650000" />
+            <district name="富民县" zipcode="650000" />
+            <district name="官渡区" zipcode="650000" />
+            <district name="晋宁县" zipcode="650000" />
+            <district name="禄劝彝族苗族自治县" zipcode="650000" />
+            <district name="盘龙区" zipcode="650000" />
+            <district name="石林彝族自治县" zipcode="650000" />
+            <district name="嵩明县" zipcode="650000" />
+            <district name="五华区" zipcode="650000" />
+            <district name="西山区" zipcode="650000" />
+            <district name="寻甸回族彝族自治县" zipcode="650000" />
+            <district name="宜良县" zipcode="650000" />
+            <district name="其他" zipcode="650000" />
+        </city>
+        <city name="丽江市">
+            <district name="古城区" zipcode="674100" />
+            <district name="华坪县" zipcode="674100" />
+            <district name="宁蒗彝族自治县" zipcode="674100" />
+            <district name="永胜县" zipcode="674100" />
+            <district name="玉龙纳西族自治县" zipcode="674100" />
+            <district name="其他" zipcode="674100" />
+        </city>
+        <city name="临沧市">
+            <district name="沧源佤族自治县" zipcode="677000" />
+            <district name="凤庆县" zipcode="677000" />
+            <district name="耿马傣族佤族自治县" zipcode="677000" />
+            <district name="临翔区" zipcode="677000" />
+            <district name="双江拉祜族佤族布朗族傣族自治县" zipcode="677000" />
+            <district name="永德县" zipcode="677000" />
+            <district name="云县" zipcode="677000" />
+            <district name="镇康县" zipcode="677000" />
+            <district name="其他" zipcode="677000" />
+        </city>
+        <city name="怒江傈僳族自治州">
+            <district name="福贡县" zipcode="673100" />
+            <district name="贡山独龙族怒族自治县" zipcode="673100" />
+            <district name="兰坪白族普米族自治县" zipcode="673100" />
+            <district name="泸水县" zipcode="673100" />
+            <district name="其他" zipcode="673100" />
+        </city>
+        <city name="普洱市">
+            <district name="江城哈尼族彝族自治县" zipcode="665000" />
+            <district name="景东彝族自治县" zipcode="665000" />
+            <district name="景谷傣族彝族自治县" zipcode="665000" />
+            <district name="澜沧拉祜族自治县" zipcode="665000" />
+            <district name="孟连傣族拉祜族佤族自治县" zipcode="665000" />
+            <district name="墨江哈尼族自治县" zipcode="665000" />
+            <district name="宁洱哈尼族彝族自治县" zipcode="665000" />
+            <district name="思茅区" zipcode="665000" />
+            <district name="西盟佤族自治县" zipcode="665000" />
+            <district name="镇沅彝族哈尼族拉祜族自治县" zipcode="665000" />
+            <district name="其他" zipcode="665000" />
+        </city>
+        <city name="曲靖市">
+            <district name="富源县" zipcode="655000" />
+            <district name="会泽县" zipcode="655000" />
+            <district name="陆良县" zipcode="655000" />
+            <district name="罗平县" zipcode="655000" />
+            <district name="马龙县" zipcode="655000" />
+            <district name="麒麟区" zipcode="655000" />
+            <district name="师宗县" zipcode="655000" />
+            <district name="宣威市" zipcode="655000" />
+            <district name="沾益县" zipcode="655000" />
+            <district name="其他" zipcode="655000" />
+        </city>
+        <city name="文山壮族苗族自治州">
+            <district name="富宁县" zipcode="663000" />
+            <district name="广南县" zipcode="663000" />
+            <district name="麻栗坡县" zipcode="663000" />
+            <district name="马关县" zipcode="663000" />
+            <district name="丘北县" zipcode="663000" />
+            <district name="文山县" zipcode="663000" />
+            <district name="西畴县" zipcode="663000" />
+            <district name="砚山县" zipcode="663000" />
+            <district name="其他" zipcode="663000" />
+        </city>
+        <city name="西双版纳傣族自治州">
+            <district name="景洪市" zipcode="666100" />
+            <district name="勐海县" zipcode="666100" />
+            <district name="勐腊县" zipcode="666100" />
+            <district name="其他" zipcode="666100" />
+        </city>
+        <city name="玉溪市">
+            <district name="澄江县" zipcode="653100" />
+            <district name="峨山彝族自治县" zipcode="653100" />
+            <district name="红塔区" zipcode="653100" />
+            <district name="华宁县" zipcode="653100" />
+            <district name="江川县" zipcode="653100" />
+            <district name="通海县" zipcode="653100" />
+            <district name="新平彝族傣族自治县" zipcode="653100" />
+            <district name="易门县" zipcode="653100" />
+            <district name="元江哈尼族彝族傣族自治县" zipcode="653100" />
+            <district name="其他" zipcode="653100" />
+        </city>
+        <city name="昭通市">
+            <district name="大关县" zipcode="657000" />
+            <district name="鲁甸县" zipcode="657000" />
+            <district name="巧家县" zipcode="657000" />
+            <district name="水富县" zipcode="657000" />
+            <district name="绥江县" zipcode="657000" />
+            <district name="威信县" zipcode="657000" />
+            <district name="盐津县" zipcode="657000" />
+            <district name="彝良县" zipcode="657000" />
+            <district name="永善县" zipcode="657000" />
+            <district name="昭阳区" zipcode="657000" />
+            <district name="镇雄县" zipcode="657000" />
+            <district name="其他" zipcode="657000" />
+        </city>
+    </province>
+    <province name="浙江省">
+        <city name="杭州市">
+            <district name="滨江区" zipcode="310000" />
+            <district name="淳安县" zipcode="310000" />
+            <district name="富阳市" zipcode="310000" />
+            <district name="拱墅区" zipcode="310000" />
+            <district name="建德市" zipcode="310000" />
+            <district name="江干区" zipcode="310000" />
+            <district name="临安市" zipcode="310000" />
+            <district name="上城区" zipcode="310000" />
+            <district name="桐庐县" zipcode="310000" />
+            <district name="西湖区" zipcode="310000" />
+            <district name="下城区" zipcode="310000" />
+            <district name="萧山区" zipcode="310000" />
+            <district name="余杭区" zipcode="310000" />
+            <district name="其他" zipcode="310000" />
+        </city>
+        <city name="湖州市">
+            <district name="安吉县" zipcode="313000" />
+            <district name="长兴县" zipcode="313000" />
+            <district name="德清县" zipcode="313000" />
+            <district name="南浔区" zipcode="313000" />
+            <district name="吴兴区" zipcode="313000" />
+            <district name="其他" zipcode="313000" />
+        </city>
+        <city name="嘉兴市">
+            <district name="海宁市" zipcode="314000" />
+            <district name="海盐县" zipcode="314000" />
+            <district name="嘉善县" zipcode="314000" />
+            <district name="南湖区" zipcode="314000" />
+            <district name="平湖市" zipcode="314000" />
+            <district name="桐乡市" zipcode="314000" />
+            <district name="秀洲区" zipcode="314000" />
+            <district name="其他" zipcode="314000" />
+        </city>
+        <city name="金华市">
+            <district name="东阳市" zipcode="321000" />
+            <district name="金东区" zipcode="321000" />
+            <district name="兰溪市" zipcode="321000" />
+            <district name="磐安县" zipcode="321000" />
+            <district name="浦江县" zipcode="321000" />
+            <district name="武义县" zipcode="321000" />
+            <district name="婺城区" zipcode="321000" />
+            <district name="义乌市" zipcode="321000" />
+            <district name="永康市" zipcode="321000" />
+            <district name="其他" zipcode="321000" />
+        </city>
+        <city name="丽水市">
+            <district name="缙云县" zipcode="323000" />
+            <district name="景宁畲族自治县" zipcode="323000" />
+            <district name="莲都区" zipcode="323000" />
+            <district name="龙泉市" zipcode="323000" />
+            <district name="青田县" zipcode="323000" />
+            <district name="庆元县" zipcode="323000" />
+            <district name="松阳县" zipcode="323000" />
+            <district name="遂昌县" zipcode="323000" />
+            <district name="云和县" zipcode="323000" />
+            <district name="其他" zipcode="323000" />
+        </city>
+        <city name="宁波市">
+            <district name="北仑区" zipcode="315000" />
+            <district name="慈溪市" zipcode="315000" />
+            <district name="奉化市" zipcode="315000" />
+            <district name="海曙区" zipcode="315000" />
+            <district name="江北区" zipcode="315000" />
+            <district name="江东区" zipcode="315000" />
+            <district name="宁海县" zipcode="315000" />
+            <district name="象山县" zipcode="315000" />
+            <district name="鄞州区" zipcode="315000" />
+            <district name="余姚市" zipcode="315000" />
+            <district name="镇海区" zipcode="315000" />
+            <district name="其他" zipcode="315000" />
+        </city>
+        <city name="衢州市">
+            <district name="常山县" zipcode="324000" />
+            <district name="江山市" zipcode="324000" />
+            <district name="开化县" zipcode="324000" />
+            <district name="柯城区" zipcode="324000" />
+            <district name="龙游县" zipcode="324000" />
+            <district name="衢江区" zipcode="324000" />
+            <district name="其他" zipcode="324000" />
+        </city>
+        <city name="绍兴市">
+            <district name="上虞市" zipcode="312000" />
+            <district name="绍兴县" zipcode="312000" />
+            <district name="嵊州市" zipcode="312000" />
+            <district name="新昌县" zipcode="312000" />
+            <district name="越城区" zipcode="312000" />
+            <district name="诸暨市" zipcode="312000" />
+            <district name="其他" zipcode="312000" />
+        </city>
+        <city name="台州市">
+            <district name="黄岩区" zipcode="318000" />
+            <district name="椒江区" zipcode="318000" />
+            <district name="临海市" zipcode="318000" />
+            <district name="路桥区" zipcode="318000" />
+            <district name="三门县" zipcode="318000" />
+            <district name="天台县" zipcode="318000" />
+            <district name="温岭市" zipcode="318000" />
+            <district name="仙居县" zipcode="318000" />
+            <district name="玉环县" zipcode="318000" />
+            <district name="其他" zipcode="318000" />
+        </city>
+        <city name="温州市">
+            <district name="苍南县" zipcode="325000" />
+            <district name="洞头县" zipcode="325000" />
+            <district name="乐清市" zipcode="325000" />
+            <district name="龙湾区" zipcode="325000" />
+            <district name="鹿城区" zipcode="325000" />
+            <district name="瓯海区" zipcode="325000" />
+            <district name="平阳县" zipcode="325000" />
+            <district name="瑞安市" zipcode="325000" />
+            <district name="泰顺县" zipcode="325000" />
+            <district name="文成县" zipcode="325000" />
+            <district name="永嘉县" zipcode="325000" />
+            <district name="其他" zipcode="325000" />
+        </city>
+        <city name="舟山市">
+            <district name="岱山县" zipcode="316000" />
+            <district name="定海区" zipcode="316000" />
+            <district name="普陀区" zipcode="316000" />
+            <district name="嵊泗县" zipcode="316000" />
+            <district name="其他" zipcode="316000" />
+        </city>
+    </province>
+    <province name="重庆市">
+        <city name="重庆市">
+            <district name="巴南区" zipcode="404100" />
+            <district name="北碚区" zipcode="404100" />
+            <district name="璧山县" zipcode="404100" />
+            <district name="长寿区" zipcode="404100" />
+            <district name="城口县" zipcode="404100" />
+            <district name="大渡口区" zipcode="404100" />
+            <district name="大足县" zipcode="404100" />
+            <district name="垫江县" zipcode="404100" />
+            <district name="丰都县" zipcode="404100" />
+            <district name="奉节县" zipcode="404100" />
+            <district name="涪陵区" zipcode="404100" />
+            <district name="合川区" zipcode="404100" />
+            <district name="江北区" zipcode="404100" />
+            <district name="江津区" zipcode="404100" />
+            <district name="九龙坡区" zipcode="404100" />
+            <district name="开县" zipcode="404100" />
+            <district name="梁平县" zipcode="404100" />
+            <district name="南岸区" zipcode="404100" />
+            <district name="南川区" zipcode="404100" />
+            <district name="彭水苗族土家族自治县" zipcode="404100" />
+            <district name="綦江县" zipcode="404100" />
+            <district name="黔江区" zipcode="404100" />
+            <district name="荣昌县" zipcode="404100" />
+            <district name="沙坪坝区" zipcode="404100" />
+            <district name="石柱土家族自治县" zipcode="404100" />
+            <district name="双桥区" zipcode="404100" />
+            <district name="铜梁县" zipcode="404100" />
+            <district name="潼南县" zipcode="404100" />
+            <district name="万盛区" zipcode="404100" />
+            <district name="万州区" zipcode="404100" />
+            <district name="巫山县" zipcode="404100" />
+            <district name="巫溪县" zipcode="404100" />
+            <district name="武隆县" zipcode="404100" />
+            <district name="秀山土家族苗族自治县" zipcode="404100" />
+            <district name="永川区" zipcode="404100" />
+            <district name="酉阳土家族苗族自治县" zipcode="404100" />
+            <district name="渝北区" zipcode="404100" />
+            <district name="渝中区" zipcode="404100" />
+            <district name="云阳县" zipcode="404100" />
+            <district name="忠县" zipcode="404100" />
+            <district name="其他" zipcode="404100" />
+        </city>
+    </province>
+    <province name="香港">
+        <city name="香港特别行政区">
+            <district name="香港市区内" zipcode="999077" />
+            <district name="其他" zipcode="999077" />
+        </city>
+    </province>
+    <province name="澳门">
+        <city name="澳门特别行政区">
+            <district name="澳门市区内" zipcode="999078" />
+            <district name="其他" zipcode="999078" />
+        </city>
+    </province>
+    <province name="台湾">
+        <city name="台北市">
+            <district name="台北市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="髙雄市">
+            <district name="髙雄市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="基隆市">
+            <district name="基隆市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="台中市">
+            <district name="台中市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="台南市">
+            <district name="台南市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="新竹市">
+            <district name="新竹市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="嘉义市">
+            <district name="嘉义市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="新北市">
+            <district name="新北市区" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="宜兰县">
+            <district name="宜兰县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="桃园县">
+            <district name="桃园县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="新竹县">
+            <district name="新竹县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="苗栗县">
+            <district name="苗栗县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="彰化县">
+            <district name="彰化县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="南投县">
+            <district name="南投县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="云林县">
+            <district name="云林县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="嘉义县">
+            <district name="嘉义县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="屏东县">
+            <district name="屏东县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="台东县">
+            <district name="台东县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="花莲县">
+            <district name="花莲县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+        <city name="澎湖县">
+            <district name="澎湖县" zipcode="000000" />
+            <district name="其他" zipcode="000000" />
+        </city>
+
+    </province>
+</root>

+ 8 - 0
app/assets/server.json

@@ -0,0 +1,8 @@
+{
+  "id" : "o2CenterServer",
+  "name" : "develop",
+  "centerHost" : "43.154.125.245",
+  "centerContext" : "/x_program_center",
+  "centerPort" : 8089,
+  "httpProtocol" : "http"
+}

+ 328 - 0
app/build.gradle

@@ -0,0 +1,328 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'realm-android'
+
+ext {
+    //定义变量
+    signingConfigKeyAlias = ""
+    signingConfigKeyPassword = ""
+    signingConfigStoreFilePath = ""
+    signingConfigStorePassword = ""
+
+    jpushAppKey = ""
+    baiduSpeechAppId = ""
+    baiduSpeechSecret = ""
+    baiduSpeechAppKey = ""
+    baiduMapAppKey = ""
+    jpushIMPassword = ""
+    xiaomiAppId=""
+    xiaomiAppKey=""
+    oppoAppKey=""
+    oppoAppId=""
+    oppoAppSecret=""
+    vivoAppKey=""
+    vivoAppId=""
+}
+
+
+def loadProperties() {
+    Properties properties = new Properties()
+    properties.load(project.rootProject.file('local.properties').newDataInputStream())
+    // 打包apk使用的密钥文件相关信息,请填写到local.properties文件中
+    project.signingConfigKeyAlias = properties.getProperty("signingConfig.keyAlias")
+    project.signingConfigKeyPassword = properties.getProperty("signingConfig.keyPassword")
+    project.signingConfigStoreFilePath = properties.getProperty("signingConfig.storeFilePath")
+    project.signingConfigStorePassword = properties.getProperty("signingConfig.storePassword")
+
+    //release key
+    project.jpushAppKey = project.property("JPUSH_APPKEY").toString()
+    project.baiduSpeechAppId = project.property("BAIDU_SPEECH_APPID").toString()
+    project.baiduSpeechSecret = project.property("BAIDU_SPEECH_SECRET").toString()
+    project.baiduSpeechAppKey = project.property("BAIDU_SPEECH_APPKEY").toString()
+    project.baiduMapAppKey = project.property("BAIDU_MAP_APPKEY").toString()
+    project.xiaomiAppId=project.property("XIAOMI_APPID").toString()
+    project.xiaomiAppKey=project.property("XIAOMI_APPKEY").toString()
+//    project.oppoAppKey=project.property("OPPO_APPKEY").toString()
+//    project.oppoAppId=project.property("OPPO_APPID").toString()
+//    project.oppoAppSecret=project.property("OPPO_APPSECRET").toString()
+//    project.vivoAppKey=project.property("VIVO_APPKEY").toString()
+//    project.vivoAppId=project.property("VIVO_APPID").toString()
+
+
+}
+
+
+loadProperties()
+
+task printVersionName {
+    def v = project.property("o2.versionName").toString()
+    println("${v}")
+}
+
+
+android {
+    compileSdkVersion 31
+    buildToolsVersion "30.0.3"
+    sourceSets {
+        main {
+            jniLibs.srcDir 'libs'
+            assets.srcDirs = ['assets']
+            res.srcDirs = ['src/main/res', 'src/main/res/raw']
+        }
+    }
+    signingConfigs {
+        release {
+            v1SigningEnabled true
+            v2SigningEnabled true
+            keyAlias project.signingConfigKeyAlias
+            keyPassword project.signingConfigKeyPassword
+            storeFile file(project.signingConfigStoreFilePath)
+            storePassword project.signingConfigStorePassword
+        }
+        debug {
+            v1SigningEnabled true
+            v2SigningEnabled true
+            keyAlias project.signingConfigKeyAlias
+            keyPassword project.signingConfigKeyPassword
+            storeFile file(project.signingConfigStoreFilePath)
+            storePassword project.signingConfigStorePassword
+//            keyAlias 'androiddebugkey'
+//            storeFile file('debug.keystore')
+//            keyPassword 'android'
+        }
+    }
+
+    defaultConfig {
+        applicationId "com.dccr.oa"
+        minSdkVersion 21
+        targetSdkVersion 31
+        versionCode project.property("o2.versionCode").toInteger()
+        versionName project.property("o2.versionName").toString()
+        multiDexEnabled true
+        ndk {
+            //选择要添加的对应cpu类型的.so库。
+//            abiFilters 'armeabi', 'armeabi-v7a'
+//            abiFilters 'armeabi-v7a', 'arm64-v8a'
+            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
+        }
+        multiDexKeepProguard file('multidex_keep_file.pro')
+        vectorDrawables.useSupportLibrary = true
+        dataBinding {
+            enabled = true
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+
+    buildTypes {
+        debug {
+            signingConfig signingConfigs.release
+            buildConfigField "Boolean", "InnerServer", "true"
+            buildConfigField "Boolean", "LOG_ENABLE", "true"
+            buildConfigField "Boolean", "NEED_UPDATE", "true"
+            buildConfigField "Boolean", "LOG_FILE", "true"
+            manifestPlaceholders = [JPUSH_PKGNAME      : defaultConfig.applicationId,
+                                    JPUSH_APPKEY       : project.jpushAppKey,
+                                    XIAOMI_APPID : project.xiaomiAppId,
+                                    XIAOMI_APPKEY : project.xiaomiAppKey,
+                                    BAIDU_SPEECH_APPID : project.baiduSpeechAppId,
+                                    BAIDU_SPEECH_SECRET: project.baiduSpeechSecret,
+                                    BAIDU_SPEECH_APPKEY: project.baiduSpeechAppKey,
+                                    BAIDU_MAP_APPKEY   : project.baiduMapAppKey]
+            zipAlignEnabled true
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+        release {
+            signingConfig signingConfigs.release
+            buildConfigField "Boolean", "InnerServer", "true"
+            buildConfigField "Boolean", "LOG_ENABLE", "false"
+            buildConfigField "Boolean", "NEED_UPDATE", "true"
+            buildConfigField "Boolean", "LOG_FILE", "true"
+            manifestPlaceholders = [JPUSH_PKGNAME      : defaultConfig.applicationId,
+                                    JPUSH_APPKEY       : project.jpushAppKey,
+                                    XIAOMI_APPID : project.xiaomiAppId,
+                                    XIAOMI_APPKEY : project.xiaomiAppKey,
+                                    BAIDU_SPEECH_APPID : project.baiduSpeechAppId,
+                                    BAIDU_SPEECH_SECRET: project.baiduSpeechSecret,
+                                    BAIDU_SPEECH_APPKEY: project.baiduSpeechAppKey,
+                                    BAIDU_MAP_APPKEY   : project.baiduMapAppKey]
+            zipAlignEnabled true
+            minifyEnabled true
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+
+            //apk包重命名
+            applicationVariants.all { variant ->
+                variant.outputs.all {
+                    outputFileName = "${variant.productFlavors[0].name}-${variant.versionName}.apk"
+                }
+            }
+
+        }
+    }
+
+    android {
+        lintOptions {
+            abortOnError false
+        }
+
+    }
+
+    lintOptions {
+        checkReleaseBuilds false
+        abortOnError false
+    }
+    flavorDimensions "type"
+    productFlavors {
+        O2PLATFORM {
+            manifestPlaceholders = [JPUSH_CHANNEL: "pgy"]
+        }
+        huawei {
+            manifestPlaceholders = [JPUSH_CHANNEL: "huawei"]
+        }
+        xiaomi {
+            manifestPlaceholders = [JPUSH_CHANNEL: "xiaomi"]
+        }
+    }
+
+}
+
+
+buildscript {
+    repositories {
+        mavenCentral()
+
+    }
+    dependencies {
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version"
+
+    }
+}
+
+repositories {
+    flatDir {
+        dirs 'libs'
+    }
+    //o2oa
+    maven { url "../o2_flutter/buildAAR/host/outputs/repo" }
+    maven {
+        url 'https://storage.googleapis.com/download.flutter.io'
+    }
+}
+
+dependencies {
+    // 百度地图
+    implementation files('libs/BaiduLBS_Android.jar')
+    // 扫码
+    implementation 'com.google.zxing:core:3.4.1'
+    implementation files('libs/pinyin4j-2.5.0.jar')
+    implementation files('libs/universal-image-loader-1.9.5.jar')
+//    implementation(name: 'material-calendarview-fancy-1.1', ext: 'aar')
+    implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
+    implementation 'com.github.prolificinteractive:material-calendarview:2.0.0'
+    implementation project(path: ':o2_auth_sdk')
+
+//    // flutter 相关的aar
+    // flutter模块开发调试用
+//    implementation project(':flutter')
+//    debugImplementation 'io.flutter.plugins.pathprovider:path_provider_android_debug:1.0'
+    implementation 'io.flutter.plugins.pathprovider:path_provider_android_release:1.0'
+//    debugImplementation 'io.flutter.plugins.sharedpreferences:shared_preferences_android_debug:1.0'
+    implementation 'io.flutter.plugins.sharedpreferences:shared_preferences_android_release:1.0'
+//    debugImplementation 'net.o2oa.app.flutter.o2_flutter:flutter_debug:1.0'
+    implementation 'net.o2oa.app.flutter.o2_flutter:flutter_release:1.0'
+
+
+
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+    implementation "org.jetbrains.anko:anko-common:$anko_version"
+
+    implementation('androidx.legacy:legacy-support-v4:1.0.0')
+    implementation('androidx.recyclerview:recyclerview:1.2.1') {
+        exclude module: 'support-v4'
+    }
+    implementation('androidx.appcompat:appcompat:1.2.0')
+    implementation('androidx.cardview:cardview:1.0.0')
+    implementation('com.google.android.material:material:1.2.0')
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    implementation 'androidx.multidex:multidex:2.0.1'
+    implementation 'com.github.PhilJay:MPAndroidChart:v2.2.4'
+    implementation('com.github.bumptech.glide:glide:4.12.0')
+    implementation 'com.afollestad.material-dialogs:core:0.8.5.9'
+    implementation(name: 'changeskin-1.3.0', ext: 'aar')
+    implementation 'io.o2oa:signatureview:1.0.0'
+    implementation 'com.github.fancylou:CalendarView:v1.2.0'
+    implementation 'com.readystatesoftware.systembartint:systembartint:1.0.3'
+    implementation 'com.facebook.shimmer:shimmer:0.1.0@aar'
+    implementation 'com.borax12.materialdaterangepicker:library:1.9'
+    implementation 'com.yanzhenjie.recyclerview:x:1.3.2'
+    implementation 'com.race604.waveloading:library:1.1.1'
+    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
+    implementation 'com.squareup.retrofit2:converter-gson:2.2.0'
+    implementation 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
+    implementation 'com.squareup.okhttp3:okhttp:3.11.0'
+    implementation 'io.reactivex:rxjava:1.1.6'
+    implementation 'io.reactivex:rxandroid:1.2.1'
+
+    /////////////////////////////////////////////////推送///////////////////////
+    implementation project(':jiguang')
+
+
+    implementation 'com.contrarywind:Android-PickerView:4.1.9'
+
+    //滚动选择器
+    implementation 'com.jzxiang.pickerview:TimePickerDialog:1.0.1'
+
+    //链式方式获取Activity返回结果
+    implementation 'com.github.lwugang:ActivityResult:59b23e3682'
+
+    //google architecture component
+    // ViewModel and LiveData
+    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+
+    // alternatively - just ViewModel
+    implementation 'androidx.lifecycle:lifecycle-viewmodel:2.2.0'
+
+    // use -ktx for Kotlin
+
+    // alternatively - just LiveData
+    implementation 'androidx.lifecycle:lifecycle-livedata:2.2.0'
+
+
+    //     Support library depends on this lightweight import
+    implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0'
+    //noinspection LifecycleAnnotationProcessorWithJava8
+    annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.2.0'
+
+    implementation 'com.google.code.gson:gson:2.8.5'
+
+    //GSYVideo播放器
+    implementation('com.shuyu:GSYVideoPlayer:8.0.0')
+
+    //mp3录音
+    implementation 'com.github.zhaolewei:ZlwAudioRecorder:v1.07'
+
+    //gif 播放
+    implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.24'
+
+    // 图片放大缩小
+    implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
+
+
+
+    testImplementation 'junit:junit:4.12'
+
+}
+
+
+tasks.whenTaskAdded { task ->
+    if (task.name == "lint") {
+        task.enabled = false
+    }
+}

BIN
app/debug.keystore


BIN
app/libs/BaiduLBS_Android.jar


BIN
app/libs/arm64-v8a/libBaiduMapSDK_base_v7_5_2.so


BIN
app/libs/arm64-v8a/libBaiduMapSDK_map_v7_5_2.so


BIN
app/libs/arm64-v8a/libgnustl_shared.so


BIN
app/libs/arm64-v8a/liblocSDK8b.so


BIN
app/libs/armeabi-v7a/libBaiduMapSDK_base_v7_5_2.so


BIN
app/libs/armeabi-v7a/libBaiduMapSDK_map_v7_5_2.so


BIN
app/libs/armeabi-v7a/libgnustl_shared.so


BIN
app/libs/armeabi-v7a/liblocSDK8b.so


BIN
app/libs/bdasr_V3_20180320_9066860.jar


BIN
app/libs/changeskin-1.3.0.aar


BIN
app/libs/com.baidu.tts_2.3.1.20170808_e39ea89.jar


BIN
app/libs/material-calendarview-fancy-1.1.aar


BIN
app/libs/pinyin4j-2.5.0.jar


BIN
app/libs/universal-image-loader-1.9.5.jar


+ 0 - 0
app/multidex_keep_file.pro


+ 367 - 0
app/proguard-rules.pro

@@ -0,0 +1,367 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in E:\workApp\Android\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+########################      通用配置    ###################################
+
+#指定代码的压缩级别
+-optimizationpasses 5
+
+#包明不混合大小写
+-dontusemixedcaseclassnames
+
+#不去忽略非公共的库类
+-dontskipnonpubliclibraryclasses
+
+ #优化  不优化输入的类文件
+-dontoptimize
+
+ #预校验
+-dontpreverify
+
+ #混淆时是否记录日志
+-verbose
+
+ # 混淆时所采用的算法
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+
+
+# 保持哪些类不被混淆
+-keep public class * extends android.app.Fragment
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+#如果有引用v4包可以添加下面这行
+-keep public class * extends androidx.fragment.app.Fragment
+
+
+-ignorewarnings
+
+#如果引用了v4或者v7包
+-dontwarn android.support.**
+
+
+####混淆保护自己项目的部分代码以及引用的第三方jar包library-end####
+
+# 视频播放库 com.shuyu:GSYVideoPlayer
+-keep class com.shuyu.gsyvideoplayer.video.** { *; }
+-dontwarn com.shuyu.gsyvideoplayer.video.**
+-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
+-dontwarn com.shuyu.gsyvideoplayer.video.base.**
+-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
+-dontwarn com.shuyu.gsyvideoplayer.utils.**
+-keep class tv.danmaku.ijk.** { *; }
+-dontwarn tv.danmaku.ijk.**
+
+
+
+
+-keep public class * extends android.view.View{
+    *** get*();
+    void set*(***);
+    public <init>(android.content.Context);
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+#保持 native 方法不被混淆
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+#保持自定义控件类不被混淆
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+#保持自定义控件类不被混淆
+-keepclassmembers class * extends android.app.Activity {
+   public void *(android.view.View);
+}
+
+-keep public class * extends android.view.View {
+    public <init>(android.content.Context);
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+    public void set*(...);
+}
+
+#保持 Parcelable 不被混淆
+-keep class * implements android.os.Parcelable {
+  public static final android.os.Parcelable$Creator *;
+}
+
+#保持 Serializable 不被混淆
+-keepnames class * implements java.io.Serializable
+
+#保持 Serializable 不被混淆并且enum 类也不被混淆
+-keepclassmembers class * implements java.io.Serializable {
+    static final long serialVersionUID;
+    private static final java.io.ObjectStreamField[] serialPersistentFields;
+    !static !transient <fields>;
+    !private <fields>;
+    !private <methods>;
+    private void writeObject(java.io.ObjectOutputStream);
+    private void readObject(java.io.ObjectInputStream);
+    java.lang.Object writeReplace();
+    java.lang.Object readResolve();
+}
+
+#保持枚举 enum 类不被混淆
+-keepclassmembers enum * {
+  public static **[] values();
+  public static ** valueOf(java.lang.String);
+}
+
+-keepclassmembers class * {
+    public void *ButtonClicked(android.view.View);
+}
+
+#不混淆资源类
+-keepclassmembers class **.R$* {
+    public static <fields>;
+}
+
+#避免混淆泛型 如果混淆报错建议关掉
+#-keepattributes Signature
+
+
+
+#######################     常用第三方模块的混淆选项         ###################################
+#gson
+#如果用用到Gson解析包的,直接添加下面这几行就能成功混淆,不然会报错。
+#保护注解
+-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
+# Gson specific classes
+-keep class sun.misc.Unsafe { *; }
+# Application classes that will be serialized/deserialized over Gson
+-keep class com.google.gson.** { *; }
+-keep class com.google.gson.stream.** { *; }
+
+
+#butterknife
+-keep class butterknife.** { *; }
+-dontwarn butterknife.internal.**
+-keep class **$$ViewBinder { *; }
+
+-keepclasseswithmembernames class * {
+    @butterknife.* <fields>;
+}
+
+-keepclasseswithmembernames class * {
+    @butterknife.* <methods>;
+}
+
+
+#Glide
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
+  **[] $VALUES;
+  public *;
+}
+-keep class  com.bumptech.glide.** { *; }
+#-keepresourcexmlelements manifest/application/meta-data@value=GlideModule
+
+
+# 如果使用了Gson之类的工具要使被它解析的JavaBean类即实体类不被混淆。
+-keep class net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.** { *; }
+
+#rx
+-keep class rx.** { *; }
+#retrofit
+-keep class retrofit2.** { *; }
+#guava
+-keep class com.google.common.base.** { *; }
+#chart
+-keep class com.github.PhilJay.** { *; }
+#materialdialogs
+-keep class com.afollestad.materialdialogs.** { *; }
+#日历控件 github > material-calendarview
+-keep class com.prolificinteractive.** { *; }
+
+#log4j
+-keep class org.apache.log4j.** { *; }
+-dontwarn org.apache.log4j.**
+#baidu
+-keep class com.baidu.** {*;}
+-keep class vi.com.** {*;}
+-dontwarn com.baidu.**
+#baidu yuyin
+-keep class com.baidu.speech.**{*;}
+-keep class com.baidu.tts.**{*;}
+-keep class com.baidu.speechsynthesizer.**{*;}
+
+##umeng push
+-dontwarn com.taobao.**
+-dontwarn anet.channel.**
+-dontwarn anetwork.channel.**
+-dontwarn org.android.**
+-dontwarn org.apache.thrift.**
+-dontwarn com.xiaomi.**
+-dontwarn com.huawei.**
+
+-keepattributes *Annotation*
+
+-keep class com.taobao.** {*;}
+-keep class org.android.** {*;}
+-keep class anet.channel.** {*;}
+-keep class com.umeng.** {*;}
+-keep class com.xiaomi.** {*;}
+-keep class com.huawei.** {*;}
+-keep class org.apache.thrift.** {*;}
+
+-keep class com.alibaba.sdk.android.**{*;}
+-keep class com.ut.**{*;}
+-keep class com.ta.**{*;}
+
+-keep public class **.R$*{
+   public static final int *;
+}
+
+
+
+#jiguang
+-dontoptimize
+-dontpreverify
+-keepattributes  EnclosingMethod,Signature
+-dontwarn cn.jpush.**
+-keep class cn.jpush.** { *; }
+
+-dontwarn cn.jiguang.**
+-keep class cn.jiguang.** { *; }
+
+ -keepclassmembers class ** {
+     public void onEvent*(**);
+ }
+
+-keep class sj.qqkeyboard.** { *; }
+-keep class com.sj.emoji.** { *; }
+-keep class com.testemticon.** { *; }
+-keep class jiguang.chat.** { *; }
+
+
+#========================gson================================
+-dontwarn com.google.**
+-keep class com.google.gson.** {*;}
+-keep class com.google.code.gson.** {*;}
+
+#========================protobuf================================
+-keep class com.google.protobuf.** {*;}
+
+
+
+#-optimizationpasses 7
+#-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+-dontoptimize
+-dontusemixedcaseclassnames
+-verbose
+-dontskipnonpubliclibraryclasses
+-dontskipnonpubliclibraryclassmembers
+-dontwarn dalvik.**
+#-overloadaggressively
+
+# ------------------ Keep LineNumbers and properties ---------------- #
+-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
+# --------------------------------------------------------------------------
+
+
+#------------------  下方是android平台自带的排除项,这里不要动         ----------------
+
+-keep public class * extends android.app.Activity{
+	public <fields>;
+	public <methods>;
+}
+
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+
+-keepclassmembers enum * {
+    public static **[] values();
+    public static ** valueOf(java.lang.String);
+}
+
+-keepclasseswithmembers class * {
+	public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+	public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepattributes *Annotation*
+
+-keepclasseswithmembernames class *{
+	native <methods>;
+}
+
+-keep class * implements android.os.Parcelable {
+  public static final android.os.Parcelable$Creator *;
+}
+
+#------------------  下方是共性的排除项目         ----------------
+# 方法名中含有“JNI”字符的,认定是Java Native Interface方法,自动排除
+# 方法名中含有“JRI”字符的,认定是Java Reflection Interface方法,自动排除
+
+-keepclasseswithmembers class * {
+    ... *JNI*(...);
+}
+
+-keepclasseswithmembernames class * {
+	... *JRI*(...);
+}
+
+-keep class **JNI* {*;}
+
+-keep class io.flutter.app.** { *; }
+-keep class io.flutter.plugin.**  { *; }
+-keep class io.flutter.util.**  { *; }
+-keep class io.flutter.view.**  { *; }
+-keep class io.flutter.**  { *; }
+-keep class io.flutter.plugins.**  { *; }
+
+
+-keep class net.sourceforge.pinyin4j.** { *; }
+
+# 华为push
+-ignorewarnings
+-keepattributes *Annotation*
+-keepattributes Exceptions
+-keepattributes InnerClasses
+-keepattributes Signature
+-keepattributes SourceFile,LineNumberTable
+-keep class com.huawei.hianalytics.**{*;}
+-keep class com.huawei.updatesdk.**{*;}
+-keep class com.huawei.hms.**{*;}
+
+# 图片选择器
+-keep class com.luck.picture.lib.** { *; }
+
+# use Camerax
+-keep class com.luck.lib.camerax.** { *; }
+
+# use uCrop
+-dontwarn com.yalantis.ucrop**
+-keep class com.yalantis.ucrop** { *; }
+-keep interface com.yalantis.ucrop** { *; }

+ 496 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,496 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="net.zoneland.x.bpm.mobile.v1.zoneXBPM"
+    tools:ignore="LockedOrientationActivity">
+    <!-- baidu需要 -->
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" /> <!-- 获取网络状态 -->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <!-- 网络通信 -->
+    <uses-permission android:name="android.permission.INTERNET" /> <!-- 获取设备信息 -->
+    <uses-permission
+        android:name="android.permission.READ_PHONE_STATE"
+        android:maxSdkVersion="28" /> <!-- <uses-permission -->
+    <!-- android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> -->
+    <!-- 读写sdcard,storage等等 -->
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="28" /> <!-- 允许程序录制音频 -->
+    <uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 拨打电话 -->
+    <uses-permission android:name="android.permission.CALL_PHONE" /> <!-- 拍照 -->
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.BROADCAST_STICKY" />
+    <uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" /> <!-- 允许访问震动器 -->
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 选举使用,当应用有删除或者更新时需要重新选举,复用推送通道 -->
+    <uses-permission android:name="android.permission.BROADCAST_PACKAGE_CHANGED" />
+    <uses-permission android:name="android.permission.BROADCAST_PACKAGE_REPLACED" /> <!-- 允许task重排序 -->
+    <uses-permission android:name="android.permission.REORDER_TASKS" /> <!-- 蓝牙 -->
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- gps -->
+    <uses-feature android:name="android.hardware.location.gps" /> <!-- android 8.0 安装未知来源apk的权限问题 -->
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+
+    <application
+        android:name=".O2App"
+        android:allowBackup="false"
+        android:hardwareAccelerated="true"
+        android:icon="@mipmap/logo"
+        android:label="@string/app_name"
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:requestLegacyExternalStorage="true"
+        android:roundIcon="@mipmap/logo_round"
+        android:theme="@style/XBPMTheme.NoActionBar"
+        android:usesCleartextTraffic="true"
+        tools:replace="android:allowBackup">
+        <activity
+            android:name=".app.o2.security.EmpowerCreateActivity"
+            android:exported="false" />
+        <activity
+            android:name=".app.o2.security.EmpowerListActivity"
+            android:exported="false" />
+        <activity
+            android:name=".app.attendance.appeal.AttendanceV2AppealActivity"
+            android:exported="false" />
+        <activity
+            android:name=".app.clouddrive.v3.folder.FolderFileListActivity"
+            android:exported="false" />
+        <activity
+            android:name=".app.clouddrive.v3.zone.ZoneActivity"
+            android:exported="false" />
+        <activity
+            android:name=".app.clouddrive.v3.CloudDiskV3Activity"
+            android:exported="false" /> <!-- 图片选择器 -->
+        <activity
+            android:name=".utils.pick.PicturePickActivity"
+            android:theme="@style/tyhjtranslucentTheme" />
+        <activity android:name=".app.o2.logs.LogsActivity" />
+        <activity
+            android:name=".app.o2.main.SearchActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait"
+            android:windowSoftInputMode="stateVisible|adjustPan" />
+        <activity
+            android:name=".app.o2.main.SearchV2Activity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait"
+            android:windowSoftInputMode="stateVisible|adjustPan" />
+        <activity
+            android:name=".app.o2.webview.O2WebViewActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.clouddrive.v2.share.CloudShareActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.im.O2LocationActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.im.O2InstantMessageActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.im.O2ChatActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.im.O2ChatGroupMemberActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.im.fm.O2IMConversationPickerActivity"
+            android:configChanges="orientation"
+            android:exported="false" />
+        <activity
+            android:name=".app.VideoPlayerActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMTheme.fullscreen" />
+        <activity
+            android:name=".app.clouddrive.v2.viewer.BigImageViewActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMTheme.fullscreen" />
+        <activity
+            android:name=".app.clouddrive.v2.type.CloudDiskFileTypeActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.clouddrive.v2.picker.CloudDiskFolderPickerActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.clouddrive.v2.CloudDiskActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_yunpan"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity
+            android:name=".app.o2.organization.ContactPickerActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.cms.application.CMSPublishDocumentActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.security.DeviceManagerActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.launch.LaunchActivity"
+            android:configChanges="orientation"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMLauncherTheme">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+
+                <data
+                    android:host="launch"
+                    android:scheme="o2oa" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".app.o2.bind.BindPhoneActivity"
+            android:configChanges="orientation"
+            android:label="@string/app_name"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMClearActivityTheme" />
+        <activity
+            android:name=".app.o2.login.LoginActivity"
+            android:configChanges="orientation"
+            android:label="@string/app_name"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMClearActivityTheme"
+            android:windowSoftInputMode="adjustUnspecified|stateHidden" />
+        <activity
+            android:name=".app.o2.main.MainActivity"
+            android:configChanges="orientation"
+            android:label="@string/app_name"
+            android:launchMode="singleTask"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMClearActivityTheme" />
+        <activity
+            android:name=".app.o2.group.GroupActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_group"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.person.PersonActivity"
+            android:configChanges="orientation"
+            android:exported="true"
+            android:label="@string/title_activity_person_info"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="o2_person" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".app.o2.organization.NewOrganizationActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_contact"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.my.MyInfoActivity"
+            android:configChanges="orientation"
+            android:exported="true"
+            android:label="@string/title_activity_my_info"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="o2_my_info" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".app.o2.my.ClipAvatarActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_clip_avatar"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.security.AccountSecurityActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_account_security"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.notice.NoticeSettingActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_notice_setting"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.notice.NoticeSettingHelpActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_notice_setting"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.about.AboutActivity"
+            android:configChanges="orientation"
+            android:label="@string/app_about"
+            android:screenOrientation="portrait" /> <!-- 扫描二维码 -->
+        <activity
+            android:name=".utils.zxing.activity.CaptureActivity"
+            android:configChanges="orientation"
+            android:label="@string/str_scan_title"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.scanlogin.ScanLoginActivity"
+            android:configChanges="orientation"
+            android:label="@string/scan_login_confirm_title"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.process.StartProcessActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_start_process"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.webview.TaskWebViewActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:label="@string/title_activity_work_web_view"
+            android:screenOrientation="nosensor" />
+        <activity
+            android:name=".app.o2.process.TaskListActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_task_list"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.process.TaskCompletedListActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_task_complete"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.process.TaskCompletedSearchActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_task_complete"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.process.ReadListActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_read_list"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.o2.process.ReadCompletedListActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_read_complete"
+            android:screenOrientation="portrait" /> <!-- bbs -->
+        <activity
+            android:name=".app.bbs.main.BBSMainActivity"
+            android:configChanges="orientation"
+            android:label="@string/bbs"
+            android:screenOrientation="portrait"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity
+            android:name=".app.bbs.section.BBSSectionActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_bbs_section"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".app.bbs.view.BBSWebViewSubjectActivity"
+            android:configChanges="orientation"
+            android:label="@string/title_activity_bbs_view"
+            android:screenOrientation="portrait"
+            android:windowSoftInputMode="adjustResize|stateHidden" />
+        <activity
+            android:name=".app.bbs.publish.BBSPublishSubjectActivity"
+            android:label="@string/title_activity_bbs_publish_subject" />
+        <activity
+            android:name=".app.bbs.reply.BBSReplyActivity"
+            android:label="@string/title_activity_bbs_reply" /> <!-- cms -->
+        <activity
+            android:name=".app.cms.index.CMSIndexActivity"
+            android:label="@string/cms"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity
+            android:name=".app.cms.application.CMSApplicationActivity"
+            android:label="@string/title_cms_application" />
+        <activity
+            android:name=".app.cms.view.CMSWebViewActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:label="@string/title_cms_view"
+            android:screenOrientation="nosensor" /> <!-- cloud drive -->
+        <activity
+            android:name=".app.clouddrive.CloudDriveActivity"
+            android:label="@string/title_activity_yunpan"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity
+            android:name=".app.clouddrive.viewer.PictureViewActivity"
+            android:label="@string/title_activity_picture_viewer" /> <!-- meeting -->
+        <activity
+            android:name=".app.meeting.main.MeetingMainActivity"
+            android:label="@string/title_activity_meeting"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity android:name=".app.meeting.room.MeetingRoomChooseActivity" />
+        <activity
+            android:name=".app.meeting.apply.MeetingApplyActivity"
+            android:label="@string/title_activity_meeting_create_form" />
+        <activity
+            android:name=".app.meeting.edit.MeetingEditActivity"
+            android:label="@string/title_activity_meeting_edit_form" />
+        <activity
+            android:name=".app.meeting.invited.MeetingDetailInfoActivity"
+            android:label="@string/meeting_detail" />
+        <activity
+            android:name=".app.meeting.reserve.MeetingRoomDetailActivity"
+            android:label="@string/meeting_detail" /> <!-- attendance -->
+        <activity
+            android:name=".app.attendance.main.AttendanceMainActivity"
+            android:label="@string/attendance_check_in_title"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity
+            android:name=".app.attendance.list.AttendanceListActivity"
+            android:label="@string/title_activity_attendance" />
+        <activity
+            android:name=".app.attendance.appeal.AttendanceAppealActivity"
+            android:label="@string/title_activity_attendance_appeal" />
+        <activity
+            android:name=".app.attendance.approval.AttendanceAppealApprovalActivity"
+            android:label="@string/title_activity_attendance_appeal_approval" />
+        <activity
+            android:name=".app.attendance.setting.AttendanceLocationSettingActivity"
+            android:label="@string/title_activity_attendance_location_setting" /> <!-- <activity -->
+        <!-- android:name=".app.o2.main.MyAppActivity" -->
+        <!-- android:label="所有应用" /> -->
+        <!-- ai -->
+        <!-- <activity -->
+        <!-- android:name=".app.o2.ai.O2AIActivity" -->
+        <!-- android:launchMode="singleTask" /> -->
+        <!-- calendar -->
+        <activity
+            android:name=".app.calendar.CalendarMainActivity"
+            android:label="@string/calendar_name"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" />
+        <activity android:name=".app.calendar.CreateEventActivity" />
+        <activity android:name=".app.calendar.CreateCalendarActivity" />
+        <activity android:name=".app.calendar.CalendarStoreActivity" /> <!-- portal -->
+        <activity
+            android:name=".app.o2.webview.PortalWebViewActivity"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:screenOrientation="nosensor"
+            android:theme="@style/XBPMTheme.NoActionBar.Transparent" /> <!-- 换肤 -->
+        <activity
+            android:name=".app.o2.skin.SkinManagerActivity"
+            android:label="@string/skin_manager" /> <!-- 蓝牙相关 -->
+        <activity android:name=".app.bluetooth.BlueToothClientActivity" />
+        <activity android:name=".app.bluetooth.BlueToothServerActivity" />
+        <activity android:name=".app.bluetooth.BlueToothBLEClientActivity" />
+        <activity android:name=".app.bluetooth.BlueToothBLEServerActivity" />
+        <activity
+            android:name=".flutter.FlutterConnectActivity"
+            android:configChanges="orientation"
+            android:screenOrientation="portrait" /> <!-- ```````````````````service```````````````````` -->
+        <!-- baidu -->
+        <service
+            android:name="com.baidu.location.f"
+            android:enabled="true"
+            android:process=":remote" />
+
+        <meta-data
+            android:name="com.baidu.lbsapi.API_KEY"
+            android:value="${BAIDU_MAP_APPKEY}" />
+        <!--
+ baidu yuyin
+        <service
+            android:name="com.baidu.speech.VoiceRecognitionService"
+            android:exported="false" />
+
+        <meta-data
+            android:name="com.baidu.speech.APP_ID"
+            android:value="${BAIDU_SPEECH_APPID}" />
+        <meta-data
+            android:name="com.baidu.speech.API_KEY"
+            android:value="${BAIDU_SPEECH_APPKEY}" />
+        <meta-data
+            android:name="com.baidu.speech.SECRET_KEY"
+            android:value="${BAIDU_SPEECH_SECRET}" />
+        -->
+        <!-- 删除临时文件任务 -->
+        <service
+            android:name=".core.service.ClearTempFileJobService"
+            android:exported="true"
+            android:permission="android.permission.BIND_JOB_SERVICE" /> <!-- 下载apk -->
+        <service
+            android:name=".core.service.DownloadAPKService"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="${applicationId}.action.UPDATE" />
+            </intent-filter>
+        </service> <!-- 重启应用的service -->
+        <service android:name=".core.service.RestartSelfService" />
+        <service
+            android:name=".core.service.WebSocketService"
+            android:exported="false" /> <!-- 录音 转mp3 service -->
+        <service android:name="com.zlw.main.recorderlib.recorder.RecordService" /> <!-- jpush -->
+        <!-- Since JCore2.0.0 Required SDK核心功能 -->
+        <!-- 可配置android:process参数将Service放在其他进程中;android:enabled属性不能是false -->
+        <!-- 这个是自定义Service,要继承极光JCommonService,可以在更多手机平台上使得推送通道保持的更稳定 -->
+        <service
+            android:name=".core.service.JpushService"
+            android:enabled="true"
+            android:exported="false"
+            android:process=":pushcore">
+            <intent-filter>
+                <action android:name="cn.jiguang.user.service.action" />
+            </intent-filter>
+        </service> <!-- <receiver -->
+        <!-- android:name=".core.receiver.JpushNoticeBroadReceiver" -->
+        <!-- android:enabled="true"> -->
+        <!-- <intent-filter> -->
+        <!-- <action android:name="cn.jpush.android.intent.REGISTRATION" /> -->
+        <!-- <action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" /> -->
+        <!-- <action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" /> -->
+        <!-- <category android:name="${applicationId}" /> -->
+        <!-- </intent-filter> -->
+        <!-- </receiver> -->
+        <!-- Required since 3.0.7 -->
+        <!-- 新的 tag/alias 接口结果返回需要开发者配置一个自定的广播 -->
+        <!-- 3.3.0开始所有事件将通过该类回调 -->
+        <!-- 该广播需要继承 JPush 提供的 JPushMessageReceiver 类, 并如下新增一个 Intent-Filter -->
+        <receiver
+            android:name=".core.receiver.O2JPushMessageReceiver"
+            android:enabled="true"
+            android:exported="false">
+            <intent-filter>
+                <action android:name="cn.jpush.android.intent.RECEIVE_MESSAGE" />
+
+                <category android:name="${applicationId}" />
+            </intent-filter>
+        </receiver>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.fileProvider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths" />
+        </provider>
+        <provider
+            android:name="cn.jpush.android.service.DataProvider"
+            android:authorities="${applicationId}.DataProvider"
+            android:exported="true"
+            tools:replace="android:exported, android:authorities" />
+
+        <uses-library
+            android:name="org.apache.http.legacy"
+            android:required="false" />
+    </application>
+
+</manifest>

+ 373 - 0
app/src/main/java/com/bigkoo/convenientbanner/ConvenientBanner.java

@@ -0,0 +1,373 @@
+package com.bigkoo.convenientbanner;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build;
+import androidx.viewpager.widget.ViewPager;
+import androidx.viewpager.widget.ViewPager.PageTransformer;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.bigkoo.convenientbanner.adapter.CBPageAdapter;
+import com.bigkoo.convenientbanner.holder.CBViewHolderCreator;
+import com.bigkoo.convenientbanner.listener.CBPageChangeListener;
+import com.bigkoo.convenientbanner.listener.OnItemClickListener;
+import com.bigkoo.convenientbanner.transformer.ScaleYTransformer;
+import com.bigkoo.convenientbanner.view.CBLoopViewPager;
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 页面翻转控件,极方便的广告栏
+ * 支持无限循环,自动翻页,翻页特效
+ * @author Sai 支持自动翻页
+ */
+public class ConvenientBanner<T> extends LinearLayout {
+    private List<T> mDatas;
+    private int[] page_indicatorId;
+    private ArrayList<ImageView> mPointViews = new ArrayList<ImageView>();
+    private CBPageChangeListener pageChangeListener;
+    private ViewPager.OnPageChangeListener onPageChangeListener;
+    private CBPageAdapter pageAdapter;
+    private CBLoopViewPager viewPager;
+    private ViewPagerScroller scroller;
+    private ViewGroup loPageTurningPoint;
+    private long autoTurningTime;
+    private boolean turning;
+    private boolean canTurn = false;
+    private boolean manualPageable = true;
+    private boolean canLoop = true;
+    private boolean coverMode = false;
+    public enum PageIndicatorAlign{
+        ALIGN_PARENT_LEFT,ALIGN_PARENT_RIGHT,CENTER_HORIZONTAL
+    }
+    private AdSwitchTask adSwitchTask ;
+
+    public ConvenientBanner(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public ConvenientBanner(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.ConvenientBanner);
+        canLoop = a.getBoolean(R.styleable.ConvenientBanner_canLoop,true);
+        coverMode = a.getBoolean(R.styleable.ConvenientBanner_coverMode,false);
+        a.recycle();
+        init(context);
+    }
+
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+    public ConvenientBanner(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.ConvenientBanner);
+        canLoop = a.getBoolean(R.styleable.ConvenientBanner_canLoop,true);
+        coverMode = a.getBoolean(R.styleable.ConvenientBanner_coverMode,false);
+        a.recycle();
+        init(context);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public ConvenientBanner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.ConvenientBanner);
+        canLoop = a.getBoolean(R.styleable.ConvenientBanner_canLoop,true);
+        coverMode = a.getBoolean(R.styleable.ConvenientBanner_coverMode,false);
+        a.recycle();
+        init(context);
+    }
+
+    private void init(Context context) {
+        View hView;
+        if (coverMode){
+            hView = LayoutInflater.from(context).inflate(
+                    R.layout.include_viewpager_cover_mode, this, true);
+        }else {
+            hView = LayoutInflater.from(context).inflate(
+                    R.layout.include_viewpager, this, true);
+        }
+        viewPager = (CBLoopViewPager) hView.findViewById(R.id.cbLoopViewPager);
+        loPageTurningPoint = (ViewGroup) hView
+                .findViewById(R.id.loPageTurningPoint);
+        initViewPagerScroll();
+
+        adSwitchTask = new AdSwitchTask(this);
+
+        setCorner();
+    }
+
+    private void setCorner() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            ViewStyleSetter viewStyleSetter = new ViewStyleSetter(this);
+            viewStyleSetter.setRound(20);
+        }
+    }
+
+    static class AdSwitchTask implements Runnable {
+
+        private final WeakReference<ConvenientBanner> reference;
+
+        AdSwitchTask(ConvenientBanner convenientBanner) {
+            this.reference = new WeakReference<ConvenientBanner>(convenientBanner);
+        }
+
+        @Override
+        public void run() {
+            ConvenientBanner convenientBanner = reference.get();
+
+            if(convenientBanner != null){
+                if (convenientBanner.viewPager != null && convenientBanner.turning) {
+                    int page = convenientBanner.viewPager.getCurrentItem() + 1;
+                    convenientBanner.viewPager.setCurrentItem(page);
+                    convenientBanner.postDelayed(convenientBanner.adSwitchTask, convenientBanner.autoTurningTime);
+                }
+            }
+        }
+    }
+
+    public ConvenientBanner setPages(CBViewHolderCreator holderCreator,List<T> datas){
+        this.mDatas = datas;
+        if (coverMode){
+            viewPager.setPageTransformer(false,new ScaleYTransformer());
+        }
+        pageAdapter = new CBPageAdapter(holderCreator,mDatas);
+        viewPager.setAdapter(pageAdapter,canLoop);
+
+        if (page_indicatorId != null)
+            setPageIndicator(page_indicatorId);
+        return this;
+    }
+
+    /**
+     * 通知数据变化
+     * 如果只是增加数据建议使用 notifyDataSetAdd()
+     */
+    public void notifyDataSetChanged(){
+        viewPager.getAdapter().notifyDataSetChanged();
+        if (page_indicatorId != null)
+            setPageIndicator(page_indicatorId);
+    }
+
+    /**
+     * 设置底部指示器是否可见
+     *
+     * @param visible
+     */
+    public ConvenientBanner setPointViewVisible(boolean visible) {
+        loPageTurningPoint.setVisibility(visible ? View.VISIBLE : View.GONE);
+        return this;
+    }
+
+    /**
+     * 底部指示器资源图片
+     *
+     * @param page_indicatorId
+     */
+    public ConvenientBanner setPageIndicator(int[] page_indicatorId) {
+        loPageTurningPoint.removeAllViews();
+        mPointViews.clear();
+        this.page_indicatorId = page_indicatorId;
+        if(mDatas==null)return this;
+        for (int count = 0; count < mDatas.size(); count++) {
+            // 翻页指示的点
+            ImageView pointView = new ImageView(getContext());
+            pointView.setPadding(5, 0, 5, 0);
+            if (mPointViews.isEmpty())
+                pointView.setImageResource(page_indicatorId[1]);
+            else
+                pointView.setImageResource(page_indicatorId[0]);
+            mPointViews.add(pointView);
+            loPageTurningPoint.addView(pointView);
+        }
+        pageChangeListener = new CBPageChangeListener(mPointViews,
+                page_indicatorId);
+        viewPager.setOnPageChangeListener(pageChangeListener);
+        pageChangeListener.onPageSelected(viewPager.getRealItem());
+        if(onPageChangeListener != null)pageChangeListener.setOnPageChangeListener(onPageChangeListener);
+
+        return this;
+    }
+
+    /**
+     * 指示器的方向
+     * @param align  三个方向:居左 (RelativeLayout.ALIGN_PARENT_LEFT),居中 (RelativeLayout.CENTER_HORIZONTAL),居右 (RelativeLayout.ALIGN_PARENT_RIGHT)
+     * @return
+     */
+    public ConvenientBanner setPageIndicatorAlign(PageIndicatorAlign align) {
+        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) loPageTurningPoint.getLayoutParams();
+        layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, align == PageIndicatorAlign.ALIGN_PARENT_LEFT ? RelativeLayout.TRUE : 0);
+        layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, align == PageIndicatorAlign.ALIGN_PARENT_RIGHT ? RelativeLayout.TRUE : 0);
+        layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, align == PageIndicatorAlign.CENTER_HORIZONTAL ? RelativeLayout.TRUE : 0);
+        loPageTurningPoint.setLayoutParams(layoutParams);
+        return this;
+    }
+
+    /***
+     * 是否开启了翻页
+     * @return
+     */
+    public boolean isTurning() {
+        return turning;
+    }
+
+    /***
+     * 开始翻页
+     * @param autoTurningTime 自动翻页时间
+     * @return
+     */
+    public ConvenientBanner startTurning(long autoTurningTime) {
+        //如果是正在翻页的话先停掉
+        if(turning){
+            stopTurning();
+        }
+        //设置可以翻页并开启翻页
+        canTurn = true;
+        this.autoTurningTime = autoTurningTime;
+        turning = true;
+        postDelayed(adSwitchTask, autoTurningTime);
+        return this;
+    }
+
+    public void stopTurning() {
+        turning = false;
+        removeCallbacks(adSwitchTask);
+    }
+
+    /**
+     * 自定义翻页动画效果
+     *
+     * @param transformer
+     * @return
+     */
+    public ConvenientBanner setPageTransformer(PageTransformer transformer) {
+        viewPager.setPageTransformer(true, transformer);
+        return this;
+    }
+
+
+    /**
+     * 设置ViewPager的滑动速度
+     * */
+    private void initViewPagerScroll() {
+        try {
+            Field mScroller = null;
+            mScroller = ViewPager.class.getDeclaredField("mScroller");
+            mScroller.setAccessible(true);
+            scroller = new ViewPagerScroller(
+                    viewPager.getContext());
+            mScroller.set(viewPager, scroller);
+
+        } catch (NoSuchFieldException e) {
+            e.printStackTrace();
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public boolean isManualPageable() {
+        return viewPager.isCanScroll();
+    }
+
+    public void setManualPageable(boolean manualPageable) {
+        viewPager.setCanScroll(manualPageable);
+    }
+
+    //触碰控件的时候,翻页应该停止,离开的时候如果之前是开启了翻页的话则重新启动翻页
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+
+        int action = ev.getAction();
+        if (action == MotionEvent.ACTION_UP||action == MotionEvent.ACTION_CANCEL||action == MotionEvent.ACTION_OUTSIDE) {
+            // 开始翻页
+            if (canTurn)startTurning(autoTurningTime);
+        } else if (action == MotionEvent.ACTION_DOWN) {
+            // 停止翻页
+            if (canTurn)stopTurning();
+        }
+        return super.dispatchTouchEvent(ev);
+    }
+
+    //获取当前的页面index
+    public int getCurrentItem(){
+        if (viewPager!=null) {
+            return viewPager.getRealItem();
+        }
+        return -1;
+    }
+    //设置当前的页面index
+    public void setcurrentitem(int index){
+        if (viewPager!=null) {
+            viewPager.setCurrentItem(index);
+        }
+    }
+
+    public ViewPager.OnPageChangeListener getOnPageChangeListener() {
+        return onPageChangeListener;
+    }
+
+    /**
+     * 设置翻页监听器
+     * @param onPageChangeListener
+     * @return
+     */
+    public ConvenientBanner setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
+        this.onPageChangeListener = onPageChangeListener;
+        //如果有默认的监听器(即是使用了默认的翻页指示器)则把用户设置的依附到默认的上面,否则就直接设置
+        if(pageChangeListener != null)pageChangeListener.setOnPageChangeListener(onPageChangeListener);
+        else viewPager.setOnPageChangeListener(onPageChangeListener);
+        return this;
+    }
+
+    public boolean isCanLoop() {
+        return viewPager.isCanLoop();
+    }
+
+    /**
+     * 监听item点击
+     * @param onItemClickListener
+     */
+    public ConvenientBanner setOnItemClickListener(OnItemClickListener onItemClickListener) {
+        if (onItemClickListener == null) {
+            viewPager.setOnItemClickListener(null);
+            return this;
+        }
+        viewPager.setOnItemClickListener(onItemClickListener);
+        return this;
+    }
+
+    /**
+     * 设置ViewPager的滚动速度
+     * @param scrollDuration
+     */
+    public void setScrollDuration(int scrollDuration){
+        scroller.setScrollDuration(scrollDuration);
+    }
+
+    public int getScrollDuration() {
+        return scroller.getScrollDuration();
+    }
+
+    public CBLoopViewPager getViewPager() {
+        return viewPager;
+    }
+
+    public void setCanLoop(boolean canLoop) {
+        this.canLoop = canLoop;
+        viewPager.setCanLoop(canLoop);
+    }
+
+}

+ 72 - 0
app/src/main/java/com/bigkoo/convenientbanner/OvalViewOutlineProvider.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 jiajunhui<junhui_jia@163.com>
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+package com.bigkoo.convenientbanner;
+
+import android.annotation.TargetApi;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+/**
+ * Created by ShadowWalker on 2018/8/27.
+ */
+
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class OvalViewOutlineProvider extends ViewOutlineProvider {
+
+
+    public OvalViewOutlineProvider() {
+    }
+
+    @Override
+    public void getOutline(final View view, final Outline outline) {
+        Rect selfRect;
+        Rect rect = new Rect();
+        view.getGlobalVisibleRect(rect);
+        selfRect = getOvalRect(rect);
+        outline.setOval(selfRect);
+    }
+
+    /**
+     * 以矩形的中心点为圆心,较短的边为直径画圆
+     *
+     * @param rect
+     * @return
+     */
+    private Rect getOvalRect(Rect rect) {
+        int width = rect.right - rect.left;
+        int height = rect.bottom - rect.top;
+        int left, top, right, bottom;
+        int dW = width / 2;
+        int dH = height / 2;
+        if (width > height) {
+            left = dW - dH;
+            top = 0;
+            right = dW + dH;
+            bottom = dH * 2;
+        } else {
+            left = dH - dW;
+            top = 0;
+            right = dH + dW;
+            bottom = dW * 2;
+        }
+        return new Rect(left, top, right, bottom);
+    }
+
+}

+ 46 - 0
app/src/main/java/com/bigkoo/convenientbanner/RoundViewOutlineProvider.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 jiajunhui<junhui_jia@163.com>
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+package com.bigkoo.convenientbanner;
+
+import android.annotation.TargetApi;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+/**
+ * Created by ShadowWalker on 2018/8/27.
+ */
+
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+public class RoundViewOutlineProvider extends ViewOutlineProvider {
+
+    private float mRadius;//圆角弧度
+
+    public RoundViewOutlineProvider(float radius) {
+        this.mRadius = radius;
+    }
+
+    @Override
+    public void getOutline(View view, Outline outline) {
+        Rect rect = new Rect();
+        view.getGlobalVisibleRect(rect);//将view的区域保存在rect中
+        Rect selfRect = new Rect(0, 0, rect.right - rect.left, rect.bottom - rect.top);//绘制区域
+        outline.setRoundRect(selfRect, mRadius);
+    }
+}

+ 49 - 0
app/src/main/java/com/bigkoo/convenientbanner/ViewPagerScroller.java

@@ -0,0 +1,49 @@
+package com.bigkoo.convenientbanner;
+
+import android.content.Context;
+import android.view.animation.Interpolator;
+import android.widget.Scroller;
+
+public class ViewPagerScroller extends Scroller {
+	private int mScrollDuration = 800;// 滑动速度,值越大滑动越慢,滑动太快会使3d效果不明显
+	private boolean zero;
+
+	public ViewPagerScroller(Context context) {
+		super(context);
+	}
+
+	public ViewPagerScroller(Context context, Interpolator interpolator) {
+		super(context, interpolator);
+	}
+
+	public ViewPagerScroller(Context context, Interpolator interpolator,
+			boolean flywheel) {
+		super(context, interpolator, flywheel);
+	}
+
+	@Override
+	public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+		super.startScroll(startX, startY, dx, dy, zero ? 0 : mScrollDuration);
+	}
+
+	@Override
+	public void startScroll(int startX, int startY, int dx, int dy) {
+		super.startScroll(startX, startY, dx, dy, zero ? 0 : mScrollDuration);
+	}
+
+	public int getScrollDuration() {
+		return mScrollDuration;
+	}
+
+	public void setScrollDuration(int scrollDuration) {
+		this.mScrollDuration = scrollDuration;
+	}
+
+	public boolean isZero() {
+		return zero;
+	}
+
+	public void setZero(boolean zero) {
+		this.zero = zero;
+	}
+}

+ 53 - 0
app/src/main/java/com/bigkoo/convenientbanner/ViewStyleSetter.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 jiajunhui<junhui_jia@163.com>
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+package com.bigkoo.convenientbanner;
+
+import android.os.Build;
+import android.view.View;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * Created by ShadowWalker on 2018/8/27
+ * A util that can change a rectangle to round or oval
+ * it only support SDK_INT >= 21 since.
+ */
+
+public class ViewStyleSetter {
+
+    private View mView;
+
+    public ViewStyleSetter(View view) {
+        this.mView = view;
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public void setRound(float radius) {
+        this.mView.setClipToOutline(true);//用outline裁剪内容区域
+        this.mView.setOutlineProvider(new RoundViewOutlineProvider(radius));
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public void setOval() {
+        this.mView.setClipToOutline(true);//用outline裁剪内容区域
+        this.mView.setOutlineProvider(new OvalViewOutlineProvider());
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+    public void clearShapeStyle() {
+        this.mView.setClipToOutline(false);
+    }
+}

+ 107 - 0
app/src/main/java/com/bigkoo/convenientbanner/adapter/CBPageAdapter.java

@@ -0,0 +1,107 @@
+package com.bigkoo.convenientbanner.adapter;
+
+import androidx.viewpager.widget.PagerAdapter;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.bigkoo.convenientbanner.holder.CBViewHolderCreator;
+import com.bigkoo.convenientbanner.holder.Holder;
+import com.bigkoo.convenientbanner.view.CBLoopViewPager;
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R;
+
+import java.util.List;
+
+/**
+ * Created by Sai on 15/7/29.
+ */
+public class CBPageAdapter<T> extends PagerAdapter {
+    protected List<T> mDatas;
+    protected CBViewHolderCreator holderCreator;
+//    private View.OnClickListener onItemClickListener;
+    private boolean canLoop = true;
+    private CBLoopViewPager viewPager;
+    private final int MULTIPLE_COUNT = 300;
+
+    public int toRealPosition(int position) {
+        int realCount = getRealCount();
+        if (realCount == 0)
+            return 0;
+        int realPosition = position % realCount;
+        return realPosition;
+    }
+
+    @Override
+    public int getCount() {
+        return canLoop ? getRealCount()*MULTIPLE_COUNT : getRealCount();
+    }
+
+    public int getRealCount() {
+        return mDatas == null ? 0 : mDatas.size();
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int position) {
+        int realPosition = toRealPosition(position);
+
+        View view = getView(realPosition, null, container);
+//        if(onItemClickListener != null) view.setOnClickListener(onItemClickListener);
+        container.addView(view);
+        return view;
+    }
+
+    @Override
+    public void destroyItem(ViewGroup container, int position, Object object) {
+        View view = (View) object;
+        container.removeView(view);
+    }
+
+    @Override
+    public void finishUpdate(ViewGroup container) {
+        int position = viewPager.getCurrentItem();
+        if (position == 0) {
+            position = viewPager.getFristItem();
+        } else if (position == getCount() - 1) {
+            position = viewPager.getLastItem();
+        }
+        try {
+            viewPager.setCurrentItem(position, false);
+        }catch (IllegalStateException e){}
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object object) {
+        return view == object;
+    }
+
+    public void setCanLoop(boolean canLoop) {
+        this.canLoop = canLoop;
+    }
+
+    public void setViewPager(CBLoopViewPager viewPager) {
+        this.viewPager = viewPager;
+    }
+
+    public CBPageAdapter(CBViewHolderCreator holderCreator, List<T> datas) {
+        this.holderCreator = holderCreator;
+        this.mDatas = datas;
+    }
+
+    public View getView(int position, View view, ViewGroup container) {
+        Holder holder = null;
+        if (view == null) {
+            holder = (Holder) holderCreator.createHolder();
+            view = holder.createView(container.getContext());
+            view.setTag(R.id.cb_item_tag, holder);
+        } else {
+            holder = (Holder<T>) view.getTag(R.id.cb_item_tag);
+        }
+        if (mDatas != null && !mDatas.isEmpty())
+            holder.UpdateUI(container.getContext(), position, mDatas.get(position));
+        return view;
+    }
+
+//    public void setOnItemClickListener(View.OnClickListener onItemClickListener) {
+//        this.onItemClickListener = onItemClickListener;
+//    }
+}

+ 10 - 0
app/src/main/java/com/bigkoo/convenientbanner/holder/CBViewHolderCreator.java

@@ -0,0 +1,10 @@
+package com.bigkoo.convenientbanner.holder;
+/**
+ * @ClassName :  ViewHolderCreator 
+ * @Description : 
+ * @Author Sai
+ * @Date 2014年11月30日 下午3:29:34
+ */
+public interface CBViewHolderCreator<Holder> {
+	public Holder createHolder();
+}

+ 14 - 0
app/src/main/java/com/bigkoo/convenientbanner/holder/Holder.java

@@ -0,0 +1,14 @@
+package com.bigkoo.convenientbanner.holder;
+
+/**
+ * Created by Sai on 15/12/14.
+ * @param <T> 任何你指定的对象
+ */
+
+import android.content.Context;
+import android.view.View;
+
+public interface Holder<T>{
+    View createView(Context context);
+    void UpdateUI(Context context,int position,T data);
+}

+ 45 - 0
app/src/main/java/com/bigkoo/convenientbanner/listener/CBPageChangeListener.java

@@ -0,0 +1,45 @@
+package com.bigkoo.convenientbanner.listener;
+
+import androidx.viewpager.widget.ViewPager;
+import android.widget.ImageView;
+
+import java.util.ArrayList;
+
+/**
+ * Created by Sai on 15/7/29.
+ * 翻页指示器适配器
+ */
+public class CBPageChangeListener implements ViewPager.OnPageChangeListener {
+    private ArrayList<ImageView> pointViews;
+    private int[] page_indicatorId;
+    private ViewPager.OnPageChangeListener onPageChangeListener;
+    public CBPageChangeListener(ArrayList<ImageView> pointViews,int page_indicatorId[]){
+        this.pointViews=pointViews;
+        this.page_indicatorId = page_indicatorId;
+    }
+    @Override
+    public void onPageScrollStateChanged(int state) {
+        if(onPageChangeListener != null)onPageChangeListener.onPageScrollStateChanged(state);
+    }
+
+    @Override
+    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        if(onPageChangeListener != null)onPageChangeListener.onPageScrolled(position,positionOffset,positionOffsetPixels);
+    }
+
+    @Override
+    public void onPageSelected(int index) {
+        for (int i = 0; i < pointViews.size(); i++) {
+            pointViews.get(index).setImageResource(page_indicatorId[1]);
+            if (index != i) {
+                pointViews.get(i).setImageResource(page_indicatorId[0]);
+            }
+        }
+        if(onPageChangeListener != null)onPageChangeListener.onPageSelected(index);
+
+    }
+
+    public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
+        this.onPageChangeListener = onPageChangeListener;
+    }
+}

+ 8 - 0
app/src/main/java/com/bigkoo/convenientbanner/listener/OnItemClickListener.java

@@ -0,0 +1,8 @@
+package com.bigkoo.convenientbanner.listener;
+
+/**
+ * Created by Sai on 15/11/13.
+ */
+public interface OnItemClickListener {
+    public void onItemClick(int position);
+}

+ 26 - 0
app/src/main/java/com/bigkoo/convenientbanner/transformer/ScaleYTransformer.java

@@ -0,0 +1,26 @@
+package com.bigkoo.convenientbanner.transformer;
+
+import androidx.viewpager.widget.ViewPager;
+import android.view.View;
+
+/**
+ * Created by fancyLou on 2017/12/18.
+ * Copyright © 2017 O2. All rights reserved.
+ */
+
+public class ScaleYTransformer implements ViewPager.PageTransformer {
+    private static final float MIN_SCALE = 0.9F;
+    @Override
+    public void transformPage(View page, float position) {
+        if(position < -1){
+            page.setScaleY(MIN_SCALE);
+        }else if(position<= 1){
+            //
+            float scale = Math.max(MIN_SCALE,1 - Math.abs(position));
+            page.setScaleY(scale);
+
+        }else{
+            page.setScaleY(MIN_SCALE);
+        }
+    }
+}

+ 168 - 0
app/src/main/java/com/bigkoo/convenientbanner/view/CBLoopViewPager.java

@@ -0,0 +1,168 @@
+package com.bigkoo.convenientbanner.view;
+
+import android.content.Context;
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.bigkoo.convenientbanner.adapter.CBPageAdapter;
+import com.bigkoo.convenientbanner.listener.OnItemClickListener;
+
+
+public class CBLoopViewPager extends ViewPager {
+    OnPageChangeListener mOuterPageChangeListener;
+    private OnItemClickListener onItemClickListener;
+    private CBPageAdapter mAdapter;
+
+    private boolean isCanScroll = true;
+    private boolean canLoop = true;
+
+    public void setAdapter(PagerAdapter adapter, boolean canLoop) {
+        mAdapter = (CBPageAdapter) adapter;
+        mAdapter.setCanLoop(canLoop);
+        mAdapter.setViewPager(this);
+        super.setAdapter(mAdapter);
+
+        setCurrentItem(getFristItem(), false);
+    }
+
+    public int getFristItem() {
+        return canLoop ? mAdapter.getRealCount() : 0;
+    }
+
+    public int getLastItem() {
+        return mAdapter.getRealCount() - 1;
+    }
+
+    public boolean isCanScroll() {
+        return isCanScroll;
+    }
+
+    public void setCanScroll(boolean isCanScroll) {
+        this.isCanScroll = isCanScroll;
+    }
+
+    private float oldX = 0, newX = 0;
+    private static final float sens = 5;
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (isCanScroll) {
+            if (onItemClickListener != null) {
+                switch (ev.getAction()) {
+                    case MotionEvent.ACTION_DOWN:
+                        oldX = ev.getX();
+                        break;
+
+                    case MotionEvent.ACTION_UP:
+                        newX = ev.getX();
+                        if (Math.abs(oldX - newX) < sens) {
+                            onItemClickListener.onItemClick((getRealItem()));
+                        }
+                        oldX = 0;
+                        newX = 0;
+                        break;
+                }
+            }
+            return super.onTouchEvent(ev);
+        } else
+            return false;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (isCanScroll)
+            return super.onInterceptTouchEvent(ev);
+        else
+            return false;
+    }
+
+    public CBPageAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    public int getRealItem() {
+        return mAdapter != null ? mAdapter.toRealPosition(super.getCurrentItem()) : 0;
+    }
+
+    @Override
+    public void setOnPageChangeListener(OnPageChangeListener listener) {
+        mOuterPageChangeListener = listener;
+    }
+
+
+    public CBLoopViewPager(Context context) {
+        super(context);
+        init();
+    }
+
+    public CBLoopViewPager(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    private void init() {
+        super.setOnPageChangeListener(onPageChangeListener);
+    }
+
+    private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
+        private float mPreviousPosition = -1;
+
+        @Override
+        public void onPageSelected(int position) {
+            int realPosition = mAdapter.toRealPosition(position);
+            if (mPreviousPosition != realPosition) {
+                mPreviousPosition = realPosition;
+                if (mOuterPageChangeListener != null) {
+                    mOuterPageChangeListener.onPageSelected(realPosition);
+                }
+            }
+        }
+
+        @Override
+        public void onPageScrolled(int position, float positionOffset,
+                                   int positionOffsetPixels) {
+            int realPosition = position;
+
+            if (mOuterPageChangeListener != null) {
+                if (realPosition != mAdapter.getRealCount() - 1) {
+                    mOuterPageChangeListener.onPageScrolled(realPosition,
+                            positionOffset, positionOffsetPixels);
+                } else {
+                    if (positionOffset > .5) {
+                        mOuterPageChangeListener.onPageScrolled(0, 0, 0);
+                    } else {
+                        mOuterPageChangeListener.onPageScrolled(realPosition,
+                                0, 0);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {
+            if (mOuterPageChangeListener != null) {
+                mOuterPageChangeListener.onPageScrollStateChanged(state);
+            }
+        }
+    };
+
+    public boolean isCanLoop() {
+        return canLoop;
+    }
+
+    public void setCanLoop(boolean canLoop) {
+        this.canLoop = canLoop;
+        if (canLoop == false) {
+            setCurrentItem(getRealItem(), false);
+        }
+        if (mAdapter == null) return;
+        mAdapter.setCanLoop(canLoop);
+        mAdapter.notifyDataSetChanged();
+    }
+
+    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
+        this.onItemClickListener = onItemClickListener;
+    }
+}

+ 179 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/O2App.kt

@@ -0,0 +1,179 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.util.Log
+import androidx.multidex.MultiDex
+import androidx.multidex.MultiDexApplication
+import cn.jiguang.api.utils.JCollectionAuth
+import cn.jpush.android.api.JPushInterface
+import com.baidu.mapapi.SDKInitializer
+import com.zlw.main.recorderlib.RecordManager
+import io.realm.Realm
+import net.muliba.changeskin.FancySkinManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.skin.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.AndroidUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.LogSingletonService
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.O2MediaPlayerManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.edit
+
+
+/**
+ * Created by fancy on 2017/6/5.
+ */
+
+class O2App : MultiDexApplication() {
+
+
+    companion object {
+        lateinit var instance: O2App
+    }
+
+
+    /**
+     * baidu
+     */
+    val BAIDU_APP_ID: String by lazy {
+        getAppMeta("com.baidu.speech.APP_ID")
+    }
+    val BAIDU_APP_KEY: String by lazy {
+        getAppMeta("com.baidu.speech.API_KEY")
+    }
+    val BAIDU_SECRET_KEY: String by lazy {
+        getAppMeta("com.baidu.speech.SECRET_KEY")
+    }
+
+    private val notificationList: ArrayList<Int> = ArrayList()
+
+    override fun onCreate() {
+        super.onCreate()
+        instance = this
+        //sdk
+        O2SDKManager.instance().init(this)
+        //数据库
+        Realm.init(this)
+        //注册日志记录器
+        LogSingletonService.instance().registerApp(this)
+        //换肤插件
+        FancySkinManager.instance().withoutActivity(this)
+                .addSupportAttr("icon", IconChangeColorIconSkinAttr())
+                .addSupportAttr("iconCompleted", IconCompletedChangeColorIconSkinAttr())
+                .addSupportAttr("color", ColorChangeColorIconSkinAttr())
+                .addSupportAttr("drawableLeft", DrawableLeftRadioButtonSkinAttr())
+                .addSupportAttr("tabIndicatorColor", TabIndicatorColorTabLayoutSkinAttr())
+                .addSupportAttr("backgroundTint", BackgroundTintFloatingActionButtonSkinAttr())
+                .addSupportAttr("o2ButtonColor", O2ProgressButtonColorSkinAttr())
+
+        //播放器
+        O2MediaPlayerManager.instance().init(this)
+        //录音
+        RecordManager.getInstance().init(this, false)
+
+        initThirdParty()
+
+        Log.i("O2app", "O2app init.....................................................")
+    }
+
+
+
+    private fun initThirdParty() {
+        val isAgree = O2SDKManager.instance().prefs().getBoolean(O2.PRE_APP_PRIVACY_AGREE_KEY, false)
+        XLog.debug("init privacy isagree: $isAgree")
+        if ( isAgree ) {
+            try {
+
+                SDKInitializer.setAgreePrivacy(this, true)
+                SDKInitializer.initialize(applicationContext)
+                //极光推送
+                JCollectionAuth.setAuth(this, true)
+                JPushInterface.init(this)
+
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }else {
+            JCollectionAuth.setAuth(this, false)
+        }
+    }
+
+    fun agreePrivacyAndInitThirdParty(isAgree: Boolean) {
+        XLog.debug("set privacy isagree: $isAgree")
+        O2SDKManager.instance().prefs().edit {
+            putBoolean(O2.PRE_APP_PRIVACY_AGREE_KEY, isAgree)
+        }
+        if (isAgree) {
+            try {
+                SDKInitializer.setAgreePrivacy(this, true)
+                SDKInitializer.initialize(applicationContext)
+                //极光推送
+                JCollectionAuth.setAuth(this, true)
+                JPushInterface.init(this)
+
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }
+    }
+
+
+    override fun attachBaseContext(base: Context) {
+        super.attachBaseContext(base)
+        MultiDex.install(this)
+    }
+
+
+//  获取Application下的meta值
+    fun getAppMeta(metaName: String, default: String = "") : String {
+        return try {
+            if ("com.baidu.speech.APP_ID" == metaName) {
+                val appid = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData.getInt("com.baidu.speech.APP_ID")
+                ""+appid
+            }else {
+                packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData.getString(metaName, default)
+            }
+        }catch (e: Exception) {
+            Log.e("O2app", "", e)
+            default
+        }
+    }
+
+
+    private fun initJMessageAndJPush() {
+        if (BuildConfig.InnerServer) {
+            JCollectionAuth.setAuth(this, true)
+            JPushInterface.init(this)
+        } else {
+            val isAgree = O2SDKManager.instance().prefs().getBoolean(O2.PRE_APP_PRIVACY_AGREE_KEY, false)
+            XLog.debug("init jpush isagree: $isAgree")
+//            if (isAgree) {
+            if (!(AndroidUtils.isHuaweiChannel(this) && !isAgree )) {
+                JCollectionAuth.setAuth(this, true)
+                JPushInterface.init(this)
+            }
+        }
+    }
+
+    fun addNotification(nId: Int) {
+        notificationList.add(nId)
+        XLog.info("添加通知 $nId !!!!!!")
+    }
+
+    fun clearAllNotification() {
+        XLog.info("清除所有的通知!!!!!!")
+        try {
+            if (notificationList.isNotEmpty()) {
+                notificationList.forEach {
+                    JPushInterface.clearNotificationById(this, it)
+                    XLog.info("清除通知:$it")
+                }
+            }
+            // 清除通知 清除角标 这句写了好像角标不会再出现了
+            JPushInterface.setBadgeNumber(this, 0)
+        } catch (e: Exception) {
+            XLog.error("", e)
+        }
+    }
+
+
+}

+ 207 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/O2AppUpdateManager.kt

@@ -0,0 +1,207 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM
+
+import android.app.Activity
+import android.text.TextUtils
+import com.xiaomi.push.id
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.APIAddressHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.service.PackingClientAssembleSurfaceService
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.O2AppUpdateBean
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.O2AppUpdateBeanData
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.AndroidUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import java.lang.Exception
+import java.lang.RuntimeException
+import java.util.*
+
+
+class O2AppUpdateManager private constructor() {
+
+    companion object {
+        private var INSTANCE: O2AppUpdateManager? = null
+        fun instance(): O2AppUpdateManager {
+            if (INSTANCE == null) {
+                INSTANCE = O2AppUpdateManager()
+            }
+            return INSTANCE!!
+        }
+    }
+
+    //更新的json地址
+//    private val o2AppVersionJsonUrl = "https://sample.o2oa.net/app/app.json"
+    private val o2AppVersionJsonUrl = "https://app.o2oa.net/download/app.json"
+    private val client = OkHttpClient()
+
+
+    /**
+     * 官方app更新
+     */
+    fun checkUpdate(activity: Activity, call: O2AppUpdateCallback) {
+        val ranStr = getRandomStringOfLength(6)
+        Observable.just("$o2AppVersionJsonUrl?$ranStr").subscribeOn(Schedulers.io())
+                .map { url ->
+                    XLog.debug(url)
+                    val request = Request.Builder().url(url).build()
+                    try {
+                        val response = client.newCall(request).execute()
+                        val json = response.body()?.string()
+                        XLog.debug("json: $json")
+                        if (json != null && !TextUtils.isEmpty(json)) {
+                            val data = O2SDKManager.instance().gson.fromJson(json, O2AppUpdateBeanData::class.java)
+                            if (data?.android != null) {
+                                data.android
+                            }else {
+                                throw Exception("Json解析出错,请检查版本更新的json格式!")
+                            }
+                        }else {
+                            throw Exception("没有获取到版本更新的json文件内容!")
+                        }
+                    }catch (e: Exception) {
+                        XLog.error("", e)
+                        throw e
+                    }
+                }.observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        val vcode = AndroidUtils.getAppVersionCode(activity)
+                        try {
+                            XLog.debug("vcode: $vcode , build:${it?.buildNo}")
+                            if (it != null && it.buildNo.toInt() > vcode) {
+                                call.onUpdate(it)
+                            }else {
+                                call.onNoneUpdate("没有新版本!")
+                            }
+                        } catch (e: Exception) {
+                            call.onNoneUpdate(e.message ?: "")
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        call.onNoneUpdate(e?.message ?: "")
+                    }
+                }
+    }
+
+
+    /**
+     * 自助打包的应用更新
+     */
+    fun checkUpdateInner(activity: Activity, call: O2AppUpdateCallback) {
+        try {
+            val service = RetrofitClient.instance().packingClientService()
+            service.echo()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        checkUpdateVipInner(activity, call, service)
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        checkUpdateCenterInner(activity, call)
+                    }
+                }
+        } catch (e: Exception) {
+            XLog.error("", e)
+            checkUpdateCenterInner(activity, call)
+        }
+    }
+
+
+    private fun checkUpdateCenterInner(activity: Activity, call: O2AppUpdateCallback) {
+        try {
+            val url = O2SDKManager.instance().prefs().getString(O2.PRE_CENTER_URL_KEY, "") ?: ""
+            RetrofitClient.instance().api(url).androidPackLastAPk()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        val vcode = AndroidUtils.getAppVersionCode(activity)
+                        try {
+                            if (it != null && it.data != null && it.data.appVersionNo.isNotBlank() && it.data.appVersionNo.toInt() > vcode) {
+                                XLog.debug("vcode: $vcode , build:${it.data.appVersionNo}")
+                                val needBean = O2AppUpdateBean()
+                                needBean.content = ""
+                                needBean.versionName = it.data.appVersionName
+                                needBean.buildNo = it.data.appVersionNo
+                                needBean.downloadUrl = "${url}jaxrs/apppackanony/pack/info/file/download/${it.data.id}"
+                                call.onUpdate(needBean)
+                            }else {
+                                call.onNoneUpdate("没有新版本!")
+                            }
+                        } catch (e: Exception) {
+                            call.onNoneUpdate(e.message ?: "")
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        call.onNoneUpdate(e?.message ?: "")
+                    }
+                }
+        } catch (e: Exception) {
+            XLog.error("", e)
+            call.onNoneUpdate(e.message ?: "")
+        }
+    }
+
+    private fun checkUpdateVipInner(activity: Activity, call: O2AppUpdateCallback, service: PackingClientAssembleSurfaceService) {
+        service.androidPackLastAPk()
+            .subscribeOn(Schedulers.io())
+            .observeOn(AndroidSchedulers.mainThread())
+            .o2Subscribe {
+                onNext {
+                    val vcode = AndroidUtils.getAppVersionCode(activity)
+                    try {
+                        if (it != null && it.data != null && it.data.appVersionNo.isNotBlank() && it.data.appVersionNo.toInt() > vcode) {
+                            XLog.debug("vcode: $vcode , build:${it.data.appVersionNo}")
+                            val needBean = O2AppUpdateBean()
+                            needBean.content = ""
+                            needBean.versionName = it.data.appVersionName
+                            needBean.buildNo = it.data.appVersionNo
+                            needBean.downloadUrl = APIAddressHelper.instance().getPackingClientAppInnerDownloadUrl(it.data.id)
+                            call.onUpdate(needBean)
+                        }else {
+                            call.onNoneUpdate("没有新版本!")
+                        }
+                    } catch (e: Exception) {
+                        call.onNoneUpdate(e.message ?: "")
+                    }
+                }
+                onError { e, _ ->
+                    XLog.error("", e)
+                    call.onNoneUpdate(e?.message ?: "")
+                }
+            }
+    }
+
+
+
+    private val characters = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
+
+    fun getRandomStringOfLength(len: Int): String {
+        if (len < 0) {
+            throw RuntimeException("len 不能小于 1")
+        }
+        var ret = ""
+        val r = Random()
+        for (x in 0..len ) {
+            val index = r.nextInt(characters.size)
+            val rc = characters[index]
+            ret += rc.toString()
+        }
+        return ret
+    }
+
+
+}
+
+interface O2AppUpdateCallback {
+    fun onUpdate(version: O2AppUpdateBean)
+    fun onNoneUpdate(error: String)
+}

+ 132 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/VideoPlayerActivity.kt

@@ -0,0 +1,132 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app
+
+import android.app.Activity
+import android.content.pm.ActivityInfo
+import android.os.Build
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.shuyu.gsyvideoplayer.GSYVideoManager
+import com.shuyu.gsyvideoplayer.utils.OrientationUtils
+import kotlinx.android.synthetic.main.activity_video_player.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.AndroidUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.go
+import java.io.File
+
+class VideoPlayerActivity : AppCompatActivity() {
+
+
+
+    companion object {
+        const val VIDEO_URL_KEY = "VIDEO_URL_KEY"
+        const val VIDEO_TITLE_KEY = "VIDEO_TITLE_KEY"
+        fun startPlay(activity: Activity, videoUrl: String, title: String = "") {
+            val bundle = Bundle()
+            bundle.putString(VIDEO_URL_KEY, videoUrl)
+            bundle.putString(VIDEO_TITLE_KEY, title)
+            activity.go<VideoPlayerActivity>(bundle)
+        }
+    }
+
+    private var orientationUtils: OrientationUtils? = null
+    private var currentUrl: String? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            val lp = window.attributes
+            // 始终允许窗口延伸到屏幕短边上的刘海区域
+            lp.layoutInDisplayCutoutMode =
+                WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+            window.attributes = lp
+        }
+
+        setContentView(R.layout.activity_video_player)
+
+        // 透明状态栏
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            window.addFlags(
+                WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+        }
+        val statusBarHeight = AndroidUtils.getStatusBarHeight(this)
+
+        val barLayout = ll_video_player_bar.layoutParams as ConstraintLayout.LayoutParams
+        barLayout.topMargin = statusBarHeight
+        ll_video_player_bar.layoutParams = barLayout
+
+        val url = intent.getStringExtra(VIDEO_URL_KEY)
+        val title = intent.getStringExtra(VIDEO_TITLE_KEY) ?: ""
+        if (url != null && url.isNotEmpty()) {
+            init(url, title)
+        }else {
+            XToast.toastShort(this, getString(R.string.message_have_no_play_url))
+            finish()
+        }
+    }
+
+
+    private fun init(url: String, title: String) {
+        currentUrl = url
+        tv_video_player_title.text = title
+        btn_video_player_close.setOnClickListener { onBackPressed() }
+        btn_video_player_share.setOnClickListener { share() }
+
+        video_player.setUp(url, true, title)
+        //增加封面
+//        val imageView = ImageView(this)
+//        imageView.scaleType = ImageView.ScaleType.CENTER_CROP
+//        imageView.setImageResource(R.mipmap.my_app_top)
+//        video_player.thumbImageView = imageView
+        //增加title
+        video_player.titleTextView.visibility = View.GONE
+        //设置返回键
+        video_player.backButton.visibility = View.GONE
+        //设置旋转
+        orientationUtils = OrientationUtils(this, video_player)
+        //设置全屏按键功能,这是使用的是选择屏幕,而不是全屏
+        video_player.fullscreenButton.setOnClickListener(View.OnClickListener { orientationUtils?.resolveByClick() })
+        //是否可以滑动调整
+        video_player.setIsTouchWiget(true)
+        //设置返回按键功能
+//        video_player.backButton.setOnClickListener(View.OnClickListener { onBackPressed() })
+        video_player.startPlayLogic()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        video_player.onVideoResume()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        video_player.onVideoPause()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        GSYVideoManager.releaseAllVideos()
+        orientationUtils?.releaseListener()
+    }
+
+    override fun onBackPressed() {
+        //先返回正常状态
+        if (orientationUtils?.screenType == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+            video_player.fullscreenButton.performClick()
+            return
+        }
+        //释放所有
+        video_player.setVideoAllCallBack(null)
+        super.onBackPressed()
+
+    }
+
+    private fun share() {
+        currentUrl?.let {
+            AndroidUtils.shareFile(this, File(it))
+        }
+    }
+}

+ 447 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/FastCheckInManager.kt

@@ -0,0 +1,447 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.CountDownTimer
+import android.provider.Settings
+import android.text.TextUtils
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.baidu.location.BDAbstractLocationListener
+import com.baidu.location.BDLocation
+import com.baidu.location.LocationClient
+import com.baidu.location.LocationClientOption
+import com.baidu.mapapi.model.LatLng
+import com.baidu.mapapi.utils.DistanceUtil
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.service.AttendanceAssembleControlService
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.permission.PermissionRequester
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import java.util.*
+
+/**
+ * Created by fancyLou on 2023-04-14.
+ * Copyright © 2023 o2android. All rights reserved.
+ */
+class FastCheckInManager() {
+
+    //定位
+    private val mLocationClient: LocationClient by lazy { LocationClient(O2App.instance) }
+    private var myLocation: BDLocation? = null // 当前我的位置
+    private var checkInPosition: AttendanceV2WorkPlace? = null// 离的最近的工作地点位置
+    private var isInCheckInPositionRange = false
+
+    private val workplaceList = ArrayList<AttendanceV2WorkPlace>()
+//    private val recordList = ArrayList<AttendanceV2CheckItemData>()
+    private var nextCheckInRecord: AttendanceV2CheckItemData? = null // 需要打卡的数据
+    private var needCheckIn = false //是否可打卡
+    private var allowFieldWork = false // 是否允许外勤
+    private var onDutyFastCheckInEnable = false // 上班极速打卡
+    private var offDutyFastCheckInEnable = false // 下班极速打卡
+
+    // 倒计时timer 持续定位时间不超过5分钟
+    private var countDownTimer: CountDownTimer? = null
+
+    private val service: AttendanceAssembleControlService? = try {
+        RetrofitClient.instance().attendanceAssembleControlApi()
+    } catch (e: Exception) {
+        XLog.error("", e)
+        null
+    }
+
+
+    // 是否已经结束 防止 多次 start的情况
+    private var isChecking = false
+
+    /**
+     * 启动
+     */
+    fun start(activity: Activity) {
+        if (isChecking) {
+            XLog.info("还没有结束 不执行!!!!")
+            return
+        }
+        isChecking = true
+        // 首先加载配置文件
+        if (service != null) {
+            service.attendanceV2Config()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        val config = it.data
+                        if (config != null && (config.onDutyFastCheckInEnable || config.offDutyFastCheckInEnable)) {
+                            onDutyFastCheckInEnable= config.onDutyFastCheckInEnable
+                            offDutyFastCheckInEnable = config.offDutyFastCheckInEnable
+                            PermissionRequester(activity).request(Manifest.permission.ACCESS_FINE_LOCATION)
+                                .o2Subscribe {
+                                    onNext {  (granted, shouldShowRequestPermissionRationale, deniedPermissions) ->
+                                        if (!granted){
+                                            O2DialogSupport.openAlertDialog(activity, "需要定位权限, 去设置", {
+                                                isChecking = false
+                                                val packageUri = Uri.parse("package:${activity.packageName}")
+                                                activity.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageUri))
+                                            })
+                                        }else{
+                                            startAfterPermission()
+                                        }
+                                    }
+                                    onError { e, _ ->
+                                        XLog.error( "检查权限出错", e)
+                                    }
+                                }
+
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                    }
+                }
+        } else {
+            XLog.error("配置文件加载失败,没有极速打开功能!!!!!!!")
+        }
+    }
+
+    /**
+     * 结束
+     */
+    fun stopAll() {
+        stopLocation()
+        countDownTimer?.cancel()
+        XLog.info("极速打卡,全部结束了。。。。")
+        isChecking = false
+    }
+
+    private fun startAfterPermission() {
+        // 开始计算逻辑 进行定位和打卡
+        startLocation()
+        startTimer()
+        preCheckDataLoad()
+    }
+
+    /**
+     * 开始倒计时 5分钟后没有结果直接停止
+     */
+    private fun startTimer() {
+        // 由于CountDownTimer并不是准确计时,在onTick方法调用的时候,time会有1-10ms左右的误差,这会导致最后一秒不会调用onTick()
+        // 因此,设置间隔的时候,默认减去了10ms,从而减去误差。
+        // 经过以上的微调,最后一秒的显示时间会由于10ms延迟的积累,导致显示时间比1s长max*10ms的时间,其他时间的显示正常,总时间正常
+        if (countDownTimer == null) {
+            // 2分钟倒计时
+            countDownTimer = object : CountDownTimer((60 * 2) * 1000,   1000 - 10) {
+                override fun onTick(time: Long) {
+                    XLog.debug( "极速打卡倒计时, time = $time text = ${(time + 15) / 1000}"  )
+                }
+
+                override fun onFinish() {
+                    XLog.info("倒计时结束, 全部停止。")
+                    stopAll()
+                }
+            }
+        }
+        countDownTimer?.start()
+        XLog.info("开始倒计时")
+    }
+
+    /**
+     * 尝试打卡
+     */
+    private fun tryCheckIn() {
+        if (myLocation == null || TextUtils.isEmpty(myLocation?.addrStr)) {
+            XLog.error("没有定位到信息,可能是定位权限没开!!!")
+            return
+        }
+        if (needCheckIn && nextCheckInRecord != null && isInCheckInPositionRange && checkInPosition != null) {
+            // 上班打卡
+            if ((nextCheckInRecord?.checkInType == AttendanceV2RecordCheckInType.OnDuty.value && onDutyFastCheckInEnable)
+                ||
+                (nextCheckInRecord?.checkInType == AttendanceV2RecordCheckInType.OffDuty.value && offDutyFastCheckInEnable)) {
+                // 是否在打卡限制时间内
+                val dutyTime = nextCheckInRecord?.preDutyTime ?: ""
+                val preBeforeTime = nextCheckInRecord?.preDutyTimeBeforeLimit ?: ""
+                val preAfterTime = nextCheckInRecord?.preDutyTimeAfterLimit ?: ""
+                if (!checkLimitTime(nextCheckInRecord!!.checkInType, dutyTime, preBeforeTime, preAfterTime)) {
+                    XLog.error("不在限制时间内!!!!")
+                    return
+                }
+                postCheckIn(nextCheckInRecord!!, checkInPosition!!.id)
+            } else {
+                XLog.info("当前打卡类型:${nextCheckInRecord?.checkInType} 不允许极速打卡!")
+                stopAll()
+            }
+        }
+    }
+    // 是否有打卡时间限制
+    private fun checkLimitTime(checkInType: String,  dutyTime: String, preDutyTimeBeforeLimit: String, preDutyTimeAfterLimit: String): Boolean {
+        val now = Date()
+        val today = DateHelper.getDateTime("yyyy-MM-dd", now)
+        val dutyTimeDate = DateHelper.convertStringToDate("$today $dutyTime:00")
+        // 极速打卡开始时间
+        var fastCheckInBeforeLimit = if (checkInType == AttendanceV2RecordCheckInType.OnDuty.value) {
+            DateHelper.addMinute(dutyTimeDate, -60) //上班前一个小时
+        } else {
+            dutyTimeDate
+        }
+        // 极速打卡结束时间
+        var fastCheckInAfterLimit = if (checkInType == AttendanceV2RecordCheckInType.OnDuty.value) {
+            dutyTimeDate
+        } else {
+            DateHelper.addMinute(dutyTimeDate, 60) // 下班后1个小时
+        }
+
+        if (!TextUtils.isEmpty(preDutyTimeBeforeLimit)) { // 考勤组配置了开始打卡时间的
+            val beforeTime = DateHelper.convertStringToDate("$today $preDutyTimeBeforeLimit:00")
+            if (beforeTime != null && beforeTime.after(fastCheckInBeforeLimit)) {
+                fastCheckInBeforeLimit = beforeTime
+            }
+        }
+        if (!TextUtils.isEmpty(preDutyTimeAfterLimit)) {
+            val afterTime = DateHelper.convertStringToDate("$today $preDutyTimeAfterLimit:00");
+            if (afterTime != null && afterTime.before(fastCheckInAfterLimit)) {
+                fastCheckInAfterLimit = afterTime
+            }
+        }
+        // 当前时间在 限制时间内
+        XLog.info("打卡时间,$dutyTimeDate 极速打卡开始时间:$fastCheckInBeforeLimit 极速打卡结束时间: $fastCheckInAfterLimit")
+        if (now.after(fastCheckInBeforeLimit) && now.before(fastCheckInAfterLimit)) {
+            return true
+        }
+        return false
+    }
+
+    /**
+     * 开始定位 包含初始化sdk
+     */
+    private fun startLocation() {
+        LocationClient.setAgreePrivacy(true)
+        //定位
+        mLocationClient.registerLocationListener(object : BDAbstractLocationListener() {
+            override fun onReceiveLocation(location: BDLocation?) {
+                XLog.debug("onReceive locType:${location?.locType}, latitude:${location?.latitude}, longitude:${location?.longitude}")
+                if (location != null) {
+                    myLocation = location
+                    //计算
+                    calNearestWorkplace()
+                }
+            }
+        })
+        initBaiduLocation()
+        mLocationClient.start() // 开始定位
+    }
+    // 结束定位
+    private fun stopLocation() {
+        try {
+            mLocationClient.stop()
+        } catch (e: Exception) {
+            XLog.error("", e)
+        }
+    }
+
+    /**
+     * 获取预打卡请求数据
+     */
+    private fun preCheckDataLoad() {
+        if (service != null) {
+            service.attendanceV2PreCheck()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        preCheckData(it?.data)
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        XLog.info("没有数据, 全部停止。")
+                        stopAll()
+                    }
+                }
+        } else {
+            XLog.error("考勤服务模块加载失败!!!!!!!!")
+            stopAll()
+        }
+    }
+    /**
+     * 提交打卡信息
+     */
+    private var isPosting = false // 防止重复提交
+    private fun postCheckIn(record: AttendanceV2CheckItemData, workPlaceId: String) {
+        if (isPosting) {
+            XLog.info("正在提交中。。。。。。。")
+            return
+        }
+        isPosting = true
+        val body = AttendanceV2CheckInBody()
+        body.recordId = record.id
+        body.checkInType = record.checkInType
+        body.latitude = myLocation!!.latitude.toString()
+        body.longitude =  myLocation!!.longitude.toString()
+        body.recordAddress = myLocation!!.addrStr
+        body.workPlaceId = workPlaceId
+        body.fieldWork = false
+        body.signDescription = ""
+        body.sourceType = "FAST_CHECK" // 极速打卡
+        if (service != null) {
+            service.attendanceV2CheckIn(body)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        if (it != null && it.data != null) {
+                            XLog.info("极速打卡成功!")
+                            stopAll()
+                            // 发送通知
+                            val intent = Intent(O2.FAST_CHECK_IN_RECEIVER_ACTION)
+                            intent.putExtra(O2.FAST_CHECK_IN_RECORD_TIME_KEY, it.data.recordDate)
+                            LocalBroadcastManager.getInstance(O2App.instance.applicationContext).sendBroadcast(intent)
+                        } else {
+                            isPosting = false
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        isPosting = false
+                    }
+                }
+        } else {
+            isPosting = false
+        }
+    }
+
+    private fun preCheckData(data: AttendanceV2PreCheckData?) {
+        if (data == null) {
+            XLog.error("获取打卡信息错误,结束极速打卡!!!!")
+            stopAll()
+            return
+        }
+        needCheckIn = data.canCheckIn   //今天是否还需要打卡
+        allowFieldWork = data.allowFieldWork
+        workplaceList.clear()
+        workplaceList.addAll(data.workPlaceList ?: ArrayList())
+        // 检查是否在范围内
+        calNearestWorkplace()
+        if (needCheckIn) {
+            // 打卡记录
+            val checkItemList = data.checkItemList ?: ArrayList()
+            // 是否最后一条已经打卡过的数据
+            nextCheckInRecord = checkItemList.firstOrNull { element -> element.checkInResult == AttendanceV2RecordResult.PreCheckIn.value }
+            needCheckIn = nextCheckInRecord != null
+//            for ((index, item) in checkItemList.withIndex()) {
+//                var isRecord = false
+//                var recordTime = ""
+//                if (item.checkInResult != AttendanceV2RecordResult.PreCheckIn.value) {
+//                    isRecord = true
+//                    var signTime = item.recordDate
+//                    if (signTime.length > 16) {
+//                        signTime = signTime.substring(11, 16);
+//                    }
+//                    var status = O2App.instance.getString(R.string.attendance_v2_check_in_completed)
+//                    if (item.checkInResult != AttendanceV2RecordResult.Normal.value) {
+//                        status = item.resultText()
+//                    }
+//                    recordTime = "$status $signTime"
+//                }
+//                item.recordTime = recordTime
+//                item.isRecord = isRecord // 是否已经打卡
+//                item.checkInTypeString =  if(item.checkInType == AttendanceV2RecordCheckInType.OnDuty.value) { AttendanceV2RecordCheckInType.OnDuty.label }else{ AttendanceV2RecordCheckInType.OffDuty.label }
+//                var preDutyTime = item.preDutyTime
+//                if (TextUtils.isEmpty(item.shiftId)) {
+//                    preDutyTime = "" // 如果没有班次信息 表示 自由工时 或者 休息日 不显示 打卡时间
+//                }
+//                item.preDutyTime = preDutyTime
+//                // 处理是否是最后一个已经打卡的记录
+//                if (item.checkInResult != AttendanceV2RecordResult.PreCheckIn.value) {
+//                    if (index == checkItemList.size - 1) { // 最后一条
+//                        item.isLastRecord = true // 最后一条已经打卡的记录
+//                    } else {
+//                        val nextItem = checkItemList[index+1]
+//                        if (nextItem.checkInResult == AttendanceV2RecordResult.PreCheckIn.value) {
+//                            item.isLastRecord = true
+//                        }
+//                    }
+//                }
+//                checkItemList[index] = item
+//            }
+//            recordList.clear()
+//            recordList.addAll(checkItemList)
+        }
+
+        // 如果不能打卡了 就结束 固定班制 才有打卡时间 才能进行极速打卡
+        if (!needCheckIn && nextCheckInRecord?.groupCheckType != "1") {
+            XLog.info("今天无需打卡, 全部停止。")
+            stopAll()
+        } else {
+            tryCheckIn()
+        }
+    }
+
+
+    /**
+     * 检查是否进入打卡范围
+     */
+    private fun checkIsInWorkplace() {
+        XLog.info("检查是否进入打卡范围.....${checkInPosition?.placeName}, ${myLocation?.addrStr}")
+        if (checkInPosition != null && myLocation != null) {
+            val workplacePosition = LatLng(checkInPosition!!.latitude.toDouble(), checkInPosition!!.longitude.toDouble())
+            val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+            val distance = DistanceUtil.getDistance(position, workplacePosition)
+            XLog.info("distance:$distance")
+            isInCheckInPositionRange = distance < checkInPosition!!.errorRange
+            if (isInCheckInPositionRange) {
+                tryCheckIn()
+            }
+        }
+    }
+
+    /**
+     * 找到最近的打卡地点
+     */
+    private fun calNearestWorkplace() {
+        if ( myLocation!=null) {
+            if (workplaceList.isNotEmpty()) {
+                var minDistance: Double = -1.0
+                workplaceList.map {
+                    val p2 = LatLng(it.latitude.toDouble(), it.longitude.toDouble())
+                    val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+                    val distance = DistanceUtil.getDistance(position, p2)
+                    if (minDistance == -1.0) {
+                        minDistance = distance
+                        checkInPosition = it
+                    } else {
+                        if (minDistance > distance) {
+                            minDistance = distance
+                            checkInPosition = it
+                        }
+                    }
+                }
+                XLog.info("打卡的工作场所: ${checkInPosition?.placeName}")
+                checkIsInWorkplace()
+            }
+        }
+    }
+
+    private fun initBaiduLocation() {
+        val option = LocationClientOption()
+        option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
+        option.setCoorType("bd09ll")//百度坐标系 可选,默认gcj02,设置返回的定位结果坐标系
+        option.setScanSpan(5000)//5秒一次定位 可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
+        option.setIsNeedAddress(true)//可选,设置是否需要地址信息,默认不需要
+        option.isOpenGps = true//可选,默认false,设置是否使用gps
+        option.isLocationNotify = true//可选,默认false,设置是否当GPS有效时按照1S/1次频率输出GPS结果
+        option.setIsNeedLocationDescribe(true)//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
+        option.setIsNeedLocationPoiList(true)//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
+        option.setIgnoreKillProcess(false)//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
+        option.SetIgnoreCacheException(false)//可选,默认false,设置是否收集CRASH信息,默认收集
+        option.setEnableSimulateGps(false)//可选,默认false,设置是否需要过滤GPS仿真结果,默认需要
+        mLocationClient.locOption = option
+    }
+
+}

+ 307 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceAppealActivity.kt

@@ -0,0 +1,307 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal
+
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.View
+import android.view.WindowManager
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import kotlinx.android.synthetic.main.activity_attendance_appeal.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.enums.AttendanceStatus
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.IdentityJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.SystemDialogUtil
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.text2String
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+
+
+class AttendanceAppealActivity : BaseMVPActivity<AttendanceAppealContract.View, AttendanceAppealContract.Presenter>(),
+        AttendanceAppealContract.View, View.OnClickListener {
+    override var mPresenter: AttendanceAppealContract.Presenter = AttendanceAppealPresenter()
+
+    override fun layoutResId(): Int = R.layout.activity_attendance_appeal
+
+    companion object {
+        val ATTENDANCE_DETAIL_KEY = "attendance_detail_key"
+
+        fun startBundleData(info: AttendanceDetailInfoJson) : Bundle {
+            val bundle = Bundle()
+            bundle.putSerializable(ATTENDANCE_DETAIL_KEY, info)
+            return  bundle
+        }
+    }
+
+    val APPEAL_REASON = arrayOf("", "临时请假","出差","因公外出","其他")
+    val APPEAL_LEAVE_TYPE = arrayOf("", "带薪年休假","带薪病假","带薪福利假","扣薪事假","其他")
+    val identityList:ArrayList<IdentityJson> = ArrayList()
+    var isChooseIdentity = false
+    var chooseIdentity = ""
+
+    var info:AttendanceDetailInfoJson? = null
+    var selectReasonIndex = -1//选中的申诉原因
+    var selectLeaveTypeIndex = 0//请假类型选择 默认是空
+    override fun beforeSetContentView() {
+        super.beforeSetContentView()
+        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN)
+    }
+
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        if (intent.extras?.getSerializable(ATTENDANCE_DETAIL_KEY) == null) {
+            XToast.toastShort(this, "传入考勤信息为空,无法申诉!")
+            finish()
+            return
+        }
+        info =  intent.extras?.getSerializable(ATTENDANCE_DETAIL_KEY) as AttendanceDetailInfoJson
+
+        setupToolBar(getString(R.string.title_activity_attendance_appeal), true)
+
+        tv_attendance_appeal_name.text = info?.empName
+        tv_attendance_appeal_record_day.text = info?.recordDateString
+        tv_attendance_appeal_on_duty_time.text = info?.onDutyTime
+        tv_attendance_appeal_off_duty_time.text = info?.offDutyTime
+        var status = ""
+        status = when {
+            info?.isGetSelfHolidays?:false -> AttendanceStatus.HOLIDAY.label
+            info?.isLate?:false -> AttendanceStatus.LATE.label
+            info?.isAbsent?: false -> AttendanceStatus.ABSENT.label
+            info?.isAbnormalDuty ?: false -> AttendanceStatus.ABNORMALDUTY.label
+            info?.isLackOfTime ?: false -> AttendanceStatus.LACKOFTIME.label
+            else -> AttendanceStatus.NORMAL.label
+        }
+        tv_attendance_appeal_status.text = status
+
+        val adapter  = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, APPEAL_REASON)
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        spinner_attendance_appeal_reason.adapter = adapter
+        spinner_attendance_appeal_reason.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
+            override fun onNothingSelected(parent: AdapterView<*>?) {
+            }
+
+            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+                XLog.debug("选择了:"+ APPEAL_REASON[position])
+                selectReasonIndex = position
+                when(position) {
+                    0 -> selectReason0()
+                    1 -> selectReason1()
+                    2 -> selectReason2()
+                    3 -> selectReason3()
+                    4 -> selectReason4()
+                }
+            }
+        }
+        val typeAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, APPEAL_LEAVE_TYPE)
+        typeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        spinner_attendance_appeal_type.adapter = typeAdapter
+        spinner_attendance_appeal_type.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
+            override fun onNothingSelected(parent: AdapterView<*>?) {
+            }
+
+            override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+                XLog.debug("选择了:"+ APPEAL_LEAVE_TYPE[position])
+                selectLeaveTypeIndex = position
+            }
+        }
+
+        tv_bottom_button_first.setOnClickListener(this)
+        tv_bottom_button_second.setOnClickListener(this)
+        edit_attendance_appeal_time_day.setOnClickListener(this)
+        edit_attendance_appeal_start_time.setOnClickListener(this)
+        edit_attendance_appeal_end_time.setOnClickListener(this)
+
+        mPresenter.getMyIdentity()
+    }
+
+    override fun onClick(v: View?) {
+        when(v?.id) {
+            R.id.tv_bottom_button_first -> clickPositiveBtnAppeal()
+            R.id.tv_bottom_button_second -> finish()
+            R.id.edit_attendance_appeal_time_day -> SystemDialogUtil.getDateDialog("选择日期", edit_attendance_appeal_time_day)
+            R.id.edit_attendance_appeal_start_time -> SystemDialogUtil.getTimeDialog("选择开始时间", edit_attendance_appeal_start_time)
+            R.id.edit_attendance_appeal_end_time -> SystemDialogUtil.getTimeDialog("选择结束时间", edit_attendance_appeal_end_time)
+        }
+
+    }
+
+
+    override fun myIdentity(list: List<IdentityJson>) {
+        XLog.debug("identity:$list")
+        if (list!=null && !list.isEmpty()) {
+            isChooseIdentity = true
+            identityList.clear()
+            identityList.addAll(list)
+            val array = Array<String>(identityList.size, {index->
+                identityList[index].unitName
+            })
+            val identityAdapter = ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, array)
+            identityAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+            spinner_attendance_appeal_identity.adapter = identityAdapter
+            spinner_attendance_appeal_identity.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+                override fun onNothingSelected(parent: AdapterView<*>?) {
+                }
+
+                override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+                    chooseIdentity = identityList[position].distinguishedName
+                }
+            }
+            chooseIdentity = identityList[0].distinguishedName
+            linear_attendance_appeal_form_identity_line.visible()
+        }else {
+            linear_attendance_appeal_form_identity_line.gone()
+            isChooseIdentity = false
+            identityList.clear()
+        }
+    }
+
+    override fun submitAppeal(flag: Boolean) {
+        hideLoadingDialog()
+        if (flag) {
+            finish()
+        }else {
+            XToast.toastShort(this, "提交失败!")
+        }
+    }
+
+    private fun clickPositiveBtnAppeal() {
+        when(selectReasonIndex) {
+            -1, 0 -> XToast.toastShort(this@AttendanceAppealActivity, "请选择申诉原因!")
+            1 -> validateReason1()
+            2 -> validateReason2()
+            3 -> validateReason3()
+            4 -> validateReason4()
+        }
+    }
+
+
+    private fun selectReason0() {
+        linear_attendance_appeal_form_line7_type.gone()
+        linear_attendance_appeal_form_line8_address.gone()
+        linear_attendance_appeal_form_line9_startTime.gone()
+        linear_attendance_appeal_form_line_time_day.gone()
+        linear_attendance_appeal_form_line11_desc.gone()
+    }
+    private fun selectReason1() {
+        linear_attendance_appeal_form_line7_type.visible()
+        linear_attendance_appeal_form_line8_address.gone()
+        linear_attendance_appeal_form_line9_startTime.visible()
+        linear_attendance_appeal_form_line_time_day.visible()
+        linear_attendance_appeal_form_line11_desc.gone()
+    }
+    private fun selectReason2() {
+        linear_attendance_appeal_form_line7_type.gone()
+        linear_attendance_appeal_form_line8_address.visible()
+        linear_attendance_appeal_form_line9_startTime.visible()
+        linear_attendance_appeal_form_line_time_day.visible()
+        linear_attendance_appeal_form_line11_desc.gone()
+    }
+    private fun selectReason3() {
+        linear_attendance_appeal_form_line7_type.gone()
+        linear_attendance_appeal_form_line8_address.visible()
+        linear_attendance_appeal_form_line9_startTime.visible()
+        linear_attendance_appeal_form_line_time_day.visible()
+        linear_attendance_appeal_form_line11_desc.visible()
+    }
+    private fun selectReason4() {
+        linear_attendance_appeal_form_line7_type.gone()
+        linear_attendance_appeal_form_line8_address.gone()
+        linear_attendance_appeal_form_line9_startTime.gone()
+        linear_attendance_appeal_form_line_time_day.gone()
+        linear_attendance_appeal_form_line11_desc.visible()
+    }
+
+
+    private fun validateReason1() {
+        if (selectLeaveTypeIndex<0) {
+            XToast.toastShort(this, "请选择请假类型!")
+            return
+        }
+        validateTimeAndSubmit()
+    }
+
+
+    private fun validateReason2() {
+        val address = edit_attendance_appeal_address.text2String()
+        if (TextUtils.isEmpty(address)) {
+            XToast.toastShort(this, "请输入地址!")
+            return
+        }
+        validateTimeAndSubmit(address)
+    }
+
+
+    private fun validateReason3() {
+        val address = edit_attendance_appeal_address.text2String()
+        if (TextUtils.isEmpty(address)) {
+            XToast.toastShort(this, "请输入地址!")
+            return
+        }
+        val desc = edit_attendance_appeal_desc.text2String()
+        if (TextUtils.isEmpty(desc)) {
+            XToast.toastShort(this, "请输入事由!")
+            return
+        }
+        validateTimeAndSubmit(address, desc)
+    }
+
+    private fun validateReason4() {
+        val desc = edit_attendance_appeal_desc.text2String()
+        if (TextUtils.isEmpty(desc)) {
+            XToast.toastShort(this, "请输入事由!")
+            return
+        }
+        validateTimeAndSubmit(desc=desc)
+    }
+
+    private fun validateTimeAndSubmit(address:String = "", desc:String = "") {
+
+
+
+        if (TextUtils.isEmpty(address) && !TextUtils.isEmpty(desc)) {
+            submitAppealForm("", "", "", desc)
+        }else {
+            val timeDay = edit_attendance_appeal_time_day.text2String()
+            if (TextUtils.isEmpty(timeDay)) {
+                XToast.toastShort(this, "请选择日期")
+                return
+            }
+            val startTime = edit_attendance_appeal_start_time.text2String()
+            if (TextUtils.isEmpty(startTime)) {
+                XToast.toastShort(this, "请选择开始时间")
+                return
+            }
+            val endTime = edit_attendance_appeal_end_time.text2String()
+            if (TextUtils.isEmpty(endTime)) {
+                XToast.toastShort(this, "请选择结束时间")
+                return
+            }
+            submitAppealForm(address, timeDay+" "+startTime, timeDay+" "+endTime, desc)
+        }
+
+    }
+
+    private fun submitAppealForm(address: String, startTime: String, endTime: String, desc: String) {
+        XLog.debug("address:$address, starttime:$startTime, endtime:$endTime, desc:$desc, identity:$chooseIdentity")
+        if (isChooseIdentity && TextUtils.isEmpty(chooseIdentity)) {
+            XToast.toastShort(this, "没有选择身份")
+            return
+        }
+        info?.let {
+            it.appealReason = (APPEAL_REASON[selectReasonIndex])
+            it.selfHolidayType = (APPEAL_LEAVE_TYPE[selectLeaveTypeIndex])
+            it.startTime = (startTime)
+            it.endTime = (endTime)
+            it.address = (address)
+            it.appealDescription = (desc)
+            it.appealStatus = (0)
+            it.identity = chooseIdentity
+            showLoadingDialog()
+            mPresenter.submitAppeal(it)
+        }
+    }
+
+}

+ 19 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceAppealContract.kt

@@ -0,0 +1,19 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.IdentityJson
+
+
+object AttendanceAppealContract {
+    interface View : BaseView {
+        fun submitAppeal(flag: Boolean)
+        fun myIdentity(list: List<IdentityJson>)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun  submitAppeal(info: AttendanceDetailInfoJson)
+        fun getMyIdentity()
+    }
+}

+ 66 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceAppealPresenter.kt

@@ -0,0 +1,66 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.ApiResponse
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.IdentityJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceAppealPresenter : BasePresenterImpl<AttendanceAppealContract.View>(), AttendanceAppealContract.Presenter {
+
+    override fun getMyIdentity() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.getAppealAuditorType().flatMap { response ->
+                val info = response.data
+                XLog.debug("config:$info")
+                if (info != null && info.configValue == (O2.ATTENDANCE_SETTING_AUDITOR_TYPE_NEED_CHOOSE_IDENTITY)) {
+                    getOrganizationAssembleControlApi(mView?.getContext())?.identityListWithPerson(O2SDKManager.instance().distinguishedName)
+                } else {
+                    Observable.just(ApiResponse<List<IdentityJson>>())
+                }
+            }.subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            val identities = response.data
+                            if (identities != null && identities.size > 1) {//小于等于1个身份  都不需要显示选择身份的选项
+                                mView?.myIdentity(identities)
+                            } else {
+                                mView?.myIdentity(ArrayList<IdentityJson>())
+                            }
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.myIdentity(ArrayList<IdentityJson>())
+                        }
+                    }
+        }
+    }
+
+    override fun submitAppeal(info: AttendanceDetailInfoJson) {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.submitAppeal(info, info.id)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            XLog.debug("${response.data}")
+                            mView?.submitAppeal(true)
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.submitAppeal(false)
+                        }
+                    }
+        }
+    }
+
+
+}

+ 233 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceV2AppealActivity.kt

@@ -0,0 +1,233 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal
+
+import android.os.Bundle
+import android.text.SpannableString
+import android.text.TextUtils
+import android.text.style.UnderlineSpan
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.android.synthetic.main.activity_attendance_v2_appeal.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.process.job.OpenJobDialogFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.process.start.StartProcessDialogFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.MiscUtilK
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2oaColorScheme
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.DividerItemDecoration
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+
+class AttendanceV2AppealActivity : BaseMVPActivity<AttendanceV2AppealContract.View, AttendanceV2AppealContract.Presenter>(), AttendanceV2AppealContract.View {
+
+    private var page = 1 // 当前页码
+    private var isLoadingMore = false // 加载更多
+    private var isRefresh = false // 刷新
+    private var hasMore = true // 更多数据
+    private var appealList = ArrayList<AttendanceV2AppealInfo>()
+
+    private var appealEnable = false // 是否允许申请
+    private var processId = "" // 申请的流程id
+
+    override var mPresenter: AttendanceV2AppealContract.Presenter = AttendanceV2AppealPresenter()
+
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        setupToolBar("考勤异常数据", setupBackButton = true)
+        srl_att_v2_appeal_list.o2oaColorScheme()
+        srl_att_v2_appeal_list.setOnRefreshListener {
+            if (!isRefresh && !isLoadingMore) {
+                XLog.debug("下拉刷新。。。。")
+                refreshDataList()
+            }
+        }
+        MiscUtilK.swipeRefreshLayoutRun(srl_att_v2_appeal_list, this)
+        //
+        rvInit()
+
+        mPresenter.config()
+        loadAppealList()
+    }
+
+    override fun layoutResId(): Int = R.layout.activity_attendance_v2_appeal
+
+    private val adapter: CommonRecycleViewAdapter<AttendanceV2AppealInfo> by lazy {
+        object : CommonRecycleViewAdapter<AttendanceV2AppealInfo>(this, appealList, R.layout.item_attendance_v2_appeal_list) {
+            override fun convert(holder: CommonRecyclerViewHolder?, t: AttendanceV2AppealInfo?) {
+                val time = t?.recordDate ?: ""
+                val duty = t?.record?.checkInTypeText() ?: ""
+                val showText = if (TextUtils.isEmpty(duty)) {
+                    time
+                } else {
+                    "$time ($duty)"
+                }
+                var result = t?.record?.resultText() ?: ""
+                if (t?.record?.fieldWork == true) {
+                    result = getString(R.string.attendance_v2_fieldWork)
+                }
+                holder?.setText(R.id.tv_item_att_v2_appeal_recordDateString, showText)
+                    ?.setText(R.id.tv_item_att_v2_appeal_result, result)
+                    ?.setText(R.id.tv_item_att_v2_appeal_status, t?.statsText() ?: "")
+                val resultTv = holder?.getView<TextView>(R.id.tv_item_att_v2_appeal_result)
+                if (t?.record?.fieldWork == true) {
+                    resultTv?.background = ContextCompat.getDrawable(this@AttendanceV2AppealActivity, R.drawable.bg_attendance_fieldwork)
+                } else {
+                    when ( t?.record?.checkInResult) {
+                        AttendanceV2RecordResult.Early.value -> resultTv?.background = ContextCompat.getDrawable(this@AttendanceV2AppealActivity, R.drawable.bg_attendance_early)
+                        AttendanceV2RecordResult.Late.value -> resultTv?.background = ContextCompat.getDrawable(this@AttendanceV2AppealActivity, R.drawable.bg_attendance_late)
+                        AttendanceV2RecordResult.SeriousLate.value -> resultTv?.background = ContextCompat.getDrawable(this@AttendanceV2AppealActivity, R.drawable.bg_attendance_serilate)
+                        AttendanceV2RecordResult.NotSigned.value -> resultTv?.background = ContextCompat.getDrawable(this@AttendanceV2AppealActivity, R.drawable.bg_attendance_nosign)
+                        else -> resultTv?.background = ContextCompat.getDrawable(this@AttendanceV2AppealActivity, R.drawable.bg_attendance_normal)
+                    }
+                }
+                val processBtn = holder?.getView<LinearLayout>(R.id.ll_item_att_v2_appeal_process)
+                val processTv = holder?.getView<TextView>(R.id.tv_item_att_v2_appeal_process)
+                processBtn?.gone()
+                if (t?.status == AttendanceV2AppealStatus.StatusInit.value) {
+                    processBtn?.visible()
+                    val content = SpannableString(getString(R.string.attendance_v2_appeal_process))
+                    content.setSpan(UnderlineSpan(), 0, content.length, 0)
+                    processTv?.text = content
+                } else if (!TextUtils.isEmpty(t?.jobId)) {
+                    processBtn?.visible()
+                    val content = SpannableString(getString(R.string.attendance_v2_appeal_process_view))
+                    content.setSpan(UnderlineSpan(), 0, content.length, 0)
+                    processTv?.text = content
+                }
+            }
+        }
+    }
+
+    private fun rvInit() {
+        rv_att_v2_appeal_list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+        rv_att_v2_appeal_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+                super.onScrolled(recyclerView, dx, dy)
+                // 滚动到顶部才启用下拉刷新功能
+                val topRowVerticalPosition = rv_att_v2_appeal_list?.getChildAt(0)?.top ?: 0
+                srl_att_v2_appeal_list.isEnabled = topRowVerticalPosition >= 0
+                // 上拉加载
+                val lm = recyclerView.layoutManager as LinearLayoutManager
+                val lastPosition = lm.findLastVisibleItemPosition()
+                if (lastPosition == lm.itemCount - 1 && !isRefresh && !isLoadingMore && hasMore) {
+                    XLog.debug("加载更多。。。。")
+                    loadMoreDataList()
+                }
+            }
+        })
+        rv_att_v2_appeal_list.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST))
+        rv_att_v2_appeal_list.adapter = adapter
+        adapter.setOnItemClickListener { _, position ->
+            val item  = appealList[position]
+            if (item.status == AttendanceV2AppealStatus.StatusInit.value && appealEnable) {
+                startAppeal(item)
+            } else if (!TextUtils.isEmpty(item.jobId)) {
+                 openJob(item)
+            }
+        }
+    }
+
+    override fun appealList(list: List<AttendanceV2AppealInfo>) {
+        srl_att_v2_appeal_list.isRefreshing = false
+        if (page == 1) {
+            appealList.clear()
+        }
+        appealList.addAll(list)
+        if (list.size < O2.DEFAULT_PAGE_NUMBER) { // 没有更多数据
+            hasMore = false
+        }
+        isRefresh = false
+        isLoadingMore = false
+        adapter.notifyDataSetChanged()
+    }
+
+    override fun config(config: AttendanceV2Config?) {
+        if (config != null && config.appealEnable && !TextUtils.isEmpty(config.processId)) {
+            appealEnable = true
+            processId = config.processId
+        }
+        adapter.notifyDataSetChanged()
+    }
+
+    override fun checkAppealResult(value: Boolean, appealInfo: AttendanceV2AppealInfo) {
+        hideLoadingDialog()
+        if (value) {
+            XLog.debug("检查成功,开始启动流程")
+            val data = AttendanceV2AppealInfoToProcessData(appealInfo.id, appealInfo.record)
+            val dialog = StartProcessDialogFragment.createStartProcessDialog(processId, jsonData = O2SDKManager.instance().gson.toJson(data), dismiss = { result, jobId ->
+                XLog.debug("dialog 关闭了。。。。result: $result job: $jobId")
+                if (result) {
+                    mPresenter.appealStartedProcess(appealInfo.id, jobId?:"")
+                }
+            })
+            dialog.isCancelable = false
+            dialog.show(supportFragmentManager, StartProcessDialogFragment.TAG)
+        }
+    }
+
+    override fun appealStartedProcess(value: Boolean) {
+        XLog.info("启动流程后更新状态的结果: $value")
+        refreshDataList()
+    }
+
+    override fun appealResetStatus(value: Boolean) {
+        XLog.info("还原状态的结果: $value")
+        if (!value) {
+            XToast.toastShort("还原状态失败!")
+        }
+        refreshDataList()
+    }
+
+    private fun refreshDataList() {
+        isRefresh = true
+        page = 1
+        loadAppealList()
+    }
+    private fun loadMoreDataList() {
+        isLoadingMore = true
+        page++
+        loadAppealList()
+    }
+
+    private fun loadAppealList() {
+        mPresenter.myAppealListByPage(page)
+    }
+
+    /**
+     * 开始申诉
+     */
+    private fun startAppeal(item: AttendanceV2AppealInfo) {
+        if (!appealEnable || TextUtils.isEmpty(processId)) {
+            XToast.toastShort(this, getString(R.string.attendance_v2_can_not_appeal))
+            return
+        }
+        showLoadingDialog()
+        mPresenter.checkAppeal(item)
+    }
+    private fun confirmNeedResetStatus(info: AttendanceV2AppealInfo) {
+        O2DialogSupport.openConfirmDialog(this, "当前工作没有找到,是否要还原当前数据的状态?", listener =  { d ->
+            mPresenter.appealResetStatus(info.id)
+        })
+    }
+
+    private fun openJob(info: AttendanceV2AppealInfo) {
+        val dialog = OpenJobDialogFragment.openJobDialog(info.jobId) { isOpenWork ->
+            XLog.debug("open job dialog 关闭了")
+            if (!isOpenWork) {
+                confirmNeedResetStatus(info)
+            }
+        }
+        dialog.isCancelable = false
+        dialog.show(supportFragmentManager, OpenJobDialogFragment.TAG)
+    }
+
+}

+ 27 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceV2AppealContract.kt

@@ -0,0 +1,27 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2AppealInfo
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2Config
+
+/**
+ * Created by fancyLou on 2023-04-12.
+ * Copyright © 2023 o2android. All rights reserved.
+ */
+object AttendanceV2AppealContract {
+    interface View : BaseView {
+        fun config(config: AttendanceV2Config?)
+        fun appealList(list: List<AttendanceV2AppealInfo>)
+        fun checkAppealResult(value: Boolean, appealInfo: AttendanceV2AppealInfo)
+        fun appealStartedProcess(value: Boolean)
+        fun appealResetStatus(value: Boolean)
+    }
+    interface Presenter : BasePresenter<View> {
+        fun config()
+        fun myAppealListByPage(page: Int)
+        fun checkAppeal(appealInfo: AttendanceV2AppealInfo)
+        fun appealStartedProcess(id: String, jobId: String)
+        fun appealResetStatus(id: String)
+    }
+}

+ 127 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/appeal/AttendanceV2AppealPresenter.kt

@@ -0,0 +1,127 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.exception.O2ResponseException
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2AppealInfo
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2AppealPageListFilter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2StartProcessBody
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+/**
+ * Created by fancyLou on 2023-04-12.
+ * Copyright © 2023 o2android. All rights reserved.
+ */
+class AttendanceV2AppealPresenter : BasePresenterImpl<AttendanceV2AppealContract.View>(),
+    AttendanceV2AppealContract.Presenter {
+
+    override fun config() {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        service?.attendanceV2Config()?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
+            ?.o2Subscribe {
+                onNext {
+                    mView?.config(it.data)
+                }
+                onError { e, _ ->
+                    XLog.error("", e)
+                }
+            }
+    }
+
+    override fun appealStartedProcess(id: String, jobId: String) {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        if (service != null) {
+            val body = AttendanceV2StartProcessBody()
+            body.job = jobId
+            service.attendanceV2AppealStartProcess(id, body)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        mView?.appealStartedProcess(it?.data?.isValue == true)
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        mView?.appealStartedProcess(false)
+                    }
+                }
+        } else {
+            mView?.appealStartedProcess(false)
+        }
+    }
+
+    override fun checkAppeal(appealInfo: AttendanceV2AppealInfo) {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        if (service != null) {
+            service.attendanceV2CheckCanAppeal(appealInfo.id)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        mView?.checkAppealResult(it?.data?.isValue == true, appealInfo)
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        if (e is O2ResponseException) {
+                            XToast.toastShort(mView?.getContext(), e.message ?: "无法处理!")
+                        }
+                        mView?.checkAppealResult(false, appealInfo)
+                    }
+                }
+        } else {
+            mView?.checkAppealResult(false, appealInfo)
+        }
+    }
+
+    override fun myAppealListByPage(page: Int) {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        if (service != null) {
+            service.attendanceV2MyAppealListByPage(
+                page,
+                O2.DEFAULT_PAGE_NUMBER,
+                AttendanceV2AppealPageListFilter()
+            )
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        if (it != null && it.data != null) {
+                            mView?.appealList(it.data)
+                        } else {
+                            mView?.appealList(arrayListOf())
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        mView?.appealList(arrayListOf())
+                    }
+                }
+        } else {
+            mView?.appealList(arrayListOf())
+        }
+    }
+
+    override fun appealResetStatus(id: String) {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        if (service != null) {
+            service.attendanceV2AppealResetStatus(id)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        mView?.appealResetStatus(true)
+                    }
+                    onError { e, _ ->
+                        XLog.error("${e?.message}")
+                        mView?.appealResetStatus(false)
+                    }
+                }
+        } else {
+            mView?.appealResetStatus(false)
+        }
+    }
+}

+ 236 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/approval/AttendanceAppealApprovalActivity.kt

@@ -0,0 +1,236 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.approval
+
+
+import android.os.Bundle
+import androidx.recyclerview.widget.LinearLayoutManager
+import android.text.TextUtils
+import android.view.KeyEvent
+import android.view.View
+import android.widget.CheckBox
+import kotlinx.android.synthetic.main.activity_attendance_appeal_approval.*
+import kotlinx.android.synthetic.main.content_attendance_appeal_approval.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AppealInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.DividerItemDecoration
+
+
+class AttendanceAppealApprovalActivity : BaseMVPActivity<AttendanceAppealApprovalContract.View, AttendanceAppealApprovalContract.Presenter>(),
+        AttendanceAppealApprovalContract.View, View.OnClickListener {
+    override var mPresenter: AttendanceAppealApprovalContract.Presenter = AttendanceAppealApprovalPresenter()
+
+    override fun layoutResId(): Int = R.layout.activity_attendance_appeal_approval
+
+
+    var isLoading = false
+    var isRefresh = false
+    var lastId = ""//分页显示用
+    var isEdit = false
+    val mSelectedSet = HashSet<String>()
+
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        toolbar_attendance_appeal_approval_bar.title = ""
+        setSupportActionBar(toolbar_attendance_appeal_approval_bar)
+        tv_attendance_appeal_approval_top_title.text = getString(R.string.title_activity_attendance_appeal_approval)
+
+        layout_attendance_appeal_approval_refresh.setColorSchemeResources(R.color.z_color_refresh_scuba_blue,
+                R.color.z_color_refresh_red, R.color.z_color_refresh_purple, R.color.z_color_refresh_orange)
+        layout_attendance_appeal_approval_refresh.setOnRefreshListener {
+            if (!isLoading && !isRefresh) {
+                loadData(LoadType.REFRESH)
+            }
+        }
+        layout_attendance_appeal_approval_refresh.setOnLoadMoreListener {
+            if (!isLoading && !isRefresh) {
+                loadData(LoadType.LOADMORE)
+            }
+        }
+        recycler_attendance_appeal_approval_list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+        recycler_attendance_appeal_approval_list.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST))
+        recycler_attendance_appeal_approval_list.adapter = adapter
+
+        button_attendance_appeal_approval_back.setOnClickListener(this)
+        image_attendance_appeal_approval_edit.setOnClickListener(this)
+        image_attendance_appeal_approval_close.setOnClickListener(this)
+        button_attendance_appeal_approval_choose_all.setOnClickListener(this)
+        relative_attendance_appeal_approval_agree_button.setOnClickListener(this)
+        relative_attendance_appeal_approval_disagree_button.setOnClickListener(this)
+
+        loadData(LoadType.REFRESH)
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            if (isEdit) {
+                closeEditBar()
+                return true
+            }
+        }
+        return super.onKeyDown(keyCode, event)
+    }
+
+    override fun onClick(v: View?) {
+        when (v?.id) {
+            R.id.button_attendance_appeal_approval_back -> finish()
+            R.id.image_attendance_appeal_approval_edit -> if (itemList.size > 0) {
+                showEditBar()
+            }
+            R.id.image_attendance_appeal_approval_close -> closeEditBar()
+            R.id.button_attendance_appeal_approval_choose_all -> clickChooseAll(button_attendance_appeal_approval_choose_all.isChecked)
+            R.id.relative_attendance_appeal_approval_agree_button -> submitApproval(true)
+            R.id.relative_attendance_appeal_approval_disagree_button -> submitApproval(false)
+        }
+    }
+
+    override fun attendanceAppealList(list: List<AppealInfoJson>) {
+        linear_shimmer.gone()
+        if (isRefresh) {
+            itemList.clear()
+            itemList.addAll(list)
+        }else if (isLoading) {
+            itemList.addAll(list)
+        }
+        if (itemList.size>0) {
+            layout_attendance_appeal_approval_refresh.visible()
+            tv_no_data.gone()
+            lastId = itemList.last().id
+        }else {
+            layout_attendance_appeal_approval_refresh.gone()
+            tv_no_data.visible()
+            lastId = ""
+        }
+        adapter.notifyDataSetChanged()
+        hideLoadingDialog()
+        finishLoading()
+    }
+
+    override fun approvalAppealFinish() {
+        loadData(LoadType.REFRESH)
+    }
+
+    private fun submitApproval(isAgree: Boolean) {
+        if (mSelectedSet.isEmpty()) {
+            XToast.toastShort(this, "请先选择需要审批的数据!")
+            return
+        }
+        XLog.debug("submit set size:${mSelectedSet.size}")
+        closeEditBar()
+        showLoadingDialog()
+        mPresenter.approvalAppeal(mSelectedSet, isAgree)
+    }
+
+    private fun clickChooseAll(checked: Boolean) {
+        if (isEdit) {
+            if (checked) {
+                itemList.map { mSelectedSet.add(it.id) }
+            } else {
+                mSelectedSet.clear()
+            }
+            adapter.notifyDataSetChanged()
+        }
+    }
+
+    private fun showEditBar() {
+        isEdit = true
+        button_attendance_appeal_approval_back.gone()
+        image_attendance_appeal_approval_edit.gone()
+        button_attendance_appeal_approval_choose_all.visible()
+        image_attendance_appeal_approval_close.visible()
+        linear_attendance_appeal_approval_bottom_operation_bar.visible()
+        mSelectedSet.clear()
+        adapter.notifyDataSetChanged()
+    }
+
+    private fun closeEditBar() {
+        isEdit = false
+        button_attendance_appeal_approval_choose_all.gone()
+        button_attendance_appeal_approval_choose_all.isChecked = false
+        image_attendance_appeal_approval_close.gone()
+        linear_attendance_appeal_approval_bottom_operation_bar.gone()
+        button_attendance_appeal_approval_back.visible()
+        image_attendance_appeal_approval_edit.visible()
+        adapter.notifyDataSetChanged()
+    }
+
+    private fun loadData(refresh: LoadType) {
+        if (refresh.equals(LoadType.LOADMORE)) {
+            isLoading = true
+        } else {
+            lastId = ""
+            isRefresh = true
+        }
+        mPresenter.findAttendanceAppealInfoListByPage(lastId)
+    }
+
+    private fun finishLoading() {
+        if (isRefresh) {
+            layout_attendance_appeal_approval_refresh.isRefreshing = false
+            isRefresh = false
+        }
+        if (isLoading) {
+            layout_attendance_appeal_approval_refresh.setLoading(false)
+            isLoading = false
+        }
+    }
+
+
+    val itemList = ArrayList<AppealInfoJson>()
+    val adapter: CommonRecycleViewAdapter<AppealInfoJson> by lazy {
+        object : CommonRecycleViewAdapter<AppealInfoJson>(this, itemList, R.layout.item_attendance_appeal_approval_list) {
+            override fun convert(holder: CommonRecyclerViewHolder?, t: AppealInfoJson?) {
+                if (holder != null && t != null) {
+                    var address = t.address
+                    var desc = t.appealDescription
+                    if (!TextUtils.isEmpty(address)) {
+                        address = "地点:$address"
+                        desc = " , 事由:$desc"
+                    } else {
+                        address = ""
+                        desc = "事由:$desc"
+                    }
+                    var reason = t.appealReason
+                    if (!TextUtils.isEmpty(t.selfHolidayType)) {
+                        reason = "$reason (${t.selfHolidayType})"
+                    }
+
+                    holder.setText(R.id.tv_attendance_approval_list_person, t.empName)
+                            .setText(R.id.tv_attendance_approval_list_record_day, t.recordDateString)
+                            .setText(R.id.tv_attendance_approval_list_reason, reason)
+                            .setText(R.id.tv_attendance_approval_list_desc, address + desc)
+                    val checkbox = holder.getView<CheckBox>(R.id.checkbox_attendance_approval_list_choose)
+                    if (isEdit) {
+                        checkbox.visible()
+                        checkbox.isChecked = false
+                        checkbox.setOnClickListener {
+                            val ischeck = checkbox.isChecked
+                            toggleSelect(t.id, ischeck)
+                        }
+                        mSelectedSet.filter { it.equals(t.id) }.map { checkbox.isChecked = true }
+                    } else {
+                        checkbox.gone()
+                        checkbox.isChecked = false
+                    }
+                }
+            }
+        }
+    }
+
+    private fun toggleSelect(id: String, ischeck: Boolean) {
+        if (ischeck) {
+            mSelectedSet.add(id)
+        } else {
+            mSelectedSet.remove(id)
+        }
+    }
+
+    enum class LoadType {
+        REFRESH,
+        LOADMORE
+    }
+}

+ 18 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/approval/AttendanceAppealApprovalContract.kt

@@ -0,0 +1,18 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.approval
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AppealInfoJson
+
+
+object AttendanceAppealApprovalContract {
+    interface View : BaseView {
+        fun attendanceAppealList(list: List<AppealInfoJson>)
+        fun approvalAppealFinish()
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun approvalAppeal(mSelectedSet: HashSet<String>, agree: Boolean)
+        fun  findAttendanceAppealInfoListByPage(lastId: String)
+    }
+}

+ 70 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/approval/AttendanceAppealApprovalPresenter.kt

@@ -0,0 +1,70 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.approval
+
+import android.text.TextUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AppealApprovalFormJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AppealApprovalQueryFilterJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AppealInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceAppealApprovalPresenter : BasePresenterImpl<AttendanceAppealApprovalContract.View>(), AttendanceAppealApprovalContract.Presenter {
+
+    override fun approvalAppeal(mSelectedSet: HashSet<String>, agree: Boolean) {
+        XLog.debug("submit set size:${mSelectedSet.size}")
+        if (mSelectedSet.isEmpty()) {
+            mView?.approvalAppealFinish()
+            return
+        }
+
+        val form = AppealApprovalFormJson()
+        form.ids = ArrayList(mSelectedSet.map { it })
+        form.status = if (agree) "1" else "-1"
+        XLog.debug("$form")
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.approvalAppealInfo(form)
+                    .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            val back = response.data
+                            XLog.debug("$back")
+                            mView?.approvalAppealFinish()
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.approvalAppealFinish()
+                        }
+                    }
+        }
+    }
+
+    override fun findAttendanceAppealInfoListByPage(lastId: String) {
+        var variableId = lastId
+        if (TextUtils.isEmpty(lastId)) {
+            variableId = O2.FIRST_PAGE_TAG
+        }
+        val filter = AppealApprovalQueryFilterJson()
+        filter.status = "0"//未处理的
+        filter.processPerson1 = O2SDKManager.instance().distinguishedName
+        filter.yearString = DateHelper.nowByFormate("yyyy")
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.findAttendanceAppealInfoListByPage(variableId, O2.DEFAULT_PAGE_NUMBER, filter)
+                    .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            mView?.attendanceAppealList(response.data ?: ArrayList<AppealInfoJson>())
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.attendanceAppealList(ArrayList<AppealInfoJson>())
+                        }
+                    }
+        }
+    }
+}

+ 145 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/list/AttendanceListActivity.kt

@@ -0,0 +1,145 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.list
+
+
+import android.os.Bundle
+import androidx.recyclerview.widget.LinearLayoutManager
+import android.text.TextUtils
+import android.widget.ImageView
+import kotlinx.android.synthetic.main.content_attendance.*
+import kotlinx.android.synthetic.main.snippet_shimmer_content.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal.AttendanceAppealActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.enums.AttendanceStatus
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.go
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.inVisible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.DividerItemDecoration
+
+
+class AttendanceListActivity : BaseMVPActivity<AttendanceListContract.View, AttendanceListContract.Presenter>(), AttendanceListContract.View {
+    override var mPresenter: AttendanceListContract.Presenter = AttendanceListPresenter()
+
+    override fun layoutResId(): Int = R.layout.activity_attendance
+    var appealAble = false
+    val itemList = ArrayList<AttendanceDetailInfoJson>()
+    val adapter: CommonRecycleViewAdapter<AttendanceDetailInfoJson> by lazy {
+        object : CommonRecycleViewAdapter<AttendanceDetailInfoJson>(this, itemList, R.layout.item_attendance_detail_list) {
+            override fun convert(holder: CommonRecyclerViewHolder?, info: AttendanceDetailInfoJson?) {
+                if (holder!=null && info!=null) {
+                    val appealIcon = holder.getView<ImageView>(R.id.image_attendance_detail_list_item_appeal_icon)
+                    appealIcon?.inVisible()
+                    var appealStatus = ""
+                    if (appealAble) {
+                        when (info.appealStatus) {
+                            0 -> {
+                                if (!info.isGetSelfHolidays && (info.isAbsent || info.isLate || info.isAbnormalDuty || info.isLackOfTime)) {
+                                    appealIcon?.visible()
+                                }
+                            }
+                            9 -> appealStatus = "申诉通过"
+                            1 -> appealStatus = "申诉中"
+                            -1 -> appealStatus = "申诉未通过"
+                        }
+                    }
+                    var off_on_time = ""
+                    when {
+                        TextUtils.isEmpty(info.onDutyTime) && TextUtils.isEmpty(info.offDutyTime) -> off_on_time = ""
+                        TextUtils.isEmpty(info.onDutyTime) && !TextUtils.isEmpty(info.offDutyTime) -> off_on_time = info.offDutyTime
+                        !TextUtils.isEmpty(info.onDutyTime) && TextUtils.isEmpty(info.offDutyTime) -> off_on_time = info.onDutyTime
+                        !TextUtils.isEmpty(info.onDutyTime) && !TextUtils.isEmpty(info.offDutyTime) -> off_on_time = "${info.onDutyTime} - ${info.offDutyTime}"
+                    }
+                    var desc = "工作日"
+                    when {
+                        info.isHoliday -> desc = "节假日"
+                        info.isWeekend -> desc = "周末"
+                        info.isWorkday -> desc = "调休工作日"
+                    }
+                    var status = AttendanceStatus.NORMAL.label
+                    when {
+                        info.isGetSelfHolidays -> status = AttendanceStatus.HOLIDAY.label
+                        info.isLate -> status = AttendanceStatus.LATE.label
+                        info.isAbsent -> status = AttendanceStatus.ABSENT.label
+                        info.isAbnormalDuty -> status = AttendanceStatus.ABNORMALDUTY.label
+                        info.isLackOfTime -> status = AttendanceStatus.LACKOFTIME.label
+                    }
+                    if (!TextUtils.isEmpty(appealStatus)) {
+                        status = if (!TextUtils.isEmpty(info.appealProcessor)) {
+                            val procesor = if (info.appealProcessor.contains("@")) {
+                                info.appealProcessor.split("@")[0]
+                            } else {
+                                info.appealProcessor
+                            }
+                            "$status ($appealStatus, 审核人:${procesor})"
+                        }else {
+                            "$status ($appealStatus)"
+                        }
+                    }
+                    holder.setText(R.id.tv_attendance_detail_list_item_day, info.recordDateString)
+                            .setText(R.id.tv_attendance_detail_list_item_on_off_duty_time, off_on_time)
+                            .setText(R.id.tv_attendance_detail_list_item_desc, desc)
+                            .setText(R.id.tv_attendance_detail_list_item_status, status)
+                }
+            }
+        }
+    }
+
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        setupToolBar(getString(R.string.title_activity_attendance), true)
+        swipe_refresh_content.setColorSchemeResources(R.color.z_color_refresh_scuba_blue,
+                R.color.z_color_refresh_red, R.color.z_color_refresh_purple, R.color.z_color_refresh_orange)
+        swipe_refresh_content.setOnRefreshListener {
+            mPresenter.getAttendanceDetailList(DateHelper.nowByFormate("yyyy"), DateHelper.nowByFormate("MM"), O2SDKManager.instance().distinguishedName)
+        }
+        recycler_attendance_detail_list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
+        recycler_attendance_detail_list.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST))
+        recycler_attendance_detail_list.adapter = adapter
+        adapter.setOnItemClickListener { _, position ->
+            if (appealAble ) {
+                val detail = itemList[position]
+                if (detail.appealStatus == 0) {
+                    if (!detail.isGetSelfHolidays && (detail.isAbsent || detail.isLate || detail.isAbnormalDuty || detail.isLackOfTime)) {
+                        XLog.debug("start attendance appeal ")
+                        go<AttendanceAppealActivity>(AttendanceAppealActivity.startBundleData(detail))
+                    }
+                }
+            }
+        }
+
+    }
+
+    override fun onResume() {
+        super.onResume()
+        swipe_refresh_content.isRefreshing = true
+        mPresenter.getAppealableValue()
+        mPresenter.getAttendanceDetailList(DateHelper.nowByFormate("yyyy"), DateHelper.nowByFormate("MM"), O2SDKManager.instance().distinguishedName)
+    }
+
+    override fun attendanceDetailList(list: List<AttendanceDetailInfoJson>) {
+        swipe_refresh_content.isRefreshing = false
+        shimmer_snippet_content.gone()
+        itemList.clear()
+        if (list.isEmpty()) {
+            tv_attendance_detail_no_data.visible()
+            swipe_refresh_content.gone()
+        }else {
+            swipe_refresh_content.visible()
+            tv_attendance_detail_no_data.gone()
+            itemList.addAll(list)
+        }
+        adapter.notifyDataSetChanged()
+    }
+
+    override fun appealAble(flag: Boolean) {
+        appealAble = flag
+        adapter.notifyDataSetChanged()
+    }
+}

+ 18 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/list/AttendanceListContract.kt

@@ -0,0 +1,18 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.list
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+
+
+object AttendanceListContract {
+    interface View : BaseView {
+        fun attendanceDetailList(list: List<AttendanceDetailInfoJson>)
+        fun appealAble(flag: Boolean)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun getAppealableValue()
+        fun getAttendanceDetailList(year: String, month: String, distinguishedName: String)
+    }
+}

+ 64 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/list/AttendanceListPresenter.kt

@@ -0,0 +1,64 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.list
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailQueryFilterJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceListPresenter : BasePresenterImpl<AttendanceListContract.View>(), AttendanceListContract.Presenter {
+
+    override fun getAppealableValue() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.getAppealableValue()
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            val setting = response.data
+                             if (setting!=null && setting.configValue.equals(O2.ATTENDANCE_SETTING_APPEAL_ABLE_TRUE)) {
+                                mView?.appealAble(true)
+                            }else {
+                                mView?.appealAble(false)
+                            }
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.appealAble(false)
+                        }
+                    }
+        }
+    }
+
+    override fun getAttendanceDetailList(year: String, month: String, distinguishedName: String) {
+            XLog.debug("attendance chart query year$year, month:$month, person$distinguishedName")
+            val filter = AttendanceDetailQueryFilterJson()
+            filter.cycleYear = year
+            filter.cycleMonth = month
+            filter.key = "recordDateString"
+            filter.order = "desc"
+            filter.q_empName = distinguishedName
+            getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+                service.myAttendanceDetailChartList(filter)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            if (response.data == null){
+                                mView?.attendanceDetailList(ArrayList<AttendanceDetailInfoJson>())
+                            }else {
+                                mView?.attendanceDetailList(response.data)
+                            }
+
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.attendanceDetailList(ArrayList<AttendanceDetailInfoJson>())
+                        }
+                    }
+        }
+    }
+}

+ 16 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceChartContract.kt

@@ -0,0 +1,16 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+
+
+object AttendanceChartContract {
+    interface View : BaseView {
+        fun attendanceDetailList(list: List<AttendanceDetailInfoJson>)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun  getAttendanceDetailList(year: String, month: String, distinguishedName: String)
+    }
+}

+ 133 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceChartFragment.kt

@@ -0,0 +1,133 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import android.graphics.Color
+import android.graphics.Typeface
+import com.github.mikephil.charting.animation.Easing
+import com.github.mikephil.charting.data.Entry
+import com.github.mikephil.charting.data.PieData
+import com.github.mikephil.charting.data.PieDataSet
+import com.github.mikephil.charting.formatter.PercentFormatter
+import kotlinx.android.synthetic.main.fragment_attendance_chart.*
+import net.muliba.changeskin.FancySkinManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.enums.AttendanceStatus
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+
+
+class AttendanceChartFragment : BaseMVPViewPagerFragment<AttendanceChartContract.View, AttendanceChartContract.Presenter>(), AttendanceChartContract.View {
+
+    override var mPresenter: AttendanceChartContract.Presenter = AttendanceChartPresenter()
+
+    override fun layoutResId(): Int = R.layout.fragment_attendance_chart
+
+
+    val typeFace: Typeface by lazy { Typeface.createFromAsset(activity?.assets, "OpenSans-Regular.ttf") }
+
+    override fun initUI() {
+        //chart init
+        pie_attendance_chart.setUsePercentValues(true)
+        pie_attendance_chart.setDescription("")
+        pie_attendance_chart.setExtraOffsets(5f, 10f, 5f, 5f)
+        pie_attendance_chart.dragDecelerationFrictionCoef = 0.95f
+        pie_attendance_chart.setTransparentCircleColor(Color.WHITE)
+        pie_attendance_chart.setTransparentCircleAlpha(110)
+        pie_attendance_chart.holeRadius = 58f
+        pie_attendance_chart.transparentCircleRadius = 61f
+        pie_attendance_chart.setDrawCenterText(true)
+        pie_attendance_chart.centerText = getString(R.string.attendance_pie_title)
+        pie_attendance_chart.setCenterTextSize(14f)
+        pie_attendance_chart.rotationAngle = 0f
+        pie_attendance_chart.isRotationEnabled = true
+        pie_attendance_chart.isHighlightPerTapEnabled = true
+
+    }
+
+    override fun lazyLoad() {
+        mPresenter.getAttendanceDetailList(DateHelper.nowByFormate("yyyy"), DateHelper.nowByFormate("MM"), O2SDKManager.instance().distinguishedName)
+    }
+
+    override fun attendanceDetailList(list: List<AttendanceDetailInfoJson>) {
+        val map = HashMap<AttendanceStatus, Int>()
+        list.map {
+            when {
+                it.isGetSelfHolidays -> {
+                    var i = map[AttendanceStatus.HOLIDAY] ?: 0
+                    i++
+                    map.put(AttendanceStatus.HOLIDAY, i)
+                }
+                it.isLate -> {
+                    var i = map[AttendanceStatus.LATE] ?: 0
+                    i++
+                    map.put(AttendanceStatus.LATE, i)
+                }
+                it.isAbsent -> {
+                    var i = map[AttendanceStatus.ABSENT] ?: 0
+                    i++
+                    map.put(AttendanceStatus.ABSENT, i)
+                }
+                it.isAbnormalDuty -> {
+                    var i = map[AttendanceStatus.ABNORMALDUTY] ?: 0
+                    i++
+                    map.put(AttendanceStatus.ABNORMALDUTY, i)
+                }
+                it.isLackOfTime -> {
+                    var i = map[AttendanceStatus.LACKOFTIME] ?: 0
+                    i++
+                    map.put(AttendanceStatus.LACKOFTIME, i)
+                }
+                it.appealStatus == 9 -> {
+                    var i = map[AttendanceStatus.APPEAL] ?: 0
+                    i++
+                    map.put(AttendanceStatus.APPEAL, i)
+                }
+                else -> {
+                    var i = map[AttendanceStatus.NORMAL] ?: 0
+                    i++
+                    map.put(AttendanceStatus.NORMAL, i)
+                }
+            }
+
+        }
+        val yVals1 = ArrayList<Entry>()
+        val xVals = ArrayList<String>()
+        val colors = ArrayList<Int>()
+        var i = 0
+        var label = ""
+        if (map.isEmpty()) {
+            label = getString(R.string.attendance_no_data)
+        }else {
+            AttendanceStatus.values().filter { status->
+                val value = map[status]?:0
+                (value > 0)
+            }.map { status->
+                val value = map[status]?:0
+                val entry = Entry(value.toFloat(), i)
+                yVals1.add(entry)
+                xVals.add(status.label)
+                if (activity != null) {
+                    colors.add(FancySkinManager.instance().getColor(activity!!, status.color))
+                }
+                i++
+            }
+        }
+        val dataSet = PieDataSet(yVals1, label)
+        dataSet.sliceSpace = 2f
+        dataSet.selectionShift = 5f
+        dataSet.colors = colors
+        val pieData = PieData(xVals, dataSet)
+        pieData.setValueFormatter(PercentFormatter())
+        pieData.setValueTextSize(12f)
+        pieData.setValueTextColor(Color.WHITE)
+        pieData.setValueTypeface(typeFace)
+        pie_attendance_chart.data = pieData
+        pie_attendance_chart.highlightValues(null)
+        pie_attendance_chart.invalidate()
+        pie_attendance_chart.animateY(1400, Easing.EasingOption.EaseInOutQuad)
+        val legend = pie_attendance_chart.legend
+        legend.isWordWrapEnabled = true
+        legend.formSize = 12f
+    }
+}

+ 41 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceChartPresenter.kt

@@ -0,0 +1,41 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailQueryFilterJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceChartPresenter : BasePresenterImpl<AttendanceChartContract.View>(), AttendanceChartContract.Presenter {
+
+    override fun getAttendanceDetailList(year: String, month: String, distinguishedName: String) {
+        XLog.debug("attendance chart query year$year, month:$month, person$distinguishedName")
+        val filter = AttendanceDetailQueryFilterJson()
+        filter.cycleYear = year
+        filter.cycleMonth = month
+        filter.key = "recordDateString"
+        filter.order = "desc"
+        filter.q_empName = distinguishedName
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.myAttendanceDetailChartList(filter)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            if (response.data == null) {
+                                mView?.attendanceDetailList(ArrayList<AttendanceDetailInfoJson>())
+                            } else {
+                                mView?.attendanceDetailList(response.data)
+                            }
+
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.attendanceDetailList(ArrayList<AttendanceDetailInfoJson>())
+                        }
+                    }
+        }
+    }
+}

+ 27 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInContract.kt

@@ -0,0 +1,27 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendancePreCheckInFeature
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInWorkplaceInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileMyRecords
+
+
+object AttendanceCheckInContract {
+    interface View : BaseView {
+        fun myRecords(records: MobileMyRecords?)
+        fun workplaceList(list: List<MobileCheckInWorkplaceInfoJson>)
+        fun todayCheckInRecord(list: List<MobileCheckInJson>)
+        fun checkIn(result: Boolean)
+        fun previewCheckInData(data: AttendancePreCheckInFeature)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun mobilePreviewCheckIn()
+        fun listMyRecords()
+        fun findTodayCheckInRecord(person: String)
+        fun loadAllWorkplace()
+        fun checkIn(latitude: String, longitude: String, addrStr: String?, signDesc: String, signDate: String, signTime: String, id: String, checkType: String?, isExternal: Boolean, workAddress: String?)
+    }
+}

+ 331 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInFragment.kt

@@ -0,0 +1,331 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import android.os.Handler
+import android.os.Message
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import android.text.TextUtils
+import com.baidu.location.BDLocation
+import com.baidu.location.BDLocationListener
+import com.baidu.location.LocationClient
+import com.baidu.location.LocationClientOption
+import com.baidu.mapapi.map.*
+import com.baidu.mapapi.model.LatLng
+import com.baidu.mapapi.utils.DistanceUtil
+import kotlinx.android.synthetic.main.fragment_attendance_check_in.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
+import java.util.*
+import com.baidu.mapapi.map.MapStatusUpdateFactory
+import com.baidu.mapapi.map.MapStatus
+import com.xiaomi.push.it
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+
+
+class AttendanceCheckInFragment : BaseMVPViewPagerFragment<AttendanceCheckInContract.View, AttendanceCheckInContract.Presenter>(),
+        AttendanceCheckInContract.View, BDLocationListener {
+
+    override var mPresenter: AttendanceCheckInContract.Presenter = AttendanceCheckInPresenter()
+
+    override fun layoutResId(): Int = R.layout.fragment_attendance_check_in
+
+    private val recordList = ArrayList<MobileCheckInJson>()
+    private val workplaceList = ArrayList<MobileCheckInWorkplaceInfoJson>()
+    private val recordAdapter: CommonRecycleViewAdapter<MobileCheckInJson> by lazy {
+        object : CommonRecycleViewAdapter<MobileCheckInJson>(activity, recordList, R.layout.item_attendance_check_in_record_list) {
+            override fun convert(holder: CommonRecyclerViewHolder?, t: MobileCheckInJson?) {
+                if (!TextUtils.isEmpty(t?.checkin_type)) {
+                    holder?.setText(R.id.tv_item_attendance_check_in_type, t?.checkin_type)
+                }else {
+                    holder?.setText(R.id.tv_item_attendance_check_in_type, getString(R.string.attendance_check_in_time_label))
+                }
+                var time = if(!TextUtils.isEmpty(t?.signTime) && t?.signTime?.length ?: 0 > 5) {
+                     t?.signTime?.substring(0, 5) ?: ""
+                }else {
+                    ""
+                }
+                holder?.setText(R.id.tv_item_attendance_check_in_time, time)
+                        ?.setText(R.id.tv_item_attendance_check_in_location, t?.recordAddress)
+            }
+        }
+    }
+
+    private var mBaiduMap: BaiduMap? = null
+    private val mLocationClient: LocationClient by lazy { LocationClient(activity) }
+    private var workplaceCircle: Circle? = null //最近的工作地点打卡范围的蓝圈
+    private var myLocation: BDLocation? = null //当前我的位置
+    private var checkInPosition: MobileCheckInWorkplaceInfoJson? = null//离的最近的工作地点位置
+    private var isInCheckInPositionRange = false // 是否在考勤范围内
+    private var feature : MobileFeature? = null
+
+
+    val handler = Handler { msg ->
+        if (msg?.what == 1) {
+            val nowTime = DateHelper.nowByFormate("HH:mm:ss")
+            tv_attendance_check_in_time?.text = nowTime
+        }
+        return@Handler true
+    }
+    private val timerTask = object : TimerTask() {
+        override fun run() {
+            val message = Message()
+            message.what = 1
+            handler.sendMessage(message)
+        }
+    }
+    private val timer: Timer = Timer()
+
+
+    override fun initUI() {
+        LocationClient.setAgreePrivacy(true)
+        mBaiduMap = map_baidu_attendance_check_in.map
+        val builder = MapStatus.Builder()
+        builder.zoom(19.0f)
+        mBaiduMap?.setMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build()))
+        mBaiduMap?.mapType = BaiduMap.MAP_TYPE_NORMAL
+        // 开启定位图层
+        mBaiduMap?.isMyLocationEnabled = true
+        mLocationClient.registerLocationListener(this)
+        initBaiduLocation()
+        mLocationClient.start()
+
+        rv_attendance_check_in_record_list.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
+        rv_attendance_check_in_record_list.adapter = recordAdapter
+
+        timer.schedule(timerTask, 0, 1000)
+        ll_attendance_check_in_button.setOnClickListener { clickCheckIn() }
+    }
+
+    private fun clickCheckIn() {
+
+        if (myLocation!=null ){
+            tv_attendance_check_in_button_label.text = getString(R.string.attendance_check_in_knock_loading)
+            tv_attendance_check_in_time.gone()
+            val signDate = DateHelper.nowByFormate("yyyy-MM-dd")
+            val signTime = DateHelper.nowByFormate("HH:mm:ss")
+
+
+            if (!isInCheckInPositionRange) { // 外勤打卡
+                O2DialogSupport.openConfirmDialog(activity, "当前不在打卡范围内,你确定要进行外勤打卡?", { _ ->
+                    mPresenter.checkIn(myLocation!!.latitude.toString(), myLocation!!.longitude.toString(),
+                            myLocation!!.addrStr, "", signDate, signTime, "", this.feature?.checkinType, true, "")
+                })
+            }else {
+                mPresenter.checkIn(myLocation!!.latitude.toString(), myLocation!!.longitude.toString(),
+                        myLocation!!.addrStr, "", signDate, signTime, "", this.feature?.checkinType, false, checkInPosition?.placeName)
+            }
+
+        }else {
+            XToast.toastShort(activity!!, "没有获取到你的位置信息,请确认是否开启定位功能!")
+        }
+    }
+
+    override fun lazyLoad() {
+        mPresenter.findTodayCheckInRecord(O2SDKManager.instance().distinguishedName)
+        mPresenter.listMyRecords()
+        mPresenter.loadAllWorkplace()
+    }
+
+    override fun onDestroyView() {
+        timer.cancel()
+        timerTask.cancel()
+        try {
+            map_baidu_attendance_check_in.onDestroy()
+        } catch (e: Exception) {
+        }
+        super.onDestroyView()
+
+    }
+
+    override fun onDestroy() {
+        // 退出时销毁定位
+        mLocationClient.stop()
+        // 关闭定位图层
+        mBaiduMap?.isMyLocationEnabled = false
+        super.onDestroy()
+    }
+
+    override fun onReceiveLocation(location: BDLocation?) {
+        // 画定位点
+        XLog.debug("onReceive locType:${location?.locType}, latitude:${location?.latitude}, longitude:${location?.longitude}")
+        if (location != null) {
+            doAsync {
+                myLocation = location
+                // 构造定位数据
+                val locData = MyLocationData.Builder()
+                        .accuracy(location.radius)
+                        // 此处设置开发者获取到的方向信息,顺时针0-360
+                        .direction(location.direction)
+                        .latitude(location.latitude)
+                        .longitude(location.longitude).build()
+                // 设置定位数据
+                mBaiduMap?.setMyLocationData(locData)
+                // 设置定位图层的配置(定位模式,是否允许方向信息,用户自定义定位图标)
+                val bit: BitmapDescriptor = BitmapDescriptorFactory
+                        .fromResource(R.mipmap.task_red_point)
+                val config = MyLocationConfiguration(MyLocationConfiguration.LocationMode.FOLLOWING, true, bit)
+                mBaiduMap?.setMyLocationConfiguration(config)
+
+                uiThread {
+
+                    if (workplaceCircle == null) { //定位和获取workplace数据有可能有时间间隔
+                        drawCheckInWorkplaceCircle()
+                    }
+                    checkIsInWorkplace()
+                }
+            }
+
+
+        }
+    }
+
+
+//    override fun onConnectHotSpotMessage(p0: String?, p1: Int) {
+//        XLog.debug("onConnectHotSpotMessage, p0:$p0, p1:$p1")
+//    }
+
+    override fun workplaceList(list: List<MobileCheckInWorkplaceInfoJson>) {
+        // 画公司打卡范围蓝圈
+        workplaceList.clear()
+        workplaceList.addAll(list)
+        drawCheckInWorkplaceCircle()
+    }
+
+    override fun myRecords(records: MobileMyRecords?) {
+         if (records != null) {
+             this.feature = records.feature
+             if (this.feature?.signSeq ?: -1 > 0) {
+                 ll_attendance_check_in_button.visible()
+             }else {
+                 ll_attendance_check_in_button.gone()
+             }
+         }else {//兼容老版本 没有这个接口就开放打卡功能
+             this.feature = null
+             ll_attendance_check_in_button.visible()
+         }
+
+    }
+
+    override fun todayCheckInRecord(list: List<MobileCheckInJson>) {
+        XLog.debug("todayCheckInRecord  size:${list.size}")
+        recordList.clear()
+        recordList.addAll(list)
+        recordAdapter.notifyDataSetChanged()
+    }
+
+    override fun checkIn(result: Boolean) {
+        tv_attendance_check_in_button_label.text = getString(R.string.attendance_check_in_knock)
+        tv_attendance_check_in_time.visible()
+        if (result) {
+            XToast.toastShort(activity, "打卡成功!")
+        } else {
+            XToast.toastShort(activity, "打卡失败!")
+        }
+        mPresenter.findTodayCheckInRecord(O2SDKManager.instance().distinguishedName)
+        mPresenter.listMyRecords()
+    }
+
+    override fun previewCheckInData(data: AttendancePreCheckInFeature) {
+
+    }
+
+    /**
+     * 检查是否进入打卡范围
+     */
+    private fun checkIsInWorkplace() {
+        XLog.info("checkIsInWorkplace.....${checkInPosition?.placeName}, ${myLocation?.addrStr}")
+        if (checkInPosition != null && myLocation != null) {
+            val workplacePosition = LatLng(checkInPosition!!.latitude.toDouble(), checkInPosition!!.longitude.toDouble())
+            val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+            val distance = DistanceUtil.getDistance(position, workplacePosition)
+            XLog.info("distance:$distance")
+            if (distance < checkInPosition!!.errorRange) {
+                isInCheckInPositionRange = true
+                tv_attendance_check_in_alert_label?.text = "您已进入考勤范围:${checkInPosition?.placeName}"
+                ll_attendance_check_in_alert_banner?.visible()
+            } else {
+                isInCheckInPositionRange = false
+                ll_attendance_check_in_alert_banner?.gone()
+
+            }
+        }
+    }
+
+    /**
+     * 画出打卡范围的蓝圈
+     */
+    private fun drawCheckInWorkplaceCircle() {
+        if (myLocation != null) {
+            calNearestWorkplace()
+            if (checkInPosition != null && activity != null) {
+                val llCircle = LatLng(checkInPosition!!.latitude.toDouble(), checkInPosition!!.longitude.toDouble())
+                val ooCircle = CircleOptions()
+                        .center(llCircle)
+                        .fillColor(ContextCompat.getColor(activity!!, R.color.overlay_work_place))
+                        .stroke(Stroke(2, ContextCompat.getColor(activity!!, R.color.overlay_work_place_border)))
+                        .radius(checkInPosition!!.errorRange)
+                workplaceCircle = mBaiduMap?.addOverlay(ooCircle) as Circle
+                XLog.info("draw circle over...............zoom map..")
+                val mapStatus = mBaiduMap?.mapStatus
+                if (mapStatus != null) {
+                    val newStatus = MapStatus.Builder(mapStatus).zoom(17f).build()
+                    val update = MapStatusUpdateFactory.newMapStatus(newStatus)
+                    mBaiduMap?.animateMapStatus(update)
+                }
+            }
+        }
+    }
+
+    /**
+     * 找到最近的打卡地点
+     */
+    private fun calNearestWorkplace() {
+        if (workplaceList.isNotEmpty() && myLocation!=null) {
+            var minDistance: Double = -1.0
+            XLog.debug("calNearestWorkplace...................")
+            workplaceList.map {
+                val p2 = LatLng(it.latitude.toDouble(), it.longitude.toDouble())
+                val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+                val distance = DistanceUtil.getDistance(position, p2)
+                if (minDistance == -1.0) {
+                    minDistance = distance
+                    checkInPosition = it
+                } else {
+                    if (minDistance > distance) {
+                        minDistance = distance
+                        checkInPosition = it
+                    }
+                }
+            }
+            XLog.info("checkInposition:${checkInPosition?.placeName}")
+        }
+    }
+
+
+    private fun initBaiduLocation() {
+        val option = LocationClientOption()
+        option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
+        option.setCoorType("bd09ll")//百度坐标系 可选,默认gcj02,设置返回的定位结果坐标系
+        option.setScanSpan(5000)//5秒一次定位 可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
+        option.setIsNeedAddress(true)//可选,设置是否需要地址信息,默认不需要
+        option.isOpenGps = true//可选,默认false,设置是否使用gps
+        option.isLocationNotify = true//可选,默认false,设置是否当GPS有效时按照1S/1次频率输出GPS结果
+        option.setIsNeedLocationDescribe(true)//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
+        option.setIsNeedLocationPoiList(true)//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
+        option.setIgnoreKillProcess(false)//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
+        option.SetIgnoreCacheException(false)//可选,默认false,设置是否收集CRASH信息,默认收集
+        option.setEnableSimulateGps(false)//可选,默认false,设置是否需要过滤GPS仿真结果,默认需要
+        mLocationClient.locOption = option
+    }
+}

+ 521 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInNewFragment.kt

@@ -0,0 +1,521 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import android.graphics.drawable.GradientDrawable
+import android.os.Handler
+import android.os.Message
+import android.text.TextUtils
+import android.widget.EditText
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.GridLayoutManager
+import android.widget.ImageView
+import android.widget.TextView
+import com.baidu.location.*
+import com.baidu.mapapi.model.LatLng
+import com.baidu.mapapi.utils.DistanceUtil
+import com.xiaomi.push.it
+import kotlinx.android.synthetic.main.fragment_attendance_check_in.*
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_new.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.CheckButtonDoubleClick
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.GridLayoutItemDecoration
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+import org.jetbrains.anko.dip
+import java.util.*
+import kotlin.collections.ArrayList
+
+/**
+ * Created by fancyLou on 2020-07-17.
+ * Copyright © 2020 O2. All rights reserved.
+ */
+
+class AttendanceCheckInNewFragment : BaseMVPViewPagerFragment<AttendanceCheckInContract.View, AttendanceCheckInContract.Presenter>(),
+        AttendanceCheckInContract.View {
+    override var mPresenter: AttendanceCheckInContract.Presenter = AttendanceCheckInPresenter()
+    override fun layoutResId(): Int = R.layout.fragment_attendance_check_in_new
+
+
+    private val statusChecked = "已打卡"
+    private val statusUnCheck = "未打卡"
+
+    private val recordList = ArrayList<MobileScheduleInfo>()
+    private var lastRecord: MobileCheckInJson? = null
+    private val recordAdapter: CommonRecycleViewAdapter<MobileScheduleInfo> by lazy {
+        object : CommonRecycleViewAdapter<MobileScheduleInfo>(activity, recordList, R.layout.item_attendance_check_in_schdule_list) {
+            override fun convert(holder: CommonRecyclerViewHolder?, t: MobileScheduleInfo?) {
+                if (holder != null && t != null) {
+                    holder.setText(R.id.tv_item_attendance_check_in_schedule_list_type, t.checkinType)
+                            .setText(R.id.tv_item_attendance_check_in_schedule_list_time, t.signTime)
+                            .setText(R.id.tv_item_attendance_check_in_schedule_list_status, t.checkinStatus)
+//                    val image = holder.getView<ImageView>(R.id.image_item_attendance_check_in_schedule_list_enable_icon)
+                    val updateBtn = holder.getView<TextView>(R.id.tv_item_attendance_check_in_schedule_list_update_btn)
+                    updateBtn.gone()
+                    if (t.checkinStatus == statusChecked) {
+//                        image.visible()
+                        holder.setText(R.id.tv_item_attendance_check_in_schedule_list_status, t.checkinTime.substring(0, 5)+t.checkinStatus)
+                        if (lastRecord != null && lastRecord!!.id == t.recordId) {
+                            updateBtn.visible()
+                        }
+                    }else {
+//                        image.gone()
+                    }
+                }
+
+            }
+        }
+    }
+    private val workplaceList = ArrayList<MobileCheckInWorkplaceInfoJson>()
+
+    //定位
+    private val mLocationClient: LocationClient by lazy { LocationClient(activity) }
+    private var myLocation: BDLocation? = null //当前我的位置
+    private var checkInPosition: MobileCheckInWorkplaceInfoJson? = null//离的最近的工作地点位置
+    private var isInCheckInPositionRange = false
+    private var needCheckIn = false //是否需要打卡 根据打卡结果和打卡班次判断
+
+    // 新版打卡使用mobilePreviewCheckIn预打卡功能判断当前打开的情况
+    private var previewCheckInData: AttendancePreCheckInFeature? = null
+
+    //刷新打卡按钮的时间
+    private val handler = Handler { msg ->
+        if (msg.what == 1) {
+            val nowTime = DateHelper.nowByFormate("HH:mm:ss")
+            tv_attendance_check_in_new_now_time?.text = nowTime
+        }
+        return@Handler true
+    }
+    private val timerTask = object : TimerTask() {
+        override fun run() {
+            val message = Message()
+            message.what = 1
+            handler.sendMessage(message)
+        }
+    }
+    private val timer: Timer by lazy { Timer() }
+
+
+    override fun lazyLoad() {
+        mPresenter.listMyRecords()
+        mPresenter.loadAllWorkplace()
+    }
+
+    override fun initUI() {
+        LocationClient.setAgreePrivacy(true)
+        //定位
+        mLocationClient.registerLocationListener(object : BDAbstractLocationListener() {
+            override fun onReceiveLocation(location: BDLocation?) {
+                XLog.debug("onReceive locType:${location?.locType}, latitude:${location?.latitude}, longitude:${location?.longitude}")
+                if (location != null) {
+                    myLocation = location
+                    //计算
+                    calNearestWorkplace()
+                }
+            }
+        })
+        initBaiduLocation()
+        mLocationClient.start()
+
+        //打卡班次
+        rv_attendance_check_in_new_schedules.layoutManager = GridLayoutManager(activity, 2)
+        rv_attendance_check_in_new_schedules.addItemDecoration(GridLayoutItemDecoration(activity?.dip(10) ?: 10, activity?.dip(10) ?: 10, 2))
+        rv_attendance_check_in_new_schedules.adapter = recordAdapter
+        recordAdapter.setOnItemClickListener { view, position ->
+            val t = recordList[position]
+            if (t.checkinStatus == statusChecked) {
+                if (lastRecord != null && lastRecord!!.id == t.recordId) {
+                    updateCheckIn(t)
+                }
+            }
+        }
+
+        //打卡按钮
+        rl_attendance_check_in_new_knock_btn.setOnClickListener {
+            if (CheckButtonDoubleClick.isFastDoubleClick(R.id.rl_attendance_check_in_new_knock_btn)) {
+                return@setOnClickListener
+            }
+            loadPreview()
+        }
+        //时间
+        timer.schedule(timerTask, 0, 1000)
+    }
+
+    private fun loadPreview() {
+        // 20210915 暂时先不上 预打卡
+//        showLoadingDialog()
+//        mPresenter.mobilePreviewCheckIn()
+        checkInPostOld()
+    }
+
+
+    /**
+     * 没有previewCheckIn之前的打卡
+     */
+    private fun checkInPostOld() {
+        if (!needCheckIn) {
+            XToast.toastShort(activity, R.string.attendance_message_donot_need_check_in)
+            return
+        }
+
+        if (myLocation != null && !TextUtils.isEmpty(myLocation?.addrStr)){
+            tv_attendance_check_in_new_check_in.text = getString(R.string.attendance_check_in_knock_loading)
+            tv_attendance_check_in_new_now_time.gone()
+            val signDate = DateHelper.nowByFormate("yyyy-MM-dd")
+            val signTime = DateHelper.nowByFormate("HH:mm:ss")
+            val checkType = calCheckType()
+            if (!isInCheckInPositionRange) {
+                if (activity != null) {
+                    val dialog = O2DialogSupport.openCustomViewDialog(
+                        activity!!,
+                        getString(R.string.attendance_message_work_out),
+                        R.layout.dialog_name_modify
+                    ) { dialog ->
+                        val text = dialog.findViewById<EditText>(R.id.dialog_name_editText_id)
+                        if (TextUtils.isEmpty(text.text.toString())) {
+                            XToast.toastShort(activity!!, R.string.attendance_message_work_out_hint)
+                        } else {
+                            mPresenter.checkIn(
+                                myLocation!!.latitude.toString(),
+                                myLocation!!.longitude.toString(),
+                                myLocation!!.addrStr,
+                                text.text.toString(),
+                                signDate,
+                                signTime,
+                                "",
+                                checkType,
+                                true,
+                                ""
+                            )
+                        }
+                    }
+                    val text = dialog.findViewById<EditText>(R.id.dialog_name_editText_id)
+                    text.hint = getString(R.string.attendance_message_work_out_hint)
+                }
+//                O2DialogSupport.openConfirmDialog(activity, getString(R.string.attendance_message_work_out), { _ ->
+//
+//                })
+            }else {
+                mPresenter.checkIn(myLocation!!.latitude.toString(), myLocation!!.longitude.toString(),
+                    myLocation!!.addrStr, "", signDate, signTime, "", checkType, false, checkInPosition?.placeName)
+            }
+
+        }else {
+            XLog.error("没有定位到信息,可能是定位权限没开!!!")
+            XToast.toastShort(activity!!, R.string.attendance_message_no_location_info)
+        }
+    }
+
+    /**
+     * 使用previewCheckIn返回的数据进行打卡
+     */
+    private fun checkInPostNew() {
+        if (previewCheckInData == null) {
+            checkInPostOld()
+        } else {
+            if (previewCheckInData?.signSeq == 5) {
+                XToast.toastShort(activity, R.string.attendance_message_donot_need_check_in)
+                return
+            }
+            if (myLocation != null && !TextUtils.isEmpty(myLocation?.addrStr) ) {
+                if (previewCheckInData?.signSeq != 1) {
+                    O2DialogSupport.openConfirmDialog(activity, "确定要进行【${previewCheckInData?.getSignSeqString()}】打开?", {_ ->
+                        checkInPostNewConfirm()
+                    })
+                } else {
+                    checkInPostNewConfirm()
+                }
+            }else {
+                XLog.error("没有定位到信息,可能是定位权限没开!!!")
+                XToast.toastShort(activity!!, R.string.attendance_message_no_location_info)
+            }
+        }
+    }
+
+
+    private fun checkInPostNewConfirm() {
+        tv_attendance_check_in_new_check_in.text = getString(R.string.attendance_check_in_knock_loading)
+        tv_attendance_check_in_new_now_time.gone()
+        val signDate = DateHelper.nowByFormate("yyyy-MM-dd")
+        val signTime = DateHelper.nowByFormate("HH:mm:ss")
+        val checkType = if(TextUtils.isEmpty(previewCheckInData?.checkinType)) { calCheckType() } else { previewCheckInData?.checkinType }
+        if (!isInCheckInPositionRange) {
+            O2DialogSupport.openConfirmDialog(activity, getString(R.string.attendance_message_work_out), { _ ->
+                mPresenter.checkIn(myLocation!!.latitude.toString(), myLocation!!.longitude.toString(),
+                    myLocation!!.addrStr, "", signDate, signTime, "", checkType, true, "")
+            })
+        }else {
+            mPresenter.checkIn(myLocation!!.latitude.toString(), myLocation!!.longitude.toString(),
+                myLocation!!.addrStr, "", signDate, signTime, "", checkType, false, checkInPosition?.placeName)
+        }
+    }
+
+    /**
+     * 更新打卡
+     */
+    private fun updateCheckIn(info: MobileScheduleInfo) {
+        if (myLocation != null && !TextUtils.isEmpty(myLocation?.addrStr) ) {
+            val signDate = DateHelper.nowByFormate("yyyy-MM-dd")
+            val signTime = DateHelper.nowByFormate("HH:mm:ss")
+            if (isInCheckInPositionRange) {
+                O2DialogSupport.openConfirmDialog(activity, getString(R.string.attendance_message_update_check_in_record_confirm), { _ ->
+                    mPresenter.checkIn(
+                        myLocation!!.latitude.toString(),
+                        myLocation!!.longitude.toString(),
+                        myLocation!!.addrStr,
+                        "",
+                        signDate,
+                        signTime,
+                        info.recordId,
+                        info.checkinType,
+                        false,
+                        checkInPosition?.placeName
+                    )
+                })
+            } else {
+                if (activity != null) {
+                    val dialog = O2DialogSupport.openCustomViewDialog(
+                        activity!!,
+                        getString(R.string.attendance_message_work_out),
+                        R.layout.dialog_name_modify
+                    ) { dialog ->
+                        val text = dialog.findViewById<EditText>(R.id.dialog_name_editText_id)
+                        if (TextUtils.isEmpty(text.text.toString())) {
+                            XToast.toastShort(activity!!, R.string.attendance_message_work_out_hint)
+                        } else {
+                            mPresenter.checkIn(
+                                myLocation!!.latitude.toString(),
+                                myLocation!!.longitude.toString(),
+                                myLocation!!.addrStr,
+                                text.text.toString(),
+                                signDate,
+                                signTime,
+                                info.recordId,
+                                info.checkinType,
+                                true,
+                                ""
+                            )
+                        }
+                    }
+                    val text = dialog.findViewById<EditText>(R.id.dialog_name_editText_id)
+                    text.hint = getString(R.string.attendance_message_work_out_hint)
+                }
+            }
+        } else {
+            XLog.error("没有定位到信息,可能是定位权限没开!!!")
+            XToast.toastShort(activity!!, R.string.attendance_message_no_location_info)
+        }
+    }
+
+
+    /**
+     * 计算下次打什么卡
+     */
+    private fun calCheckType() : String {
+        val list = recordList.reversed()
+        XLog.debug(list.joinToString())
+        val newList = ArrayList<MobileScheduleInfo>()
+        for (info in list) {
+            if(info.checkinStatus == statusUnCheck) {
+                newList.add(info)
+            }else {
+                break
+            }
+        }
+        XLog.debug(newList.joinToString())
+        if (newList.isNotEmpty()) {
+            return newList.last().checkinType
+        }
+        return ""
+    }
+
+    override fun onDestroyView() {
+        timer.cancel()
+        timerTask.cancel()
+        super.onDestroyView()
+
+    }
+
+    override fun onDestroy() {
+        // 退出时销毁定位
+        mLocationClient.stop()
+        super.onDestroy()
+    }
+
+    override fun todayCheckInRecord(list: List<MobileCheckInJson>) {
+    }
+
+    override fun previewCheckInData(data: AttendancePreCheckInFeature) {
+        hideLoadingDialog()
+        if (data.signSeq > 0 && !TextUtils.isEmpty(data.checkinType)){
+            previewCheckInData = data
+            checkInPostNew()
+        } else {
+            checkInPostOld()
+        }
+    }
+
+    override fun myRecords(records: MobileMyRecords?) {
+        if (records != null) {
+            val rList = records.records//打卡结果
+            lastRecord = rList.lastOrNull()
+            recordList.clear()
+            var unCheckNumber = 0
+            val list = records.scheduleInfos.map {
+                val s = MobileScheduleInfo()
+                s.signSeq = it.signSeq
+                s.checkinType = it.checkinType
+                //是否已打卡
+                val record = rList.firstOrNull { re -> re.checkin_type == it.checkinType }
+                if (record != null) {
+                    s.checkinStatus =  statusChecked
+                    s.checkinTime = record.signTime
+                    s.recordId = record.id
+                    unCheckNumber = 0 //清零
+                }else{
+                    s.checkinStatus =  statusUnCheck
+                    unCheckNumber++
+                }
+                s.signDate = it.signDate
+                s.signTime = it.signTime
+                s
+            }
+            recordList.addAll(list)
+            recordAdapter.notifyDataSetChanged()
+            if (unCheckNumber > 0) {
+                needCheckIn = true
+                val draw = rl_attendance_check_in_new_knock_btn.background as? GradientDrawable
+                activity?.let {
+                    draw?.setColor(ContextCompat.getColor(it, R.color.z_color_primary))
+                }
+
+            }else {
+                needCheckIn = false
+                val draw = rl_attendance_check_in_new_knock_btn.background as? GradientDrawable
+                activity?.let {
+                    draw?.setColor(ContextCompat.getColor(it, R.color.disabled))
+                }
+            }
+        }else {
+            needCheckIn = false
+            val draw = rl_attendance_check_in_new_knock_btn.background as? GradientDrawable
+            activity?.let {
+                draw?.setColor(ContextCompat.getColor(it, R.color.disabled))
+            }
+            XToast.toastShort(activity, "没有获取到当前用户打卡的信息!")
+        }
+    }
+
+    override fun workplaceList(list: List<MobileCheckInWorkplaceInfoJson>) {
+        workplaceList.clear()
+        workplaceList.addAll(list)
+        //计算
+        calNearestWorkplace()
+    }
+
+
+    override fun checkIn(result: Boolean) {
+        tv_attendance_check_in_new_check_in?.setText(R.string.attendance_check_in_knock)
+        tv_attendance_check_in_new_now_time?.visible()
+        if (result) {
+            XToast.toastShort(activity, "打卡成功!")
+        } else {
+            XToast.toastShort(activity, "打卡失败!")
+        }
+        mPresenter.listMyRecords()
+    }
+
+//    override fun onReceiveLocation(location: BDLocation?) {
+//        // 刷新定位信息
+//        XLog.debug("onReceive locType:${location?.locType}, latitude:${location?.latitude}, longitude:${location?.longitude}")
+//        if (location != null) {
+//            myLocation = location
+//            //计算
+//            calNearestWorkplace()
+//        }
+//    }
+
+//    override fun onConnectHotSpotMessage(p0: String?, p1: Int) {
+//        XLog.debug("onConnectHotSpotMessage, p0:$p0, p1:$p1")
+//    }
+
+    /**
+     * 检查是否进入打卡范围
+     */
+    private fun checkIsInWorkplace() {
+        XLog.info("checkIsInWorkplace.....${checkInPosition?.placeName}, ${myLocation?.addrStr}")
+        if (checkInPosition != null && myLocation != null) {
+            val workplacePosition = LatLng(checkInPosition!!.latitude.toDouble(), checkInPosition!!.longitude.toDouble())
+            val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+            val distance = DistanceUtil.getDistance(position, workplacePosition)
+            XLog.info("distance:$distance")
+            if (distance < checkInPosition!!.errorRange) {
+                isInCheckInPositionRange = true
+                activity?.runOnUiThread {
+                    tv_attendance_check_in_new_workplace.text = checkInPosition?.placeName
+                    image_attendance_check_in_new_location_check_icon.setImageResource(R.mipmap.list_selected)
+                }
+            } else {
+                isInCheckInPositionRange = false
+                activity?.runOnUiThread {
+                    tv_attendance_check_in_new_workplace.text = myLocation?.addrStr
+                    image_attendance_check_in_new_location_check_icon.setImageResource(R.mipmap.icon_delete_people)
+                }
+            }
+        }
+    }
+
+    /**
+     * 找到最近的打卡地点
+     */
+    private fun calNearestWorkplace() {
+        if ( myLocation!=null) {
+            if (workplaceList.isNotEmpty()) {
+                var minDistance: Double = -1.0
+                XLog.debug("calNearestWorkplace...................")
+                workplaceList.map {
+                    val p2 = LatLng(it.latitude.toDouble(), it.longitude.toDouble())
+                    val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+                    val distance = DistanceUtil.getDistance(position, p2)
+                    if (minDistance == -1.0) {
+                        minDistance = distance
+                        checkInPosition = it
+                    } else {
+                        if (minDistance > distance) {
+                            minDistance = distance
+                            checkInPosition = it
+                        }
+                    }
+                }
+                XLog.info("checkInposition:${checkInPosition?.placeName}")
+                checkIsInWorkplace()
+            } else {
+                activity?.runOnUiThread {
+                    tv_attendance_check_in_new_workplace.text = myLocation?.addrStr
+                }
+            }
+        }
+    }
+
+    private fun initBaiduLocation() {
+        val option = LocationClientOption()
+        option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
+        option.setCoorType("bd09ll")//百度坐标系 可选,默认gcj02,设置返回的定位结果坐标系
+        option.setScanSpan(5000)//5秒一次定位 可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
+        option.setIsNeedAddress(true)//可选,设置是否需要地址信息,默认不需要
+        option.isOpenGps = true//可选,默认false,设置是否使用gps
+        option.isLocationNotify = true//可选,默认false,设置是否当GPS有效时按照1S/1次频率输出GPS结果
+        option.setIsNeedLocationDescribe(true)//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
+        option.setIsNeedLocationPoiList(true)//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
+        option.setIgnoreKillProcess(false)//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
+        option.SetIgnoreCacheException(false)//可选,默认false,设置是否收集CRASH信息,默认收集
+        option.setEnableSimulateGps(false)//可选,默认false,设置是否需要过滤GPS仿真结果,默认需要
+        mLocationClient.locOption = option
+    }
+}

+ 144 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInPresenter.kt

@@ -0,0 +1,144 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import android.text.TextUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendancePreCheckInFeature
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInQueryFilterJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.AndroidUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceCheckInPresenter : BasePresenterImpl<AttendanceCheckInContract.View>(), AttendanceCheckInContract.Presenter {
+
+
+    override fun mobilePreviewCheckIn() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.mobilePreviewCheckIn().subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        if (it.data != null) {
+                            mView?.previewCheckInData(it.data.feature)
+                        }else {
+                            mView?.previewCheckInData(AttendancePreCheckInFeature())
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        mView?.previewCheckInData(AttendancePreCheckInFeature())
+                    }
+                }
+        }
+    }
+
+
+    override fun listMyRecords() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.listMyRecords().subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext {
+                            if (it.data != null) {
+                                mView?.myRecords(it.data)
+                            } else {
+                                mView?.myRecords(null)
+                            }
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.myRecords(null)
+                        }
+                    }
+        }
+    }
+
+    override fun findTodayCheckInRecord(person: String) {
+        val queryBean = MobileCheckInQueryFilterJson()
+        queryBean.empName = person
+        queryBean.startDate = DateHelper.nowByFormate("yyyy-MM-dd")
+        val json = O2SDKManager.instance().gson.toJson(queryBean)
+        val body = RequestBody.create(MediaType.parse("text/json"), json)
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.findAttendanceDetailMobileByPage(body, 1, 100)
+                    .subscribeOn(Schedulers.io())
+                    .flatMap { response ->
+                        val list = response.data
+                        val retList = list?.sortedByDescending { it.signTime } ?: ArrayList()
+                        Observable.just(retList)
+                    }
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { list ->
+                            mView?.todayCheckInRecord(list)
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.todayCheckInRecord(ArrayList())
+                        }
+                    }
+        }
+    }
+
+    override fun loadAllWorkplace() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.findAllWorkplace()
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            mView?.workplaceList(response.data)
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.workplaceList(ArrayList())
+                        }
+                    }
+        }
+    }
+
+    override fun checkIn(latitude: String, longitude: String, addrStr: String?, signDesc: String,
+                         signDate: String, signTime: String, id: String, checkType: String?,
+                         isExternal: Boolean, workAddress: String?) {
+        val form = MobileCheckInJson()
+        if (!TextUtils.isEmpty(id)) {
+            form.id = id
+        }
+        form.description = signDesc
+        form.latitude = latitude
+        form.longitude = longitude
+        form.recordDateString = signDate
+        form.signTime = signTime
+        form.optMachineType = AndroidUtils.getDeviceBrand() + "-" + AndroidUtils.getDeviceModelNumber()
+        form.optSystemName = O2.DEVICE_TYPE
+        form.recordAddress = addrStr ?: ""
+        form.checkin_type = checkType
+        form.isExternal = isExternal
+        form.workAddress = workAddress
+        val json = O2SDKManager.instance().gson.toJson(form)
+        val body = RequestBody.create(MediaType.parse("text/json"), json)
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.attendanceDetailCheckIn(body)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { _ ->
+                            mView?.checkIn(true)
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.checkIn(false)
+                        }
+                    }
+        }
+    }
+}

+ 18 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInV2Contract.kt

@@ -0,0 +1,18 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.*
+
+
+object AttendanceCheckInV2Contract {
+    interface View : BaseView {
+        fun preCheckData(data: AttendanceV2PreCheckData?)
+        fun checkInPostResponse(result: Boolean, message: String?)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun preCheckDataLoad()
+        fun checkInPost(body: AttendanceV2CheckInBody)
+    }
+}

+ 450 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInV2NewFragment.kt

@@ -0,0 +1,450 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import android.graphics.drawable.GradientDrawable
+import android.os.Handler
+import android.os.Message
+import android.text.TextUtils
+import android.widget.EditText
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.GridLayoutManager
+import com.baidu.location.BDAbstractLocationListener
+import com.baidu.location.BDLocation
+import com.baidu.location.LocationClient
+import com.baidu.location.LocationClientOption
+import com.baidu.mapapi.model.LatLng
+import com.baidu.mapapi.utils.DistanceUtil
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_new.*
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.*
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.image_attendance_check_in_new_location_check_icon
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.rl_attendance_check_in_new_knock_btn
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.rv_attendance_check_in_new_schedules
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.tv_attendance_check_in_new_check_in
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.tv_attendance_check_in_new_now_time
+import kotlinx.android.synthetic.main.fragment_attendance_check_in_v2.tv_attendance_check_in_new_workplace
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecycleViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.im.MessageType
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.CheckButtonDoubleClick
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.GridLayoutItemDecoration
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+import org.jetbrains.anko.dip
+import java.util.*
+import kotlin.collections.ArrayList
+
+/**
+ * Created by fancyLou on 2020-07-17.
+ * Copyright © 2020 O2. All rights reserved.
+ */
+
+class AttendanceCheckInV2NewFragment : BaseMVPViewPagerFragment<AttendanceCheckInV2Contract.View, AttendanceCheckInV2Contract.Presenter>(),
+    AttendanceCheckInV2Contract.View {
+    override var mPresenter: AttendanceCheckInV2Contract.Presenter = AttendanceCheckInV2Presenter()
+    override fun layoutResId(): Int = R.layout.fragment_attendance_check_in_v2
+
+
+    private val workplaceList = ArrayList<AttendanceV2WorkPlace>()
+    private val recordList = ArrayList<AttendanceV2CheckItemData>()
+    private var nextCheckInRecord: AttendanceV2CheckItemData? = null
+    private var needCheckIn = false //是否可打卡
+    private var allowFieldWork = false // 是否允许外勤
+    private var requiredFieldWorkRemarks = false // 外勤是否必须打卡
+
+    private val recordAdapter: CommonRecycleViewAdapter<AttendanceV2CheckItemData> by lazy {
+        object : CommonRecycleViewAdapter<AttendanceV2CheckItemData>(activity, recordList, R.layout.item_attendance_check_in_schdule_list) {
+            override fun convert(holder: CommonRecyclerViewHolder?, t: AttendanceV2CheckItemData?) {
+                if (holder != null && t != null) {
+                    val status = if(t.isRecord) {
+                        t.recordTime
+                    } else {
+                        getString(R.string.attendance_v2_need_check_in)
+                    }
+                    holder.setText(R.id.tv_item_attendance_check_in_schedule_list_type, t.checkInTypeString)
+                        .setText(R.id.tv_item_attendance_check_in_schedule_list_time, t.preDutyTime)
+                        .setText(R.id.tv_item_attendance_check_in_schedule_list_status, status)
+                    val updateBtn = holder.getView<TextView>(R.id.tv_item_attendance_check_in_schedule_list_update_btn)
+                    updateBtn.gone()
+                    if (t.isLastRecord) {
+                        updateBtn.visible()
+                    }
+                }
+            }
+        }
+    }
+
+
+    //定位
+    private val mLocationClient: LocationClient by lazy { LocationClient(activity) }
+    private var myLocation: BDLocation? = null //当前我的位置
+    private var checkInPosition: AttendanceV2WorkPlace? = null//离的最近的工作地点位置
+    private var isInCheckInPositionRange = false
+
+
+    //刷新打卡按钮的时间
+    private val handler = Handler { msg ->
+        if (msg.what == 1) {
+            val nowTime = DateHelper.nowByFormate("HH:mm:ss")
+            tv_attendance_check_in_new_now_time?.text = nowTime
+        }
+        return@Handler true
+    }
+    private val timerTask = object : TimerTask() {
+        override fun run() {
+            val message = Message()
+            message.what = 1
+            handler.sendMessage(message)
+        }
+    }
+    private val timer: Timer by lazy { Timer() }
+
+
+    override fun lazyLoad() {
+        mPresenter.preCheckDataLoad()
+    }
+
+    override fun initUI() {
+        LocationClient.setAgreePrivacy(true)
+        //定位
+        mLocationClient.registerLocationListener(object : BDAbstractLocationListener() {
+            override fun onReceiveLocation(location: BDLocation?) {
+                XLog.debug("onReceive locType:${location?.locType}, latitude:${location?.latitude}, longitude:${location?.longitude}")
+                if (location != null) {
+                    myLocation = location
+                    //计算
+                    calNearestWorkplace()
+                }
+            }
+        })
+        initBaiduLocation()
+        mLocationClient.start()
+
+        //打卡班次
+        rv_attendance_check_in_new_schedules.layoutManager = GridLayoutManager(activity, 2)
+        rv_attendance_check_in_new_schedules.addItemDecoration(GridLayoutItemDecoration(activity?.dip(10) ?: 10, activity?.dip(10) ?: 10, 2))
+        rv_attendance_check_in_new_schedules.adapter = recordAdapter
+        recordAdapter.setOnItemClickListener { _, position ->
+            // 点击更新打卡
+            clickUpdateRecord(recordList[position])
+        }
+
+        //打卡按钮
+        rl_attendance_check_in_new_knock_btn.setOnClickListener {
+            if (CheckButtonDoubleClick.isFastDoubleClick(R.id.rl_attendance_check_in_new_knock_btn)) {
+                return@setOnClickListener
+            }
+            // 打卡
+            clickCheckIn()
+        }
+        //时间
+        timer.schedule(timerTask, 0, 1000)
+    }
+
+    private fun setCheckInBtnEnable(enable: Boolean) {
+        needCheckIn = enable
+        val draw = rl_attendance_check_in_new_knock_btn.background as? GradientDrawable
+        if (enable) {
+            activity?.let {
+                draw?.setColor(ContextCompat.getColor(it, R.color.z_color_primary))
+            }
+        } else {
+            activity?.let {
+                draw?.setColor(ContextCompat.getColor(it, R.color.disabled))
+            }
+        }
+    }
+
+
+    override fun onDestroyView() {
+        timer.cancel()
+        timerTask.cancel()
+        super.onDestroyView()
+    }
+
+    override fun onDestroy() {
+        // 退出时销毁定位
+        mLocationClient.stop()
+        super.onDestroy()
+    }
+
+    override fun preCheckData(data: AttendanceV2PreCheckData?) {
+         if (data == null) {
+             setCheckInBtnEnable(false)
+             return
+         }
+        XLog.debug("$data")
+        needCheckIn = data.canCheckIn   //今天是否还需要打卡
+        allowFieldWork = data.allowFieldWork
+        requiredFieldWorkRemarks = data.requiredFieldWorkRemarks
+        workplaceList.clear()
+        workplaceList.addAll(data.workPlaceList ?: ArrayList())
+        // 检查是否在范围内
+        calNearestWorkplace()
+        if (needCheckIn) {
+            // 打卡记录
+            val checkItemList = data.checkItemList ?: ArrayList()
+            // 先排序 防止顺序错乱
+            checkItemList.sortBy { it.preDutyTime }
+            // 是否最后一条已经打卡过的数据
+            nextCheckInRecord = checkItemList.firstOrNull { element -> element.checkInResult == AttendanceV2RecordResult.PreCheckIn.value }
+            needCheckIn = nextCheckInRecord != null
+            for ((index, item) in checkItemList.withIndex()) {
+                var isRecord = false
+                var recordTime = ""
+                if (item.checkInResult != AttendanceV2RecordResult.PreCheckIn.value) {
+                    isRecord = true
+                    var signTime = item.recordDate
+                    if (signTime.length > 16) {
+                        signTime = signTime.substring(11, 16);
+                    }
+                    var status = getString(R.string.attendance_v2_check_in_completed)
+                    if (item.checkInResult != AttendanceV2RecordResult.Normal.value) {
+                        status = item.resultText()
+                    }
+                    recordTime = "$status $signTime"
+                }
+                item.recordTime = recordTime
+                item.isRecord = isRecord // 是否已经打卡
+                item.checkInTypeString =  if(item.checkInType == AttendanceV2RecordCheckInType.OnDuty.value) { AttendanceV2RecordCheckInType.OnDuty.label }else{ AttendanceV2RecordCheckInType.OffDuty.label }
+                var preDutyTime = item.preDutyTime
+                if (TextUtils.isEmpty(item.shiftId)) {
+                    preDutyTime = "" // 如果没有班次信息 表示 自由工时 或者 休息日 不显示 打卡时间
+                }
+                item.preDutyTime = preDutyTime
+                // 处理是否是最后一个已经打卡的记录
+                if (item.checkInResult != AttendanceV2RecordResult.PreCheckIn.value) {
+                    if (index == checkItemList.size - 1) { // 最后一条
+                        item.isLastRecord = true // 最后一条已经打卡的记录
+                    } else {
+                        val nextItem = checkItemList[index+1]
+                        if (nextItem.checkInResult == AttendanceV2RecordResult.PreCheckIn.value) {
+                            item.isLastRecord = true
+                        }
+                    }
+                }
+                checkItemList[index] = item
+            }
+            recordList.clear()
+            recordList.addAll(checkItemList)
+        }
+        // 刷新页面
+        setCheckInBtnEnable(needCheckIn)
+        recordAdapter.notifyDataSetChanged()
+    }
+
+    override fun checkInPostResponse(result: Boolean, message: String?) {
+        tv_attendance_check_in_new_check_in?.setText(R.string.attendance_check_in_knock)
+        tv_attendance_check_in_new_now_time?.visible()
+        if (result) {
+            XToast.toastShort(R.string.attendance_v2_check_in_success)
+        } else if (!TextUtils.isEmpty(message)) {
+            XToast.toastShort(message!!)
+        }
+        mPresenter.preCheckDataLoad()
+    }
+
+    /**
+     * 点击更新打卡
+     */
+    private fun clickUpdateRecord(record: AttendanceV2CheckItemData) {
+        if (myLocation == null || TextUtils.isEmpty(myLocation?.addrStr)) {
+            XLog.error("没有定位到信息,可能是定位权限没开!!!")
+            XToast.toastShort(activity!!, R.string.attendance_message_no_location_info)
+            return
+        }
+        O2DialogSupport.openConfirmDialog(activity!!, "确定要更新这条打卡数据?", listener = { d ->
+            if (record.isLastRecord) { // 只有最后一条可以更新
+                tv_attendance_check_in_new_check_in.text = getString(R.string.attendance_check_in_knock_loading)
+                tv_attendance_check_in_new_now_time.gone()
+                if (isInCheckInPositionRange && checkInPosition != null) { // 正常打卡
+                    postCheckIn(record, checkInPosition!!.id, false, null)
+                } else {
+                    // 外勤
+                    outSide(record)
+                }
+            } else {
+                XLog.info("不是最后一条,不能更新打卡,怎么点击到的???")
+            }
+        })
+    }
+    /**
+     * 点击打卡
+     */
+    private fun clickCheckIn() {
+        if (myLocation == null || TextUtils.isEmpty(myLocation?.addrStr)) {
+            XLog.error("没有定位到信息,可能是定位权限没开!!!")
+            XToast.toastShort(activity!!, R.string.attendance_message_no_location_info)
+            return
+        }
+        if (needCheckIn && nextCheckInRecord != null) {
+            // 是否在打卡限制时间内
+            val preBeforeTime = nextCheckInRecord?.preDutyTimeBeforeLimit ?: ""
+            val preAfterTime = nextCheckInRecord?.preDutyTimeAfterLimit ?: ""
+            if (!checkLimitTime(preBeforeTime, preAfterTime)) {
+                return
+            }
+            tv_attendance_check_in_new_check_in.text = getString(R.string.attendance_check_in_knock_loading)
+            tv_attendance_check_in_new_now_time.gone()
+            if (isInCheckInPositionRange && checkInPosition != null) { // 正常打卡
+                postCheckIn(nextCheckInRecord!!, checkInPosition!!.id, false, null)
+            } else {
+                // 外勤
+                outSide(nextCheckInRecord!!)
+            }
+        } else {
+            XLog.info("不允许打卡或nextCheckInRecord is null")
+        }
+    }
+
+
+    // 是否有打卡时间限制
+    private fun checkLimitTime(preDutyTimeBeforeLimit: String, preDutyTimeAfterLimit: String): Boolean {
+        if (!TextUtils.isEmpty(preDutyTimeBeforeLimit)) {
+            val now = Date()
+            val today = DateHelper.nowByFormate("yyyy-MM-dd")
+            val beforeTime = DateHelper.convertStringToDate("$today $preDutyTimeBeforeLimit:00")
+            if (beforeTime != null && now.time <  beforeTime.time) {
+                XToast.toastShort(getString(R.string.attendance_v2_not_in_check_in_time, preDutyTimeBeforeLimit, preDutyTimeAfterLimit))
+                return false
+            }
+        }
+        if (!TextUtils.isEmpty(preDutyTimeAfterLimit)) {
+            val now = Date()
+            val today = DateHelper.nowByFormate("yyyy-MM-dd")
+            var afterTime = DateHelper.convertStringToDate("$today $preDutyTimeAfterLimit:00");
+            if (afterTime != null && now.time > afterTime.time) {
+                XToast.toastShort(getString(R.string.attendance_v2_not_in_check_in_time, preDutyTimeBeforeLimit, preDutyTimeAfterLimit))
+                return false
+            }
+        }
+        return true
+    }
+
+    /**
+     * 外勤打卡处理
+     */
+    private fun outSide(record: AttendanceV2CheckItemData) {
+        if (allowFieldWork) {
+            if (requiredFieldWorkRemarks) {
+                if (activity != null) {
+                    val dialog = O2DialogSupport.openCustomViewDialog(
+                        activity!!,
+                        getString(R.string.attendance_message_work_out),
+                        R.layout.dialog_name_modify
+                    ) { dialog ->
+                        val text = dialog.findViewById<EditText>(R.id.dialog_name_editText_id)
+                        if (TextUtils.isEmpty(text.text.toString())) {
+                            XToast.toastShort(activity!!, R.string.attendance_message_work_out_hint)
+                        } else {
+                            postCheckIn(record, null, true, text.text.toString())
+                        }
+                    }
+                    val text = dialog.findViewById<EditText>(R.id.dialog_name_editText_id)
+                    text.hint = getString(R.string.attendance_message_work_out_hint)
+                }
+            } else {
+                postCheckIn(record, null, true, null)
+            }
+        } else {
+            XToast.toastShort(activity, "不在工作地点的打卡范围内!")
+        }
+    }
+
+    /**
+     * 提交打卡信息
+     */
+    private fun postCheckIn(record: AttendanceV2CheckItemData, workPlaceId: String?, outSide: Boolean, outSideDesc:String?) {
+        val body = AttendanceV2CheckInBody()
+        body.recordId = record.id
+        body.checkInType = record.checkInType
+        body.latitude = myLocation!!.latitude.toString()
+        body.longitude =  myLocation!!.longitude.toString()
+        body.recordAddress = myLocation!!.addrStr
+        body.workPlaceId = workPlaceId ?: ""
+        body.fieldWork = outSide
+        body.signDescription = outSideDesc ?: ""
+        XLog.debug(body.toString())
+        mPresenter.checkInPost(body)
+    }
+
+    /**
+     * 检查是否进入打卡范围
+     */
+    private fun checkIsInWorkplace() {
+        XLog.info("checkIsInWorkplace.....${checkInPosition?.placeName}, ${myLocation?.addrStr}")
+        if (checkInPosition != null && myLocation != null) {
+            val workplacePosition = LatLng(checkInPosition!!.latitude.toDouble(), checkInPosition!!.longitude.toDouble())
+            val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+            val distance = DistanceUtil.getDistance(position, workplacePosition)
+            XLog.info("distance:$distance")
+            if (distance < checkInPosition!!.errorRange) {
+                isInCheckInPositionRange = true
+                activity?.runOnUiThread {
+                    tv_attendance_check_in_new_workplace.text = checkInPosition?.placeName
+                    image_attendance_check_in_new_location_check_icon.setImageResource(R.mipmap.list_selected)
+                }
+            } else {
+                isInCheckInPositionRange = false
+                activity?.runOnUiThread {
+                    tv_attendance_check_in_new_workplace.text = myLocation?.addrStr
+                    image_attendance_check_in_new_location_check_icon.setImageResource(R.mipmap.icon_delete_people)
+                }
+            }
+        }
+    }
+
+    /**
+     * 找到最近的打卡地点
+     */
+    private fun calNearestWorkplace() {
+        if ( myLocation!=null) {
+            if (workplaceList.isNotEmpty()) {
+                var minDistance: Double = -1.0
+                XLog.debug("calNearestWorkplace...................")
+                workplaceList.map {
+                    val p2 = LatLng(it.latitude.toDouble(), it.longitude.toDouble())
+                    val position = LatLng(myLocation!!.latitude, myLocation!!.longitude)
+                    val distance = DistanceUtil.getDistance(position, p2)
+                    if (minDistance == -1.0) {
+                        minDistance = distance
+                        checkInPosition = it
+                    } else {
+                        if (minDistance > distance) {
+                            minDistance = distance
+                            checkInPosition = it
+                        }
+                    }
+                }
+                XLog.info("checkInposition:${checkInPosition?.placeName}")
+                checkIsInWorkplace()
+            } else {
+                activity?.runOnUiThread {
+                    tv_attendance_check_in_new_workplace.text = myLocation?.addrStr
+                }
+            }
+        }
+    }
+
+    private fun initBaiduLocation() {
+        val option = LocationClientOption()
+        option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
+        option.setCoorType("bd09ll")//百度坐标系 可选,默认gcj02,设置返回的定位结果坐标系
+        option.setScanSpan(5000)//5秒一次定位 可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
+        option.setIsNeedAddress(true)//可选,设置是否需要地址信息,默认不需要
+        option.isOpenGps = true//可选,默认false,设置是否使用gps
+        option.isLocationNotify = true//可选,默认false,设置是否当GPS有效时按照1S/1次频率输出GPS结果
+        option.setIsNeedLocationDescribe(true)//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
+        option.setIsNeedLocationPoiList(true)//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
+        option.setIgnoreKillProcess(false)//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
+        option.SetIgnoreCacheException(false)//可选,默认false,设置是否收集CRASH信息,默认收集
+        option.setEnableSimulateGps(false)//可选,默认false,设置是否需要过滤GPS仿真结果,默认需要
+        mLocationClient.locOption = option
+    }
+}

+ 54 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceCheckInV2Presenter.kt

@@ -0,0 +1,54 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2CheckInBody
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceCheckInV2Presenter : BasePresenterImpl<AttendanceCheckInV2Contract.View>(), AttendanceCheckInV2Contract.Presenter {
+    override fun preCheckDataLoad() {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        if (service != null) {
+            service.attendanceV2PreCheck()
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        mView?.preCheckData(it?.data)
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        mView?.preCheckData(null)
+                    }
+                }
+        } else {
+            mView?.preCheckData(null)
+        }
+    }
+
+    override fun checkInPost(body: AttendanceV2CheckInBody) {
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        if (service != null) {
+            service.attendanceV2CheckIn(body)
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext {
+                        if (it != null && it.data != null ) {
+                            mView?.checkInPostResponse(true, null)
+                        } else {
+                            mView?.checkInPostResponse(false, null)
+                        }
+                    }
+                    onError { e, _ ->
+                        XLog.error("", e)
+                        mView?.checkInPostResponse(false, e?.message)
+                    }
+                }
+        } else {
+            mView?.checkInPostResponse(false, null)
+        }
+    }
+}

+ 175 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceMainActivity.kt

@@ -0,0 +1,175 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+
+import android.Manifest
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.provider.Settings
+import androidx.fragment.app.Fragment
+import android.view.Menu
+import android.view.MenuItem
+import kotlinx.android.synthetic.main.activity_attendance_main.*
+import net.muliba.changeskin.FancySkinManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.approval.AttendanceAppealApprovalActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.list.AttendanceListActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.setting.AttendanceLocationSettingActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonFragmentPagerAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.permission.PermissionRequester
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+
+
+class AttendanceMainActivity : BaseMVPActivity<AttendanceMainContract.View, AttendanceMainContract.Presenter>(), AttendanceMainContract.View {
+    override var mPresenter: AttendanceMainContract.Presenter = AttendanceMainPresenter()
+    override fun layoutResId(): Int = R.layout.activity_attendance_main
+
+    var locationEnable: Boolean = false
+    private var isAttendanceAdmin: Boolean = false
+    private var attendanceVersion: String = "1"
+
+    private lateinit var fragmentList: ArrayList<Fragment>
+    private val titleList: ArrayList<String> by lazy { arrayListOf<String>(getString(R.string.attendance_check_in_title), getString(R.string.title_activity_attendance_chart)) }
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        setupToolBar(getString(R.string.attendance_check_in_title), setupBackButton = true, isCloseBackIcon = true)
+
+        attendanceVersion = O2SDKManager.instance().prefs().getString(O2.PRE_ATTENDANCE_VERSION_KEY, "1") ?: "1"
+        fragmentList = if (attendanceVersion == "1") {
+            arrayListOf<Fragment>(AttendanceCheckInNewFragment(), AttendanceStatisticFragment())
+        }else {
+            arrayListOf<Fragment>(AttendanceCheckInV2NewFragment(), AttendanceStatisticV2Fragment())
+        }
+        view_pager_attendance_main_content.adapter = CommonFragmentPagerAdapter(supportFragmentManager, fragmentList, titleList)
+        view_pager_attendance_main_content.addOnPageChangeListener {
+            onPageSelected { position ->
+                selected(position)
+            }
+        }
+
+        linear_attendance_main_check_in_tab.setOnClickListener {
+            view_pager_attendance_main_content.currentItem = 0
+            selected(0)
+        }
+        linear_attendance_main_statistic_tab.setOnClickListener {
+            view_pager_attendance_main_content.currentItem = 1
+            selected(1)
+        }
+
+        view_pager_attendance_main_content.currentItem = 0
+        selected(0)
+
+        mPresenter.loadAttendanceAdmin()
+        checkPermission()
+    }
+
+    override fun onResume() {
+        super.onResume()
+
+    }
+
+//    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+//        menuInflater.inflate(R.menu.menu_attendance_main_normal, menu)
+//        return super.onCreateOptionsMenu(menu)
+//    }
+
+    override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
+        menu?.clear()
+        if (attendanceVersion == "1") {
+            if (isAttendanceAdmin) {
+                menuInflater.inflate(R.menu.menu_attendance_main_admin, menu)
+            } else {
+                menuInflater.inflate(R.menu.menu_attendance_main_normal, menu)
+            }
+        } else {
+            menuInflater.inflate(R.menu.menu_attendance_main_old, menu)
+        }
+        return super.onPrepareOptionsMenu(menu)
+    }
+
+
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when(item.itemId) {
+            R.id.menu_attendance_list -> {
+                XLog.debug("click menu attendance list")
+                go<AttendanceListActivity>()
+                true
+            }
+            R.id.menu_attendance_appeal_approval -> {
+                XLog.debug("click menu attendance approval list")
+                go<AttendanceAppealApprovalActivity>()
+                true
+            }
+            R.id.menu_attendance_location_setting -> {
+                XLog.debug("click menu attendance location setting")
+                go<AttendanceLocationSettingActivity>()
+                true
+            }
+            R.id.menu_attendance_to_old -> {
+                O2SDKManager.instance().prefs().edit {
+                    putString(O2.PRE_ATTENDANCE_VERSION_KEY, "1")
+                }
+                goThenKill<AttendanceMainActivity>()
+                true
+            }
+            else -> super.onOptionsItemSelected(item)
+        }
+    }
+
+    override fun finish() {
+        super.finish()
+        overridePendingTransition(R.anim.activity_scale_in, R.anim.activity_scale_out)
+    }
+
+    override fun isAttendanceAdmin(flag: Boolean) {
+        isAttendanceAdmin = flag
+        XLog.debug("is admin : $flag")
+        invalidateOptionsMenu()
+    }
+
+    fun checkPermission() {
+        PermissionRequester(this).request(Manifest.permission.ACCESS_FINE_LOCATION)
+                .o2Subscribe {
+                    onNext {  (granted, shouldShowRequestPermissionRationale, deniedPermissions) ->
+                        if (!granted){
+                            O2DialogSupport.openAlertDialog(this@AttendanceMainActivity, "需要定位权限, 去设置", { permissionSetting() })
+                        }else{
+                            locationEnable = true
+                        }
+
+                    }
+                    onError { e, _ ->
+                        XLog.error( "检查权限出错", e)
+                    }
+                }
+    }
+
+    private fun permissionSetting() {
+        val packageUri = Uri.parse("package:$packageName")
+        startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, packageUri))
+    }
+
+
+    private fun selected(position: Int) {
+        when (position) {
+            0 -> {
+                image_attendance_main_check_in_tab_icon.setImageDrawable(FancySkinManager.instance().getDrawable(this, R.mipmap.attendance_check_in_red))
+                tv_attendance_main_check_in_tab_name.setTextColor(FancySkinManager.instance().getColor(this@AttendanceMainActivity, R.color.z_color_primary))
+                image_attendance_main_statistic_tab_icon.setImageDrawable(FancySkinManager.instance().getDrawable(this,R.mipmap.attendance_statistic_gray))
+                tv_attendance_main_statistic_tab_name.setTextColor(FancySkinManager.instance().getColor(this@AttendanceMainActivity, R.color.z_color_text_primary))
+            }
+            1 -> {
+                image_attendance_main_check_in_tab_icon.setImageDrawable(FancySkinManager.instance().getDrawable(this,R.mipmap.attendance_check_in_gray))
+                tv_attendance_main_check_in_tab_name.setTextColor(FancySkinManager.instance().getColor(this@AttendanceMainActivity, R.color.z_color_text_primary))
+                image_attendance_main_statistic_tab_icon.setImageDrawable(FancySkinManager.instance().getDrawable(this, R.mipmap.attendance_statistic_red))
+                tv_attendance_main_statistic_tab_name.setTextColor(FancySkinManager.instance().getColor(this@AttendanceMainActivity, R.color.z_color_primary))
+            }
+        }
+        toolbarTitle?.text = titleList[position]
+    }
+}

+ 15 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceMainContract.kt

@@ -0,0 +1,15 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+
+
+object AttendanceMainContract {
+    interface View : BaseView {
+        fun isAttendanceAdmin(flag: Boolean)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun loadAttendanceAdmin()
+    }
+}

+ 33 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceMainPresenter.kt

@@ -0,0 +1,33 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceMainPresenter : BasePresenterImpl<AttendanceMainContract.View>(), AttendanceMainContract.Presenter {
+
+    override fun loadAttendanceAdmin() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.attendanceAdmin()
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            var flag = false
+                            val list = response.data
+                            list?.filter { O2SDKManager.instance().distinguishedName == it.adminName }?.map { flag = true }
+                            mView?.isAttendanceAdmin(flag)
+                        }
+                        onError { e, _ ->
+                            XLog.error("", e)
+                            mView?.isAttendanceAdmin(false)
+                        }
+                    }
+        }
+    }
+
+}

+ 25 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticContract.kt

@@ -0,0 +1,25 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.group.Group
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceStatisticGroupHeader
+
+/**
+ * Created by fancyLou on 28/05/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+object AttendanceStatisticContract {
+    interface View : BaseView {
+        fun attendanceDetailList(result: List<Group<AttendanceStatisticGroupHeader, AttendanceDetailInfoJson>>, lateNumber: Int, earlierNumber: Int, absentNumber: Int, normalNumber: Int)
+        fun attendanceStatisticCycle(result: String)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun getAttendanceListByMonth(month: String)
+        fun getAttendanceStatisticCycle(month: String)
+    }
+}

+ 141 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticFragment.kt

@@ -0,0 +1,141 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.LinearLayoutManager
+import android.text.TextUtils
+import com.jzxiang.pickerview.TimePickerDialog
+import com.jzxiang.pickerview.data.Type
+import com.jzxiang.pickerview.listener.OnDateSetListener
+import kotlinx.android.synthetic.main.fragment_attendance_statistic.*
+import net.muliba.changeskin.FancySkinManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.CommonRecyclerViewHolder
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.group.Group
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.group.GroupRecyclerViewAdapter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceStatisticGroupHeader
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.CircleTextView
+import java.util.*
+
+/**
+ * Created by fancyLou on 28/05/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+class AttendanceStatisticFragment : BaseMVPViewPagerFragment<AttendanceStatisticContract.View, AttendanceStatisticContract.Presenter>(),
+        AttendanceStatisticContract.View, OnDateSetListener {
+    override var mPresenter: AttendanceStatisticContract.Presenter = AttendanceStatisticPresenter()
+
+
+    override fun layoutResId(): Int = R.layout.fragment_attendance_statistic
+
+    private val list: ArrayList<Group<AttendanceStatisticGroupHeader, AttendanceDetailInfoJson>> = ArrayList()
+    private val adapter: GroupRecyclerViewAdapter<AttendanceStatisticGroupHeader, AttendanceDetailInfoJson> by lazy {
+        object : GroupRecyclerViewAdapter<AttendanceStatisticGroupHeader, AttendanceDetailInfoJson>(list, R.layout.item_attendance_statistic_header, R.layout.item_attendance_statistic_body) {
+            override fun onBindHeaderViewHolder(holder: CommonRecyclerViewHolder, header: AttendanceStatisticGroupHeader, position: Int) {
+                when (header.groupType) {
+                    0 -> {
+                        val headerType = holder.getView<CircleTextView>(R.id.circle_tv_item_attendance_statistic_header_type)
+                        headerType.setTextAndCircleColor("迟", ContextCompat.getColor(activity!!, R.color.z_attendance_late))
+                    }
+                    1 -> {
+                        val headerType = holder.getView<CircleTextView>(R.id.circle_tv_item_attendance_statistic_header_type)
+                        headerType.setTextAndCircleColor("早", ContextCompat.getColor(activity!!, R.color.z_attendance_leaveEarlier))
+                    }
+                    2 -> {
+                        val headerType = holder.getView<CircleTextView>(R.id.circle_tv_item_attendance_statistic_header_type)
+                        headerType.setTextAndCircleColor("缺", ContextCompat.getColor(activity!!, R.color.z_attendance_absent))
+                    }
+                    3 -> {
+                        val headerType = holder.getView<CircleTextView>(R.id.circle_tv_item_attendance_statistic_header_type)
+                        headerType.setTextAndCircleColor("正", ContextCompat.getColor(activity!!, R.color.z_color_primary))
+                    }
+                }
+                var weekDay = DateHelper.getWeekDay(header.firstDetail.recordDateString, "yyyy-MM-dd")
+                weekDay = if (weekDay != null) {
+                    "($weekDay)"
+                } else {
+                    ""
+                }
+                holder.setText(R.id.tv_item_attendance_statistic_header_date, header.firstDetail.recordDateString)
+                        .setText(R.id.tv_item_attendance_statistic_header_weekday, weekDay)
+                        .setText(R.id.tv_item_attendance_statistic_header_on_work_time, header.firstDetail.onDutyTime)
+                        .setText(R.id.tv_item_attendance_statistic_header_off_work_time, header.firstDetail.offDutyTime)
+            }
+
+            override fun onBindChildViewHolder(holder: CommonRecyclerViewHolder, child: AttendanceDetailInfoJson, position: Int) {
+                var weekDay = DateHelper.getWeekDay(child.recordDateString, "yyyy-MM-dd")
+                weekDay = if (weekDay != null) {
+                    "($weekDay)"
+                } else {
+                    ""
+                }
+                holder.setText(R.id.tv_item_attendance_statistic_body_date, child.recordDateString)
+                        .setText(R.id.tv_item_attendance_statistic_body_weekday, weekDay)
+                        .setText(R.id.tv_item_attendance_statistic_body_on_work_time, child.onDutyTime)
+                        .setText(R.id.tv_item_attendance_statistic_body_off_work_time, child.offDutyTime)
+            }
+        }
+    }
+
+    private var month = DateHelper.nowByFormate("yyyy-MM")
+
+
+    override fun initUI() {
+        recycler_attendance_statistic_list.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
+        recycler_attendance_statistic_list.adapter = adapter
+        tv_attendance_statistic_month.text = month
+        rl_attendance_statistic_choose_month_btn.setOnClickListener {
+            showMonthPicker()
+        }
+    }
+
+    override fun lazyLoad() {
+        loadData()
+    }
+
+
+    override fun attendanceDetailList(result: List<Group<AttendanceStatisticGroupHeader, AttendanceDetailInfoJson>>, lateNumber: Int, earlierNumber: Int, absentNumber: Int, normalNumber: Int) {
+        circle_attendance_statistic_normal.setText("${normalNumber}天")
+        circle_attendance_statistic_late.setText("${lateNumber}次")
+        circle_attendance_statistic_leave_earlier.setText("${earlierNumber}次")
+        circle_attendance_statistic_absent.setText("${absentNumber}次")
+        list.clear()
+        list.addAll(result)
+        adapter.notifyDataSetChanged()
+    }
+
+    override fun attendanceStatisticCycle(result: String) {
+        tv_attendance_statistic_period.text = result
+    }
+
+    override fun onDateSet(timePickerView: TimePickerDialog?, millseconds: Long) {
+        val newMonth = DateHelper.getDateTime("yyyy-MM", Date(millseconds))
+        if (!TextUtils.isEmpty(newMonth)) {
+            month = newMonth
+            tv_attendance_statistic_month.text = month
+        }
+        loadData()
+    }
+
+    private fun loadData() {
+        mPresenter.getAttendanceStatisticCycle(month)
+        mPresenter.getAttendanceListByMonth(month)
+    }
+
+    private fun showMonthPicker() {
+        val currentMonth = DateHelper.convertStringToDate("yyyy-MM", month).time
+        val monthChooser = TimePickerDialog.Builder()
+                .setThemeColor(FancySkinManager.instance().getColor(activity!!, R.color.z_color_primary))
+                .setType(Type.YEAR_MONTH)
+                .setTitleStringId("月份选择")
+                .setCurrentMillseconds(currentMonth)
+                .setCallBack(this)
+                .build()
+        monthChooser.show(activity!!.supportFragmentManager, "month")
+
+    }
+}

+ 160 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticPresenter.kt

@@ -0,0 +1,160 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.group.Group
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailQueryFilterJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceStatisticGroupHeader
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+/**
+ * Created by fancyLou on 28/05/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+class AttendanceStatisticPresenter : BasePresenterImpl<AttendanceStatisticContract.View>(), AttendanceStatisticContract.Presenter {
+
+    override fun getAttendanceStatisticCycle(query: String) {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            val year = query.substring(0, 4)
+            val month = query.substring(5, 7)
+            service.myAttendanceStatisticCycle(year, month)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { result ->
+                            var message = ""
+                            if (result.data != null) {
+                                message = "考勤周期:"+result.data.cycleStartDateString + "至" + result.data.cycleEndDateString
+                            }
+                            mView?.attendanceStatisticCycle(message)
+                        }
+                        onError { e, isNetworkError ->
+                            XLog.error("获取考勤统计周期异常, isneterror:$isNetworkError", e)
+                            mView?.attendanceStatisticCycle("")
+                        }
+                    }
+        }
+    }
+
+    override fun getAttendanceListByMonth(query: String) {
+        var lateNumber = 0
+        var earlierNumber = 0
+        var absentNumber = 0
+        var normalNumber = 0
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            val year = query.substring(0, 4)
+            val month = query.substring(5, 7)
+            val filter = AttendanceDetailQueryFilterJson(cycleYear = year, cycleMonth = month, q_empName = O2SDKManager.instance().distinguishedName, key = "recordDateString")
+            service.myAttendanceDetailListByMonth(filter, O2.FIRST_PAGE_TAG, "100")
+                    .subscribeOn(Schedulers.io())
+                    .flatMap { result ->
+                        val list = ArrayList<Group<AttendanceStatisticGroupHeader, AttendanceDetailInfoJson>>()
+                        val map = HashMap<Int, ArrayList<AttendanceDetailInfoJson>>()
+                        result.data.map { detail ->
+                            when {
+                                detail.isLate -> {
+                                    if (map.containsKey(0)) {
+                                        map[0]?.add(detail)
+                                    } else {
+                                        map[0] = arrayListOf(detail)
+                                    }
+                                    lateNumber++
+                                }
+                                detail.isLeaveEarlier -> {
+                                    if (map.containsKey(1)) {
+                                        map[1]?.add(detail)
+                                    } else {
+                                        map[1] = arrayListOf(detail)
+                                    }
+                                    earlierNumber++
+                                }
+                                detail.isAbsent -> {
+                                    if (map.containsKey(2)) {
+                                        map[2]?.add(detail)
+                                    } else {
+                                        map[2] = arrayListOf(detail)
+                                    }
+                                    absentNumber++
+                                }
+                                else -> {
+                                    if (map.containsKey(3)) {
+                                        map[3]?.add(detail)
+                                    } else {
+                                        map[3] = arrayListOf(detail)
+                                    }
+                                    normalNumber++
+                                }
+                            }
+                        }
+                        val lateList = map[0]
+                        if (lateList != null && lateList.isNotEmpty()) {
+                            val newlist = lateList.sortedBy { it.recordDateString }
+                            val header = AttendanceStatisticGroupHeader(0, newlist[0])
+                            if (newlist.size > 1) {
+                                val group = Group(header, newlist.subList(1, newlist.size))
+                                list.add(group)
+                            } else {
+                                val group = Group(header, ArrayList<AttendanceDetailInfoJson>())
+                                list.add(group)
+                            }
+                        }
+                        val earlierList = map[1]
+                        if (earlierList != null && earlierList.isNotEmpty()) {
+                            val newlist = earlierList.sortedBy { it.recordDateString }
+                            val header = AttendanceStatisticGroupHeader(1, newlist[0])
+                            if (newlist.size > 1) {
+                                val group = Group(header, newlist.subList(1, newlist.size))
+                                list.add(group)
+                            } else {
+                                val group = Group(header, ArrayList<AttendanceDetailInfoJson>())
+                                list.add(group)
+                            }
+                        }
+                        val absentList = map[2]
+                        if (absentList != null && absentList.isNotEmpty()) {
+                            val newlist = absentList.sortedBy { it.recordDateString }
+                            val header = AttendanceStatisticGroupHeader(2, newlist[0])
+                            if (newlist.size > 1) {
+                                val group = Group(header, newlist.subList(1, newlist.size))
+                                list.add(group)
+                            } else {
+                                val group = Group(header, ArrayList<AttendanceDetailInfoJson>())
+                                list.add(group)
+                            }
+                        }
+                        val normalList = map[3]
+                        if (normalList != null && normalList.isNotEmpty()) {
+                            val newlist = normalList.sortedBy { it.recordDateString }
+                            val header = AttendanceStatisticGroupHeader(3, newlist[0])
+                            if (newlist.size > 1) {
+                                val group = Group(header, newlist.subList(1, newlist.size))
+                                list.add(group)
+                            } else {
+                                val group = Group(header, ArrayList<AttendanceDetailInfoJson>())
+                                list.add(group)
+                            }
+                        }
+                        XLog.info("isLate:$lateNumber, earlier:$earlierNumber, absent:$absentNumber, normal:$normalNumber, listSize:${list.size}")
+                        Observable.just(list)
+                    }.observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { list ->
+                            mView?.attendanceDetailList(list, lateNumber, earlierNumber, absentNumber, normalNumber)
+                        }
+                        onError { e, isNetworkError ->
+                            XLog.error("获取考勤数据异常,isnetworkerror:$isNetworkError", e)
+                            mView?.attendanceDetailList(emptyList(), 0, 0, 0, 0)
+                        }
+                    }
+        }
+
+    }
+}

+ 24 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticV2Contract.kt

@@ -0,0 +1,24 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.adapter.group.Group
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceDetailInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceStatisticGroupHeader
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2StatisticResponse
+
+/**
+ * Created by fancyLou on 28/05/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+object AttendanceStatisticV2Contract {
+    interface View : BaseView {
+        fun myStatistic(my: AttendanceV2StatisticResponse)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun loadMyStatistic(startDate: String, endDate: String)
+    }
+}

+ 86 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticV2Fragment.kt

@@ -0,0 +1,86 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import android.text.SpannableString
+import android.text.style.UnderlineSpan
+import android.view.View
+import com.xiaomi.push.go
+import kotlinx.android.synthetic.main.fragment_attendance_statistic_v2.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.appeal.AttendanceV2AppealActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPViewPagerFragment
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2StatisticResponse
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.DateHelper
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.go
+import java.util.*
+
+/**
+ * Created by fancyLou on 28/05/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+class AttendanceStatisticV2Fragment : BaseMVPViewPagerFragment<AttendanceStatisticV2Contract.View, AttendanceStatisticV2Contract.Presenter>(),
+    AttendanceStatisticV2Contract.View {
+    override var mPresenter: AttendanceStatisticV2Contract.Presenter = AttendanceStatisticV2Presenter()
+
+
+    override fun layoutResId(): Int = R.layout.fragment_attendance_statistic_v2
+
+
+
+    override fun initUI() {
+        val cal = Calendar.getInstance()
+        tv_att_v2_stat_year.text = "${cal.get(Calendar.YEAR)}"
+        tv_att_v2_stat_month.text = "${cal.get(Calendar.MONTH) + 1}月"
+        val content = SpannableString(getString(R.string.attendance_v2_stat_to_appeal_label))
+        content.setSpan(UnderlineSpan(), 0, content.length, 0)
+        tv_att_v2_stat_to_appeal.text = content
+        tv_att_v2_stat_to_appeal.setOnClickListener {
+            activity?.go<AttendanceV2AppealActivity>()
+        }
+    }
+
+    override fun lazyLoad() {
+        // 当前月份
+        val cal = Calendar.getInstance()
+        val first = DateHelper.getMonthFirstDay(cal).time
+        val last = DateHelper.getMonthLastDay(cal).time
+        mPresenter.loadMyStatistic(DateHelper.getDateTime("yyyy-MM-dd", first), DateHelper.getDateTime("yyyy-MM-dd", last))
+    }
+
+    override fun myStatistic(my: AttendanceV2StatisticResponse) {
+        if (my.workTimeDuration > 0) {
+            ll_att_v2_stat_averageWorkTimeDuration.alpha = 1.0f
+        } else {
+            ll_att_v2_stat_averageWorkTimeDuration.alpha = 0.5f
+        }
+        setAlpha(my.attendance, ll_att_v2_stat_attendance)
+        setAlpha(my.rest, ll_att_v2_stat_rest)
+        setAlpha(my.leaveDays, tv_att_v2_stat_leaveDays)
+        setAlpha(my.absenteeismDays, ll_att_v2_stat_absenteeismDays)
+        setAlpha(my.lateTimes, ll_att_v2_stat_lateTimes)
+        setAlpha(my.leaveEarlierTimes, ll_att_v2_stat_leaveEarlierTimes)
+        setAlpha(my.absenceTimes, ll_att_v2_stat_absenceTimes)
+        setAlpha(my.fieldWorkTimes, ll_att_v2_stat_fieldWork)
+
+        tv_att_v2_stat_averageWorkTimeDuration.text = my.averageWorkTimeDuration
+        tv_att_v2_stat_attendance.text = "${my.attendance}"
+        tv_att_v2_stat_rest.text = "${my.rest}"
+        tv_att_v2_stat_leaveDays.text = "${my.leaveDays}"
+        tv_att_v2_stat_absenteeismDays.text = "${my.absenteeismDays}"
+        tv_att_v2_stat_lateTimes.text = "${my.lateTimes}"
+        tv_att_v2_stat_leaveEarlierTimes.text = "${my.leaveEarlierTimes}"
+        tv_att_v2_stat_absenceTimes.text = "${my.absenceTimes}"
+        tv_att_v2_stat_fieldWork.text = "${my.fieldWorkTimes}"
+
+    }
+    private fun setAlpha(bigger:Int, view: View) {
+        if (bigger > 0) {
+            view.alpha = 1.0f
+        } else {
+            view.alpha = 0.5f
+        }
+    }
+
+}

+ 35 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/main/AttendanceStatisticV2Presenter.kt

@@ -0,0 +1,35 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.main
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.AttendanceV2StatisticBody
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+/**
+ * Created by fancyLou on 28/05/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+class AttendanceStatisticV2Presenter : BasePresenterImpl<AttendanceStatisticV2Contract.View>(),
+    AttendanceStatisticV2Contract.Presenter {
+
+
+    override fun loadMyStatistic(startDate: String, endDate: String) {
+        XLog.debug("查询日期,start: $startDate end: $endDate")
+        val service = getAttendanceAssembleControlService(mView?.getContext())
+        service?.attendanceV2MyStatistic(AttendanceV2StatisticBody(startDate, endDate))
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())?.o2Subscribe {
+                onNext {
+                    if (it != null && it.data != null) {
+                        mView?.myStatistic(it.data)
+                    }
+                }
+                onError { e, _ ->
+                    XLog.error("", e)
+                }
+            }
+    }
+}

+ 250 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/setting/AttendanceLocationSettingActivity.kt

@@ -0,0 +1,250 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.setting
+
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import com.baidu.location.*
+import com.baidu.mapapi.map.*
+import com.baidu.mapapi.model.LatLng
+import kotlinx.android.synthetic.main.activity_attendance_location_setting.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInWorkplaceInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.text2String
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.O2DialogSupport
+
+
+class AttendanceLocationSettingActivity : BaseMVPActivity<AttendanceLocationSettingContract.View, AttendanceLocationSettingContract.Presenter>(),
+        AttendanceLocationSettingContract.View {
+    override var mPresenter: AttendanceLocationSettingContract.Presenter = AttendanceLocationSettingPresenter()
+    override fun layoutResId(): Int = R.layout.activity_attendance_location_setting
+
+    val WORK_PLACE_ID = "WORK_PLACE_ID"
+
+    val mLocationClient: LocationClient by lazy { LocationClient(this) }
+    lateinit var mBaiduMap: BaiduMap
+    var marker: Marker? = null
+    var latitude = ""
+    var longitude = ""
+
+
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        setupToolBar(getString(R.string.title_activity_attendance_location_setting), true)
+        LocationClient.setAgreePrivacy(true)
+        mLocationClient.registerLocationListener(object: BDAbstractLocationListener() {
+            override fun onReceiveLocation(location: BDLocation?) {
+                if (location == null) {
+                    return
+                }
+                XLog.debug("onReceiveLocation locType:${location.locType}")
+                val latitude = location.latitude
+                val longitude = location.longitude
+                XLog.info("定位成功,address:${location.addrStr}, latitude:$latitude, longitude:$longitude")
+                //定义Maker坐标点
+                val point = LatLng(latitude, longitude)
+                //地图显示在当前位置
+                val builder = MapStatus.Builder().target(point).zoom(18.0f)
+                mBaiduMap.animateMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build()))
+                //完成定位
+                mLocationClient.stop()
+            }
+
+        })
+
+        initBaiduLocation()
+        mBaiduMap = map_attendance_location_setting_baidu.map
+        mBaiduMap.mapType = BaiduMap.MAP_TYPE_NORMAL
+        mBaiduMap.setOnMapClickListener(object : BaiduMap.OnMapClickListener {
+            override fun onMapClick(latLng: LatLng?) {
+                XLog.debug("onMapClick latitude:${latLng?.latitude}, longitude:${latLng?.longitude}")
+                markerPoint(latLng)
+            }
+
+            override fun onMapPoiClick(poi: MapPoi?) {
+                val latLng = poi?.position
+                XLog.debug("onMapPoiClick latitude:${latLng?.latitude}, longitude:${latLng?.longitude}")
+                markerPoint(latLng)
+            }
+        })
+        mBaiduMap.setOnMarkerClickListener { marker ->
+            val bundle = marker.extraInfo
+            if (bundle != null) {
+                val id = bundle.getString(WORK_PLACE_ID)
+                deleteWorkplace(id)
+            }
+            false
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+        menuInflater.inflate(R.menu.menu_attendance_location_setting, menu)
+        return super.onCreateOptionsMenu(menu)
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.menu_work_place_save -> {
+                saveWorkplace()
+                return true
+            }
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+
+    override fun onResume() {
+        super.onResume()
+        //在activity执行onResume时执行mMapView. onResume (),实现地图生命周期管理
+        map_attendance_location_setting_baidu.onResume()
+        refreshMap()
+    }
+
+    override fun onPause() {
+        super.onPause()
+        map_attendance_location_setting_baidu.onPause()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        map_attendance_location_setting_baidu.onDestroy()
+    }
+
+//    override fun onReceiveLocation(location: BDLocation?) {
+//        if (location == null) {
+//            return
+//        }
+//        XLog.debug("onReceiveLocation locType:${location.locType}")
+//        val latitude = location.latitude
+//        val longitude = location.longitude
+//        XLog.info("定位成功,address:${location.addrStr}, latitude:$latitude, longitude:$longitude")
+//        //定义Maker坐标点
+//        val point = LatLng(latitude, longitude)
+//        //地图显示在当前位置
+//        val builder = MapStatus.Builder().target(point).zoom(18.0f)
+//        mBaiduMap.animateMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build()))
+//        //完成定位
+//        mLocationClient.stop()
+//    }
+
+//    override fun onConnectHotSpotMessage(p0: String?, p1: Int) {
+//        XLog.info("connectHotSpot $p0, $p1")
+//    }
+
+    override fun deleteWorkplace(flag: Boolean) {
+        hideLoadingDialog()
+        if (flag) {
+            XToast.toastShort(this, "删除工作场所成功!")
+            refreshMap()
+        }else {
+            XToast.toastShort(this, "删除工作场所失败!")
+        }
+    }
+
+    override fun saveWorkplace(flag: Boolean) {
+        hideLoadingDialog()
+        if (flag) {
+            XToast.toastShort(this, "保存工作场所成功!")
+            edit_attendance_location_setting_error_range.setText("")
+            edit_attendance_location_setting_name.setText("")
+            refreshMap()
+        }else {
+            XToast.toastShort(this, "保存工作场所失败!")
+        }
+    }
+
+    override fun workplaceList(list: List<MobileCheckInWorkplaceInfoJson>) {
+        val bitmap = BitmapDescriptorFactory
+                .fromResource(R.mipmap.icon_map_location_green)
+        list.map {
+            val point = LatLng(it.latitude.toDouble(), it.longitude.toDouble())
+            val bundle = Bundle()
+            bundle.putString(WORK_PLACE_ID, it.id)
+            val options = MarkerOptions()
+                    .position(point)  //设置marker的位置
+                    .title(it.placeName)
+                    .extraInfo(bundle)
+                    .icon(bitmap)  //设置marker图标
+                    .zIndex(9)
+            mBaiduMap.addOverlay(options)
+        }
+    }
+
+    private fun refreshMap() {
+        mBaiduMap.clear()
+        mLocationClient.start()
+        mPresenter.loadAllWorkplace()
+    }
+
+    private fun saveWorkplace() {
+        val name = edit_attendance_location_setting_name.text2String()
+        if (TextUtils.isEmpty(name)) {
+            XToast.toastShort(this, "工作场所名称不能为空!")
+            return
+        }
+        if (TextUtils.isEmpty(latitude) || TextUtils.isEmpty(longitude) ) {
+            XToast.toastShort(this, "请在地图上点击进行位置标记!")
+            return
+        }
+        var errorRange = edit_attendance_location_setting_error_range.text2String()
+        if (TextUtils.isEmpty(errorRange)) {
+            errorRange = "100"
+        }
+        showLoadingDialog()
+        mPresenter.saveWorkplace(name, errorRange, latitude, longitude)
+    }
+
+    private fun deleteWorkplace(id: String?) {
+        if (TextUtils.isEmpty(id)) {
+            XLog.error("id is null!!!!")
+            return
+        }
+        O2DialogSupport.openConfirmDialog(this, "确定要删除这个工作场所吗?",{
+            showLoadingDialog()
+            mPresenter.deleteWorkplace(id!!)
+        })
+    }
+
+    private fun markerPoint(latLng: LatLng?) {
+        if (latLng==null) {
+            XLog.error("坐标为空")
+            return
+        }
+        latitude =latLng.latitude.toString()
+        longitude = latLng.longitude.toString()
+        if (marker == null) {
+            //构建Marker图标
+            val bitmap = BitmapDescriptorFactory
+                    .fromResource(R.mipmap.icon_map_location)
+            val options = MarkerOptions()
+                    .position(latLng)  //设置marker的位置
+                    .icon(bitmap)  //设置marker图标
+                    .zIndex(9)
+            //将marker添加到地图上
+            marker = mBaiduMap.addOverlay(options) as Marker
+        }else {
+            marker?.position = latLng
+        }
+
+    }
+
+    private fun initBaiduLocation() {
+        val option = LocationClientOption()
+        option.locationMode = LocationClientOption.LocationMode.Hight_Accuracy//可选,默认高精度,设置定位模式,高精度,低功耗,仅设备
+        option.setCoorType("bd09ll")//可选,默认gcj02,设置返回的定位结果坐标系
+        option.setScanSpan(0)//可选,默认0,即仅定位一次,设置发起定位请求的间隔需要大于等于1000ms才是有效的
+        option.setIsNeedAddress(true)//可选,设置是否需要地址信息,默认不需要
+        option.isOpenGps = true//可选,默认false,设置是否使用gps
+        option.isLocationNotify = true//可选,默认false,设置是否当GPS有效时按照1S/1次频率输出GPS结果
+        option.setIsNeedLocationDescribe(true)//可选,默认false,设置是否需要位置语义化结果,可以在BDLocation.getLocationDescribe里得到,结果类似于“在北京天安门附近”
+        option.setIsNeedLocationPoiList(true)//可选,默认false,设置是否需要POI结果,可以在BDLocation.getPoiList里得到
+        option.setIgnoreKillProcess(false)//可选,默认true,定位SDK内部是一个SERVICE,并放到了独立进程,设置是否在stop的时候杀死这个进程,默认不杀死
+        option.SetIgnoreCacheException(false)//可选,默认false,设置是否收集CRASH信息,默认收集
+        option.setEnableSimulateGps(false)//可选,默认false,设置是否需要过滤GPS仿真结果,默认需要
+        mLocationClient.locOption = option
+    }
+
+}

+ 20 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/setting/AttendanceLocationSettingContract.kt

@@ -0,0 +1,20 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.setting
+
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenter
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInWorkplaceInfoJson
+
+
+object AttendanceLocationSettingContract {
+    interface View : BaseView {
+        fun deleteWorkplace(flag: Boolean)
+        fun saveWorkplace(flag: Boolean)
+        fun workplaceList(list: List<MobileCheckInWorkplaceInfoJson>)
+    }
+
+    interface Presenter : BasePresenter<View> {
+        fun deleteWorkplace(id: String)
+        fun saveWorkplace(name: String, errorRange: String, latitude: String, longitude: String)
+        fun loadAllWorkplace()
+    }
+}

+ 82 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/attendance/setting/AttendanceLocationSettingPresenter.kt

@@ -0,0 +1,82 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.attendance.setting
+
+import android.text.TextUtils
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BasePresenterImpl
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.attendance.MobileCheckInWorkplaceInfoJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import okhttp3.MediaType
+import okhttp3.RequestBody
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+class AttendanceLocationSettingPresenter : BasePresenterImpl<AttendanceLocationSettingContract.View>(), AttendanceLocationSettingContract.Presenter {
+
+    override fun deleteWorkplace(id: String) {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.deleteAttendanceWorkplace(id)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext {
+                            response ->
+                            XLog.debug("$response")
+                            mView?.deleteWorkplace(true)
+                        }
+                        onError {
+                            e, _ ->
+                            XLog.error("", e)
+                            mView?.deleteWorkplace(false)
+                        }
+                    }
+        }
+    }
+
+    override fun saveWorkplace(name: String, errorRange: String, latitude: String, longitude: String) {
+            if (TextUtils.isEmpty(name) || TextUtils.isEmpty(latitude) || TextUtils.isEmpty(longitude)) {
+                mView?.saveWorkplace(false)
+                return
+            }
+            val info = MobileCheckInWorkplaceInfoJson()
+            info.placeName = name
+            info.errorRange = if (TextUtils.isEmpty(errorRange)){100}else{errorRange.toInt()}
+            info.latitude = latitude
+            info.longitude = longitude
+            val json = O2SDKManager.instance().gson.toJson(info)
+            val body = RequestBody.create(MediaType.parse("text/json"), json)
+            getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+                service.attendanceWorkplace(body)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            XLog.debug("$response")
+                            mView?.saveWorkplace(true)
+                        }
+                        onError { e, _ ->
+                            XLog.error("",e)
+                            mView?.saveWorkplace(false)
+                        }
+                    }
+
+        }
+    }
+
+    override fun loadAllWorkplace() {
+        getAttendanceAssembleControlService(mView?.getContext())?.let { service ->
+            service.findAllWorkplace().subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .o2Subscribe {
+                        onNext { response ->
+                            mView?.workplaceList(response.data?: ArrayList<MobileCheckInWorkplaceInfoJson>())
+                        }
+                        onError { e, _ ->
+                            XLog.error("",e)
+                            mView?.workplaceList(ArrayList<MobileCheckInWorkplaceInfoJson>())
+                        }
+                    }
+        }
+    }
+}

+ 250 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPActivity.kt

@@ -0,0 +1,250 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.BitmapFactory
+import android.os.Build
+import android.os.Bundle
+import android.webkit.WebView
+import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.core.app.NotificationCompat
+import com.google.android.exoplayer2.util.NotificationUtil
+import com.wugang.activityresult.library.ActivityResult
+import net.muliba.changeskin.FancySkinManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.o2.organization.ContactPickerActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.ApiResponse
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.identity.IdentityLevelForm
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.main.unit.UnitJson
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.vo.ContactPickerResult
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.LoadingDialog
+import org.jetbrains.anko.internals.AnkoInternals.createAnkoContext
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+
+/**
+ * Created by fancy on 2017/6/5.
+ */
+
+abstract class BaseMVPActivity<in V: BaseView, T: BasePresenter<V>>: AppCompatActivity(), BaseView {
+
+    //need override
+    abstract protected var mPresenter : T
+    abstract fun afterSetContentView(savedInstanceState: Bundle?)
+    abstract fun layoutResId(): Int
+    //
+    open fun beforeSetContentView(){}
+    override fun getContext(): Context  = this
+
+    //Toolbar 标题栏
+    protected var toolbar: Toolbar? = null
+    /**
+     * ActionBar居中的标题
+     */
+    protected var toolbarTitle: TextView? = null
+
+    var loadingDialog: LoadingDialog? = null
+
+    // 字体不放大
+    private val fontScale = 1f
+
+
+    override fun onCreate(savedInstanceState: Bundle?)  {
+        super.onCreate(savedInstanceState)
+        beforeSetContentView()
+        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+        setContentView(layoutResId())
+        //防止截屏
+//        window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        // 沉浸式状态栏
+//        ImmersedStatusBarUtils.setImmersedStatusBar(this)
+
+        mPresenter.attachView(this as V)
+        afterSetContentView(savedInstanceState)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            window.statusBarColor = FancySkinManager.instance().getColor(this, R.color.z_color_primary)
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mPresenter.detachView()
+    }
+
+    //固定字体比例 防止系统设置字体影响
+    override fun getResources(): Resources {
+        var res =  super.getResources()
+        if (res.configuration != null && res.configuration.fontScale != fontScale) {
+            val config = Configuration()
+            config.setToDefaults()
+            config.fontScale = fontScale
+            val ct = createConfigurationContext(config)
+            if (ct != null) {
+                res = ct.resources
+            }
+        }
+        return res
+    }
+
+
+    fun setupToolBar(title:String = "", setupBackButton:Boolean = false, isCloseBackIcon: Boolean = false) {
+        toolbar = findViewById(R.id.toolbar_snippet_top_bar)
+        toolbar?.title = ""
+        setSupportActionBar(toolbar)
+        toolbarTitle = findViewById(R.id.tv_snippet_top_title)
+        toolbarTitle?.text = title
+        if (setupBackButton) {
+            if (isCloseBackIcon){
+                setToolbarBackBtnWithCloseIcon()
+            }else {
+                setToolbarBackBtn()
+            }
+        }
+    }
+
+    fun updateToolbarTitle(title: String) {
+        toolbarTitle?.text = title
+    }
+
+    fun setToolbarBackBtn() {
+        toolbar?.setNavigationIcon(R.mipmap.ic_back_mtrl_white_alpha)
+        toolbar?.setNavigationOnClickListener { finish() }
+    }
+    fun setToolbarBackBtnWithCloseIcon() {
+        toolbar?.setNavigationIcon(R.mipmap.icon_menu_window_close)
+        toolbar?.setNavigationOnClickListener { finish() }
+    }
+
+    fun showLoadingDialog() {
+        if (loadingDialog==null) {
+            loadingDialog = LoadingDialog(this)
+        }
+        loadingDialog?.show()
+    }
+    fun hideLoadingDialog() {
+        loadingDialog?.dismiss()
+    }
+
+
+    fun contactPicker(bundle: Bundle, callback: (ContactPickerResult?)-> Unit) {
+        ActivityResult.of(this)
+                .className(ContactPickerActivity::class.java)
+                .params(bundle)
+                .greenChannel().forResult { _, data ->
+                    val result = data?.getParcelableExtra<ContactPickerResult>(ContactPickerActivity.CONTACT_PICKED_RESULT)
+                    if (result != null) {
+                        callback(result)
+                    }else {
+                        callback(null)
+                    }
+                }
+    }
+
+    fun getCurrentIdentityUnit(callback:(String?)-> Unit) {
+        val expressService = RetrofitClient.instance().assembleExpressApi()
+        val personalService = RetrofitClient.instance().assemblePersonalApi()
+        personalService.getCurrentPersonInfo()
+                .subscribeOn(Schedulers.io())
+                .flatMap { unitResponse ->
+                    val person = unitResponse.data
+                    if (person != null) {
+                        val identityList = person.woIdentityList
+                        if (identityList.isNotEmpty()) {
+                            val identity = identityList[0]
+                            val form = IdentityLevelForm(identity = identity.distinguishedName, level = 1)
+                            expressService.unitByIdentityAndLevel(form)
+                        } else {
+                            Observable.just(ApiResponse<UnitJson>())
+                        }
+                    } else {
+                        Observable.just(ApiResponse<UnitJson>())
+                    }
+                }.observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext { unitJsonApiResponse ->
+                        if (unitJsonApiResponse.data != null) {
+                            callback(unitJsonApiResponse.data.distinguishedName)
+                        }else {
+                            callback(null)
+                        }
+                    }
+                    onError{ e, _ ->
+                        XLog.error("", e)
+                        callback(null)
+                    }
+                }
+    }
+
+
+    /**
+     * webview 图片太大的问题处理
+     */
+    fun imgReset(webv: WebView) {
+        webv.loadUrl(
+            "javascript:(function(){" +
+                    "var objs = document.getElementsByTagName('img'); " +
+                    "for(var i=0;i<objs.length;i++) " +
+                    "{"
+                    + "var img = objs[i]; " +
+                    " img.style.maxWidth = '100%'; img.style.height = 'auto'; " +
+                    "}" +
+                    "})()"
+        )
+    }
+
+    private val mNotificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
+    private val mNormalDefaultChannelId = "o2oa_base_notify_default_channel"
+    private val mNormalDefaultChannelName = "普通"
+    private val mNormalHighChannelId = "o2oa_base_notify_High_channel"
+    private val mNormalHighChannelName = "重要"
+    private val mNormalNotificationId = 1024 // 应用内通知id
+    /**
+     * app通知
+     * @param importance  NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_HIGH
+     */
+    fun baseNotify(title: String, content:String, importance: Int = 0 ) {
+        XLog.debug("发送通知,$title , $content , $importance")
+        var channelId = mNormalDefaultChannelId
+        // 适配8.0及以上 创建渠道
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            var imp = importance
+            if (imp == 0) {
+                imp = NotificationManager.IMPORTANCE_DEFAULT
+            }
+            if (imp == NotificationManager.IMPORTANCE_HIGH) {
+                channelId = mNormalHighChannelId
+            }
+            val channelName = if (imp == NotificationManager.IMPORTANCE_HIGH) {
+                mNormalHighChannelName
+            } else {
+                mNormalDefaultChannelName
+            }
+            val channel = NotificationChannel(channelId, channelName, imp)
+            mNotificationManager.createNotificationChannel(channel)
+        }
+        // 构建配置
+        val mBuilder = NotificationCompat.Builder(this, channelId)
+            .setContentTitle(title) // 标题
+            .setContentText(content) // 文本
+            .setSmallIcon(R.mipmap.logo) // 小图标
+            .setPriority(NotificationCompat.PRIORITY_DEFAULT) // 7.0 设置优先级
+            .setAutoCancel(true) // 是否自动消失(点击)or mManager.cancel(mNormalNotificationId)、cancelAll、setTimeoutAfter()
+        // 发起通知
+        mNotificationManager.notify(mNormalNotificationId, mBuilder.build())
+
+    }
+
+}

+ 53 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPFragment.kt

@@ -0,0 +1,53 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.content.Context
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.LoadingDialog
+
+/**
+ * Created by fancy on 2017/6/8.
+ */
+
+
+abstract class BaseMVPFragment<in V: BaseView, T: BasePresenter<V>>: Fragment(), BaseView {
+
+    abstract protected var mPresenter : T
+    abstract fun layoutResId(): Int
+    abstract fun initUI()
+
+    open fun initData() {
+
+    }
+
+    val loadingDialog: LoadingDialog by lazy { LoadingDialog(activity) }
+
+    override fun getContext(): Context?  = activity
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mPresenter.attachView(this as V)
+        initData()
+    }
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+        return  inflater?.inflate(layoutResId(), container, false)
+    }
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        initUI()
+    }
+    override fun onDestroy() {
+        super.onDestroy()
+        mPresenter.detachView()
+    }
+
+    fun showLoadingDialog() {
+        loadingDialog.show()
+    }
+    fun hideLoadingDialog() {
+        loadingDialog.dismiss()
+    }
+}

+ 128 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseMVPViewPagerFragment.kt

@@ -0,0 +1,128 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.widgets.dialog.LoadingDialog
+
+/**
+ * Created by fancy on 2017/6/9.
+ */
+
+
+abstract class BaseMVPViewPagerFragment<in V : BaseView, T : BasePresenter<V>> : Fragment(), BaseView {
+
+    protected abstract var mPresenter: T
+    abstract fun lazyLoad()
+    abstract fun layoutResId(): Int
+    abstract fun initUI()
+
+
+    val loadingDialog: LoadingDialog by lazy { LoadingDialog(activity) }
+
+    /**
+     * fragment 是否已经建好UI
+     */
+    protected var isViewInit = false
+    /** Fragment当前状态是否可见 */
+    protected var isViewVisible = false
+
+
+    override fun getContext(): Context? = activity
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        //一定要调用,否则无法将菜单加入ActionItem
+        setHasOptionsMenu(true)
+
+    }
+
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+        val mRootView =   inflater.inflate(layoutResId(), container, false)
+        mPresenter.attachView(this as V)
+        XLog.info("onCreateView.............res:"+layoutResId())
+        if (mRootView == null) {
+            XLog.info("mRootView is null.........................")
+        }
+        return mRootView
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+        isViewInit = true
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        XLog.info("onViewCreated.............")
+        if (view == null) {
+            XLog.info("view is null.........................")
+        }
+        initUI()
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        isViewInit = false
+        mPresenter.detachView()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        // 判断当前fragment是否显示
+        if (isViewVisible) {
+            fetchData()
+        }
+    }
+
+
+    //对用户是否可见,在onCreateView之前调用
+    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
+        super.setUserVisibleHint(isVisibleToUser)
+        isViewVisible = isVisibleToUser
+        fetchData()
+    }
+
+
+    override fun onDestroy() {
+        super.onDestroy()
+
+    }
+
+    fun fetchData() {
+        if (isViewInit && isViewVisible) {
+            lazyLoad()
+        }
+    }
+
+    fun showLoadingDialog() {
+        loadingDialog.show()
+    }
+
+    fun hideLoadingDialog() {
+        loadingDialog.dismiss()
+    }
+
+    /**
+     * webview 图片太大的问题处理
+     */
+    fun imgReset(webv: WebView) {
+        webv.loadUrl(
+            "javascript:(function(){" +
+                    "var objs = document.getElementsByTagName('img'); " +
+                    "for(var i=0;i<objs.length;i++) " +
+                    "{"
+                    + "var img = objs[i]; " +
+                    " img.style.maxWidth = '100%'; img.style.height = 'auto'; " +
+                    "}" +
+                    "})()"
+        )
+    }
+}

+ 94 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseO2Activity.kt

@@ -0,0 +1,94 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import android.widget.TextView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.ImmersedStatusBarUtils
+
+/**
+ * Created by fancyLou on 20/06/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+abstract class BaseO2Activity : AppCompatActivity() {
+
+    abstract fun layoutResId(): Int
+    abstract fun afterSetContentView(savedInstanceState: Bundle?)
+    /**
+     * 可以在setContentView之前做一些事
+     */
+    open fun beforeSetContentView() {}
+
+    /**
+     * Toolbar
+     */
+    protected var toolbar: Toolbar? = null
+    /**
+     * Toolbar居中的标题
+     */
+    private var toolbarTitle: TextView? = null
+
+    //固定字体比例 防止系统设置字体影响
+    override fun getResources(): Resources {
+        var res =  super.getResources()
+        if (res.configuration != null && res.configuration.fontScale != 1f) {
+            val config = Configuration()
+            config.setToDefaults()
+            config.fontScale = 1f
+            val ct = createConfigurationContext(config)
+            if (ct != null) {
+                res = ct.resources
+            }
+        }
+        return res
+    }
+
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        beforeSetContentView()
+        setContentView(layoutResId())
+        //防止截屏
+//        window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        // 沉浸式状态栏
+        ImmersedStatusBarUtils.setImmersedStatusBar(this)
+        afterSetContentView(savedInstanceState)
+    }
+
+
+    fun setupToolBar(title: String = "", setupBackButton: Boolean = false, isCloseBackIcon: Boolean = false) {
+        toolbar = findViewById(R.id.toolbar_snippet_top_bar)
+        toolbar?.title = ""
+        setSupportActionBar(toolbar)
+        toolbarTitle = findViewById(R.id.tv_snippet_top_title)
+        toolbarTitle?.text = title
+        if (setupBackButton) {
+            if (isCloseBackIcon) {
+                setToolbarBackBtnWithCloseIcon()
+            } else {
+                setToolbarBackBtn()
+            }
+        }
+    }
+
+    fun updateToolbarTitle(title: String) {
+        toolbarTitle?.text = title
+    }
+
+    private fun setToolbarBackBtn() {
+        toolbar?.setNavigationIcon(R.mipmap.ic_back_mtrl_white_alpha)
+        toolbar?.setNavigationOnClickListener { finish() }
+    }
+
+    private fun setToolbarBackBtnWithCloseIcon() {
+        toolbar?.setNavigationIcon(R.mipmap.icon_menu_window_close)
+        toolbar?.setNavigationOnClickListener { finish() }
+    }
+
+}

+ 94 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseO2BindActivity.kt

@@ -0,0 +1,94 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Bundle
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import android.widget.TextView
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.ImmersedStatusBarUtils
+
+/**
+ * Created by fancyLou on 20/06/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+abstract class BaseO2BindActivity : AppCompatActivity() {
+
+    abstract fun bindView(savedInstanceState: Bundle?)
+    abstract fun afterSetContentView(savedInstanceState: Bundle?)
+    /**
+     * 可以在setContentView之前做一些事
+     */
+    open fun beforeSetContentView() {}
+
+    /**
+     * Toolbar
+     */
+    protected var toolbar: Toolbar? = null
+    /**
+     * Toolbar居中的标题
+     */
+    private var toolbarTitle: TextView? = null
+
+
+    //固定字体比例 防止系统设置字体影响
+    override fun getResources(): Resources {
+        var res =  super.getResources()
+        if (res.configuration != null && res.configuration.fontScale != 1f) {
+            val config = Configuration()
+            config.setToDefaults()
+            config.fontScale = 1f
+            val ct = createConfigurationContext(config)
+            if (ct != null) {
+                res = ct.resources
+            }
+        }
+        return res
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        beforeSetContentView()
+        bindView(savedInstanceState)
+        //防止截屏
+//        window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+        // 沉浸式状态栏
+        ImmersedStatusBarUtils.setImmersedStatusBar(this)
+        afterSetContentView(savedInstanceState)
+    }
+
+
+    fun setupToolBar(title: String = "", setupBackButton: Boolean = false, isCloseBackIcon: Boolean = false) {
+        toolbar = findViewById(R.id.toolbar_snippet_top_bar)
+        toolbar?.title = ""
+        setSupportActionBar(toolbar)
+        toolbarTitle = findViewById(R.id.tv_snippet_top_title)
+        toolbarTitle?.text = title
+        if (setupBackButton) {
+            if (isCloseBackIcon) {
+                setToolbarBackBtnWithCloseIcon()
+            } else {
+                setToolbarBackBtn()
+            }
+        }
+    }
+
+    fun updateToolbarTitle(title: String) {
+        toolbarTitle?.text = title
+    }
+
+    private fun setToolbarBackBtn() {
+        toolbar?.setNavigationIcon(R.mipmap.ic_back_mtrl_white_alpha)
+        toolbar?.setNavigationOnClickListener { finish() }
+    }
+
+    private fun setToolbarBackBtnWithCloseIcon() {
+        toolbar?.setNavigationIcon(R.mipmap.icon_menu_window_close)
+        toolbar?.setNavigationOnClickListener { finish() }
+    }
+
+}

+ 234 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseO2ViewModel.kt

@@ -0,0 +1,234 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2App
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.service.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+
+/**
+ * Created by fancyLou on 20/06/2018.
+ * Copyright © 2018 O2. All rights reserved.
+ */
+
+
+open class BaseO2ViewModel(app: Application) : AndroidViewModel(app) {
+
+    fun getApiService(url: String): ApiService? {
+        return try {
+            RetrofitClient.instance().api(url)
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "中心服务异常,请联系管理员!!")
+            null
+        }
+    }
+
+    fun getCollectService(): CollectService? {
+        return try {
+            RetrofitClient.instance().collectApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "O2注册中心服务异常,请联系管理员!!")
+            null
+        }
+    }
+
+    /**
+     * 人员组织服务
+     */
+    fun getOrganizationAssembleControlApi(): OrganizationAssembleControlAlphaService? {
+        return try {
+            RetrofitClient.instance().organizationAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "人员组织模块服务异常,请联系管理员!!")
+            null
+        }
+    }
+
+    /**
+     * 认证服务
+     */
+    fun getAssembleAuthenticationService(): OrgAssembleAuthenticationService? {
+        return try {
+            RetrofitClient.instance().assembleAuthenticationApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "权限认证模块服务异常,请联系管理员!!")
+            null
+        }
+    }
+
+    /**
+     * 热图服务
+     */
+    fun getHotPicAssembleControlServiceApi(): HotpicAssembleControlService? {
+        return try {
+            RetrofitClient.instance().hotpicAssembleControlServiceApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "热点图片新闻服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 人员服务
+     */
+    fun getAssemblePersonalApi(): OrgAssemblePersonalService? {
+        return try {
+            RetrofitClient.instance().assemblePersonalApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "个人信息服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 组织服务
+     */
+    fun getAssembleExpressApi(): OrgAssembleExpressService? {
+        return try {
+            RetrofitClient.instance().assembleExpressApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "组织管理服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 流程服务
+     */
+    fun getProcessAssembleSurfaceServiceAPI(): ProcessAssembleSurfaceService? {
+        return try {
+            RetrofitClient.instance().processAssembleSurfaceServiceAPI()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "流程服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 云盘服务
+     */
+    fun getFileAssembleControlService(): FileAssembleControlService? {
+        return try {
+            RetrofitClient.instance().fileAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "云盘服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 会议管理服务
+     */
+    fun getMeetingAssembleControlService(): MeetingAssembleControlService? {
+        return try {
+            RetrofitClient.instance().meetingAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "会议管理模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 考勤管理服务
+     */
+    fun getAttendanceAssembleControlService(): AttendanceAssembleControlService? {
+        return try {
+            RetrofitClient.instance().attendanceAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "考勤管理模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 论坛服务
+     */
+    fun getBBSAssembleControlService(): BBSAssembleControlService? {
+        return try {
+            RetrofitClient.instance().bbsAssembleControlServiceApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "论坛服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 信息中心
+     */
+    fun getCMSAssembleControlService(): CMSAssembleControlService? {
+        return try {
+            RetrofitClient.instance().cmsAssembleControlService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "信息中心服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 公共配置服务
+     */
+    fun getOrganizationAssembleCustomService(): OrganizationAssembleCustomService? {
+        return try {
+            RetrofitClient.instance().organizationAssembleCustomService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "公共配置服务模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 门户模块
+     */
+    fun getPortalAssembleSurfaceService(): PortalAssembleSurfaceService? {
+        return try {
+            RetrofitClient.instance().portalAssembleSurfaceService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "门户模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 日程管理
+     */
+    fun getCalendarAssembleService(): CalendarAssembleControlService? {
+        return try {
+            RetrofitClient.instance().calendarAssembleControlService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "日程管理模块异常,请联系管理员!")
+            null
+        }
+    }
+
+    /**
+     * 图灵 v1 服务
+     */
+    fun getTuling123Service(): Tuling123Service? {
+        return try {
+            RetrofitClient.instance().tuling123Service()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            XToast.toastLong(O2App.instance, "AI查询模块异常,请联系管理员!")
+            null
+        }
+    }
+}

+ 22 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BasePresenter.kt

@@ -0,0 +1,22 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.content.Context
+import android.widget.TextView
+
+/**
+ * Created by fancy on 2017/6/5.
+ */
+
+
+interface BasePresenter<in V : BaseView> {
+    fun attachView(view: V)
+    fun detachView()
+    fun jPushBindDevice()
+    fun jPushUnBindDevice()
+    // 查询待办数量
+    fun getTaskNumber(context: Context?, tv: TextView, tvTag: String)
+    // 查询待阅数量
+    fun getReadNumber(context: Context?, tv: TextView, tvTag: String)
+
+    fun getPortalNumber(context: Context?, tv: TextView, portalId: String)
+}

+ 572 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BasePresenterImpl.kt

@@ -0,0 +1,572 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.content.Context
+import android.text.TextUtils
+import android.widget.TextView
+import cn.jpush.android.api.JPushInterface
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.BuildConfig
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.O2SDKManager
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.RetrofitClient
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.core.component.api.service.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.JPushDeviceForm
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.model.bo.api.PushType
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XToast
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.o2Subscribe
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+
+/**
+ * Created by fancy on 2017/6/5.
+ */
+
+open class BasePresenterImpl<V : BaseView> : BasePresenter<V> {
+
+    protected var mView: V? = null
+
+    override fun attachView(view: V) {
+        mView = view
+    }
+
+    override fun detachView() {
+        mView = null
+    }
+
+    fun getApiService(context: Context?, url: String): ApiService? {
+        return try {
+            RetrofitClient.instance().api(url)
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "中心服务异常,请联系管理员!!")
+            }
+            null
+        }
+    }
+
+    fun getCollectService(context: Context?): CollectService? {
+        return try {
+            RetrofitClient.instance().collectApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "O2注册中心服务异常,请联系管理员!!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 人员组织服务
+     */
+    fun getOrganizationAssembleControlApi(context: Context?): OrganizationAssembleControlAlphaService? {
+        return try {
+            RetrofitClient.instance().organizationAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "人员组织模块服务异常,请联系管理员!!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 查询服务
+     */
+    fun getQueryAssembleSurfaceServiceAPI(context: Context?): QueryAssembleSurfaceService? {
+        return try {
+            RetrofitClient.instance().queryAssembleSurfaceServiceAPI()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "查询模块异常,请联系管理员!!")
+            }
+            null
+        }
+    }
+
+
+    /**
+     * 认证服务
+     */
+    fun getAssembleAuthenticationService(context: Context?): OrgAssembleAuthenticationService? {
+        return try {
+            RetrofitClient.instance().assembleAuthenticationApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "权限认证模块服务异常,请联系管理员!!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 热图服务
+     */
+    fun getHotPicAssembleControlServiceApi(context: Context?): HotpicAssembleControlService? {
+        return try {
+            RetrofitClient.instance().hotpicAssembleControlServiceApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "热点图片新闻服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 人员服务
+     */
+    fun getAssemblePersonalApi(context: Context?): OrgAssemblePersonalService? {
+        return try {
+            RetrofitClient.instance().assemblePersonalApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "个人信息服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 组织服务
+     */
+    fun getAssembleExpressApi(context: Context?): OrgAssembleExpressService? {
+        return try {
+            RetrofitClient.instance().assembleExpressApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "组织管理服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 流程服务
+     */
+    fun getProcessAssembleSurfaceServiceAPI(context: Context?): ProcessAssembleSurfaceService? {
+        return try {
+            RetrofitClient.instance().processAssembleSurfaceServiceAPI()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "流程服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 云盘服务
+     */
+    fun getFileAssembleControlService(context: Context?): FileAssembleControlService? {
+        return try {
+            RetrofitClient.instance().fileAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "云盘服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 新版云盘服务
+     */
+    fun getCloudFileControlService(context: Context?): CloudFileControlService? {
+        return try {
+            RetrofitClient.instance().cloudFileControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "云盘服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 新版云盘服务
+     */
+    fun getCloudFileV3ControlService(context: Context?): CloudFileV3ControlService? {
+        return try {
+            RetrofitClient.instance().cloudFileV3ControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "云盘服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 会议管理服务
+     */
+    fun getMeetingAssembleControlService(context: Context?): MeetingAssembleControlService? {
+        return try {
+            RetrofitClient.instance().meetingAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "会议管理模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 考勤管理服务
+     */
+    fun getAttendanceAssembleControlService(context: Context?): AttendanceAssembleControlService? {
+        return try {
+            RetrofitClient.instance().attendanceAssembleControlApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "考勤管理模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 论坛服务
+     */
+    fun getBBSAssembleControlService(context: Context?): BBSAssembleControlService? {
+        return try {
+            RetrofitClient.instance().bbsAssembleControlServiceApi()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "论坛服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 信息中心
+     */
+    fun getCMSAssembleControlService(context: Context?): CMSAssembleControlService? {
+        return try {
+            RetrofitClient.instance().cmsAssembleControlService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "信息中心服务模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 公共配置服务
+     */
+    fun getOrganizationAssembleCustomService(context: Context?): OrganizationAssembleCustomService? {
+        return try {
+            RetrofitClient.instance().organizationAssembleCustomService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+//            if (context!=null){
+//                XToast.toastLong(context, "公共配置服务模块异常,请联系管理员!")
+//            }
+            null
+        }
+    }
+
+    /**
+     * 门户模块
+     */
+    fun getPortalAssembleSurfaceService(context: Context?): PortalAssembleSurfaceService? {
+        return try {
+            RetrofitClient.instance().portalAssembleSurfaceService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "门户模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 日程管理
+     */
+    fun getCalendarAssembleService(context: Context?): CalendarAssembleControlService? {
+        return try {
+            RetrofitClient.instance().calendarAssembleControlService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "日程管理模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 消息服务
+     */
+    fun getMessageCommunicateService(context: Context?): MessageCommunicateService? {
+        return try {
+            RetrofitClient.instance().messageCommunicateService()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "消息服务器异常, 请联系管理员!")
+            }
+            null
+        }
+    }
+
+
+    /**
+     * 极光消息管理
+     */
+    private fun getJPushControlService(): JPushControlService? {
+        return try {
+            RetrofitClient.instance().jPushControlService()
+        } catch (e: Exception) {
+            XLog.error("极光消息推送模块异常,请联系管理员!", e)
+            null
+        }
+    }
+
+    /**
+     * 绑定设备
+     * 根据 后台配置 绑定 设备号
+     */
+    override fun jPushBindDevice() {
+        XLog.info("绑定设备号")
+        val service = getJPushControlService()
+        if (service != null) {
+            service.deviceConfigPushType().subscribeOn(Schedulers.io())
+                .flatMap { res ->
+                    val config = res.data
+                    if (config != null && !TextUtils.isEmpty(config.pushType)) {
+                        val deviceToken = if (config.pushType == PushType.HUAWEI_TYPE) {
+                            O2SDKManager.instance().prefs()
+                                .getString(O2.PRE_PUSH_HUAWEI_DEVICE_ID_KEY, "") ?: ""
+                        } else {
+                            O2SDKManager.instance().prefs()
+                                .getString(O2.PRE_PUSH_JPUSH_DEVICE_ID_KEY, "") ?: ""
+                        }
+                        val form = JPushDeviceForm(deviceToken, config.pushType)
+                        service.deviceBind(form)
+                    } else {
+                        val deviceToken = O2SDKManager.instance().prefs()
+                            .getString(O2.PRE_PUSH_JPUSH_DEVICE_ID_KEY, "") ?: ""
+                        val form = JPushDeviceForm(deviceToken, PushType.JPUSH_TYPE)
+                        service.deviceBind(form)
+                    }
+                }.observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext { res ->
+                        XLog.info("绑定设备,结果:${res.data.isValue}")
+                    }
+                    onError { e, _ ->
+                        XLog.error("绑定设备出错,", e)
+                        jpushBindOldRetry()
+                    }
+                }
+        } else {
+            XLog.error("没有极光推送模块")
+        }
+    }
+
+    /**
+     * 适配老版本 重试一下
+     */
+    private fun jpushBindOldRetry() {
+        val service = getJPushControlService()
+        if (service != null) {
+            val deviceToken =
+                O2SDKManager.instance().prefs().getString(O2.PRE_PUSH_JPUSH_DEVICE_ID_KEY, "") ?: ""
+            val form = JPushDeviceForm(deviceToken, PushType.JPUSH_TYPE)
+            service.deviceBind(form).subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext { res ->
+                        XLog.info("老版接口: 绑定设备,结果:${res.data.isValue}")
+                        XLog.info("老版接口: 绑定设备,message:${res.message}")
+                    }
+                    onError { e, _ ->
+                        XLog.error("老版接口: 绑定设备出错,", e)
+                    }
+                }
+        }
+    }
+
+    /**
+     * 解除绑定设备 内部直连版本使用
+     */
+    override fun jPushUnBindDevice() {
+        XLog.info("解除绑定设备号")
+        val service = getJPushControlService()
+        if (service != null) {
+            service.deviceConfigPushType().subscribeOn(Schedulers.io())
+                .flatMap { res ->
+                    val config = res.data
+                    if (config != null && !TextUtils.isEmpty(config.pushType)) {
+                        val deviceToken = if (config.pushType == PushType.HUAWEI_TYPE) {
+                            O2SDKManager.instance().prefs()
+                                .getString(O2.PRE_PUSH_HUAWEI_DEVICE_ID_KEY, "") ?: ""
+                        } else {
+                            O2SDKManager.instance().prefs()
+                                .getString(O2.PRE_PUSH_JPUSH_DEVICE_ID_KEY, "") ?: ""
+                        }
+                        service.deviceUnBindNew(deviceToken, config.pushType)
+                    } else {
+                        val deviceToken = O2SDKManager.instance().prefs()
+                            .getString(O2.PRE_PUSH_JPUSH_DEVICE_ID_KEY, "") ?: ""
+                        service.deviceUnBindNew(deviceToken, PushType.JPUSH_TYPE)
+                    }
+                }.observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext { res ->
+                        XLog.info("解除绑定,结果:${res.data.isValue}")
+                    }
+                    onError { e, _ ->
+                        XLog.error("解除绑定出错,", e)
+                        jpushUnbindOldRetry()
+                    }
+                }
+
+
+        } else {
+            XLog.error("没有极光推送模块")
+        }
+    }
+
+    private fun jpushUnbindOldRetry() {
+        val service = getJPushControlService()
+        if (service != null) {
+            val deviceToken =
+                O2SDKManager.instance().prefs().getString(O2.PRE_PUSH_JPUSH_DEVICE_ID_KEY, "") ?: ""
+            service.deviceUnBind(deviceToken).subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .o2Subscribe {
+                    onNext { res ->
+                        XLog.info("解除绑定,结果:${res.data.isValue}")
+                    }
+                    onError { e, _ ->
+                        XLog.error("解除绑定出错,", e)
+                    }
+                }
+        }
+    }
+
+
+    override fun getTaskNumber(context: Context?, tv: TextView, tvTag: String) {
+        XLog.debug("getTaskNumber 。。。。。。。。。。。。。。")
+        val service = getProcessAssembleSurfaceServiceAPI(context)
+        service?.countWithPerson(O2SDKManager.instance().distinguishedName)
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())?.o2Subscribe {
+                onNext { res ->
+                    XLog.debug("getTaskNumber 。。。。。。。。。。。。。。1111")
+                    if (res.data != null) {
+                        val taskNumber = res.data.task ?: 0
+                        if (taskNumber > 0 && tv.tag != null) {
+                            val tag = tv.tag as String
+                            if (tag == tvTag) {
+                                tv.text = "$taskNumber"
+                                tv.visible()
+                            }
+                        }
+                    }
+                }
+                onError { e, isNetworkError ->
+                    XLog.error("", e)
+                }
+            }
+    }
+
+    override fun getPortalNumber(context: Context?, tv: TextView, portalId: String) {
+        XLog.debug("getPortalNumber ..................")
+        getPortalAssembleSurfaceService(context)
+            ?.cornerMarkNumber(portalId)
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())
+            ?.o2Subscribe {
+                onNext {
+                    if (it.data != null && it.data.count > 0 && tv.tag != null) {
+                        val tag = tv.tag as String
+                        if (tag == portalId) {
+                            tv.text = "${it.data.count}"
+                            tv.visible()
+                        }
+                    }
+                }
+                onError { e, _ ->
+                    XLog.error("", e)
+                }
+            }
+    }
+
+    override fun getReadNumber(context: Context?, tv: TextView, tvTag: String) {
+        XLog.debug("getReadNumber 。。。。。。。。。。。。。。")
+        val service = getProcessAssembleSurfaceServiceAPI(context)
+        service?.countWithPerson(O2SDKManager.instance().distinguishedName)
+            ?.subscribeOn(Schedulers.io())
+            ?.observeOn(AndroidSchedulers.mainThread())?.o2Subscribe {
+                onNext { res ->
+                    XLog.debug("getReadNumber 。。。。。。。。。。。1111。。。")
+                    if (res.data != null) {
+                        val readNumber = res.data.read ?: 0
+                        if (readNumber > 0 && tv.tag != null) {
+                            val tag = tv.tag as String
+                            if (tag == tvTag) {
+                                tv.text = "$readNumber"
+                                tv.visible()
+                            }
+                        }
+                    }
+                }
+                onError { e, isNetworkError ->
+                    XLog.error("", e)
+                }
+            }
+    }
+
+    /**
+     * 图灵 v1 服务
+     */
+    fun getTuling123Service(context: Context?): Tuling123Service? {
+        return try {
+            RetrofitClient.instance().tuling123Service()
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "AI查询模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+
+    /**
+     * 人脸识别服务
+     */
+    fun getFaceppService(baseUrl: String, context: Context?): FaceppApiService? {
+        return try {
+            RetrofitClient.instance().faceppApiService(baseUrl)
+        } catch (e: Exception) {
+            XLog.error("", e)
+            if (context != null) {
+                XToast.toastLong(context, "人脸识别模块异常,请联系管理员!")
+            }
+            null
+        }
+    }
+}

+ 14 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/base/BaseView.kt

@@ -0,0 +1,14 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base
+
+import android.content.Context
+
+/**
+ * Created by fancy on 2017/6/5.
+ */
+
+
+
+interface BaseView {
+
+    fun getContext(): Context?
+}

+ 85 - 0
app/src/main/java/net/zoneland/x/bpm/mobile/v1/zoneXBPM/app/bbs/main/BBSMainActivity.kt

@@ -0,0 +1,85 @@
+package net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.bbs.main
+
+
+import android.os.Bundle
+import com.google.android.material.tabs.TabLayout
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentPagerAdapter
+import kotlinx.android.synthetic.main.activity_bbs_main.*
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.R
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.base.BaseMVPActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.app.bbs.section.BBSSectionActivity
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.XLog
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.go
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.gone
+import net.zoneland.x.bpm.mobile.v1.zoneXBPM.utils.extension.visible
+
+
+class BBSMainActivity : BaseMVPActivity<BBSMainContract.View, BBSMainContract.Presenter>(), BBSMainContract.View {
+    override var mPresenter: BBSMainContract.Presenter = BBSMainPresenter()
+
+
+    override fun layoutResId(): Int = R.layout.activity_bbs_main
+
+    val titles:Array<String> by lazy { resources.getStringArray(R.array.bbs_main_page_list) }
+    val fragments = ArrayList<Fragment>()
+    val pagerAdapter : FragmentPagerAdapter by lazy {
+        object : FragmentPagerAdapter(supportFragmentManager){
+            override fun getItem(position: Int): Fragment = fragments[position]
+
+            override fun getCount(): Int = fragments.size
+
+            override fun getPageTitle(position: Int): CharSequence = titles[position]
+        }
+    }
+    override fun afterSetContentView(savedInstanceState: Bundle?) {
+        setupToolBar("", true, true)
+        fragments.add(BBSMainCollectionFragment())
+        fragments.add(BBSMainSectionFragment())
+        view_pager_bbs_main.adapter = pagerAdapter
+        toolbar_snippet_tab_layout.tabMode = TabLayout.MODE_FIXED
+        toolbar_snippet_tab_layout.setupWithViewPager(view_pager_bbs_main)
+        tv_bottom_bbs_main_cancel_collect_button.setOnClickListener { (fragments[0] as BBSMainCollectionFragment).cancelCollection() }
+        mPresenter.whetherThereHasCollections()
+        mPresenter.checkHasMute()
+    }
+
+    override fun onBackPressed() {
+        if (view_pager_bbs_main.currentItem == 0) {
+            val first = (fragments[0] as BBSMainCollectionFragment)
+            if (first.canCheck){
+                first.hideCheckTool()
+                return
+            }
+        }
+        super.onBackPressed()
+
+    }
+
+    override fun finish() {
+        super.finish()
+        overridePendingTransition(R.anim.activity_scale_in, R.anim.activity_scale_out)
+    }
+
+    override fun whetherThereHasAnyCollections(flag: Boolean) {
+        if (flag) {
+            view_pager_bbs_main.currentItem = 0
+        }else {
+            view_pager_bbs_main.currentItem = 1
+        }
+    }
+
+    fun showCancelButton() {
+        tv_bottom_bbs_main_cancel_collect_button.visible()
+    }
+
+    fun hideCancelButton() {
+        tv_bottom_bbs_main_cancel_collect_button.gone()
+    }
+
+    fun enterBBSSection(sectionId:String, sectionName:String) {
+        XLog.debug("点击论坛板块:$sectionName, id:$sectionId")
+        go<BBSSectionActivity>(BBSSectionActivity.startBundleData(sectionId, sectionName))
+    }
+
+}

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff