Browse Source

fisrt commit

lijilei 3 years ago
commit
7d4aad4428
100 changed files with 8266 additions and 0 deletions
  1. 10 0
      .babelrc
  2. 3 0
      .bowerrc
  3. 130 0
      .env
  4. 131 0
      .envbkk
  5. 12 0
      .eslintrc.json
  6. 18 0
      .gitignore
  7. 1 0
      .htaccess
  8. 42 0
      .travis.yml
  9. 191 0
      LICENSE
  10. 79 0
      README.md
  11. 0 0
      TPL_CALLBACK_adfsdf
  12. 1 0
      addons/.gitkeep
  13. 31 0
      addons/alisms/Alisms.php
  14. 49 0
      addons/alisms/config.php
  15. 49 0
      addons/alisms/controller/Index.php
  16. 7 0
      addons/alisms/info.ini
  17. 178 0
      addons/alisms/library/Alisms.php
  18. 63 0
      addons/alisms/view/index/index.html
  19. 46 0
      addons/loginbg/Loginbg.php
  20. 37 0
      addons/loginbg/config.php
  21. 16 0
      addons/loginbg/controller/Index.php
  22. 7 0
      addons/loginbg/info.ini
  23. 31 0
      addons/summernote/Summernote.php
  24. 59 0
      addons/summernote/bootstrap.js
  25. 5 0
      addons/summernote/config.php
  26. 16 0
      addons/summernote/controller/Index.php
  27. 7 0
      addons/summernote/info.ini
  28. 93 0
      addons/wechat/Wechat.php
  29. 109 0
      addons/wechat/application/admin/controller/wechat/Autoreply.php
  30. 152 0
      addons/wechat/application/admin/controller/wechat/Config.php
  31. 106 0
      addons/wechat/application/admin/controller/wechat/Menu.php
  32. 83 0
      addons/wechat/application/admin/controller/wechat/Response.php
  33. 8 0
      addons/wechat/application/admin/lang/zh-cn/wechat/autoreply.php
  34. 10 0
      addons/wechat/application/admin/lang/zh-cn/wechat/config.php
  35. 8 0
      addons/wechat/application/admin/lang/zh-cn/wechat/response.php
  36. 51 0
      addons/wechat/application/admin/view/wechat/autoreply/add.html
  37. 51 0
      addons/wechat/application/admin/view/wechat/autoreply/edit.html
  38. 28 0
      addons/wechat/application/admin/view/wechat/autoreply/index.html
  39. 45 0
      addons/wechat/application/admin/view/wechat/config/add.html
  40. 47 0
      addons/wechat/application/admin/view/wechat/config/edit.html
  41. 28 0
      addons/wechat/application/admin/view/wechat/config/index.html
  42. 84 0
      addons/wechat/application/admin/view/wechat/menu/index.html
  43. 48 0
      addons/wechat/application/admin/view/wechat/response/add.html
  44. 52 0
      addons/wechat/application/admin/view/wechat/response/edit.html
  45. 21 0
      addons/wechat/application/admin/view/wechat/response/index.html
  46. 16 0
      addons/wechat/application/admin/view/wechat/response/select.html
  47. 16 0
      addons/wechat/application/common/model/WechatAutoreply.php
  48. 32 0
      addons/wechat/application/common/model/WechatConfig.php
  49. 16 0
      addons/wechat/application/common/model/WechatContext.php
  50. 16 0
      addons/wechat/application/common/model/WechatResponse.php
  51. 119 0
      addons/wechat/config.php
  52. 199 0
      addons/wechat/controller/Index.php
  53. 7 0
      addons/wechat/info.ini
  54. 64 0
      addons/wechat/install.sql
  55. 82 0
      addons/wechat/library/Config.php
  56. 192 0
      addons/wechat/library/Wechat.php
  57. 77 0
      addons/wechat/public/assets/js/backend/wechat/autoreply.js
  58. 97 0
      addons/wechat/public/assets/js/backend/wechat/config.js
  59. 293 0
      addons/wechat/public/assets/js/backend/wechat/menu.js
  60. 180 0
      addons/wechat/public/assets/js/backend/wechat/response.js
  61. 1 0
      application/.htaccess
  62. 32 0
      application/Limit.php
  63. 16 0
      application/admin/behavior/AdminLog.php
  64. 114 0
      application/admin/command/AdUserDiff.php
  65. 230 0
      application/admin/command/Addon.php
  66. 45 0
      application/admin/command/Addon/stubs/addon.stub
  67. 40 0
      application/admin/command/Addon/stubs/config.stub
  68. 7 0
      application/admin/command/Addon/stubs/info.stub
  69. 44 0
      application/admin/command/BaseCommand.php
  70. 129 0
      application/admin/command/BookCollectSum.php
  71. 45 0
      application/admin/command/BookEnum.php
  72. 51 0
      application/admin/command/BookRelationInit.php
  73. 62 0
      application/admin/command/BuildTestUrl.php
  74. 91 0
      application/admin/command/CampaignAward.php
  75. 131 0
      application/admin/command/CampaignStatistics.php
  76. 253 0
      application/admin/command/ChangeMenu.php
  77. 163 0
      application/admin/command/ChangeMenuKefu.php
  78. 141 0
      application/admin/command/ChangeMenuSign.php
  79. 156 0
      application/admin/command/ChangeMenuSignDaiShu.php
  80. 218 0
      application/admin/command/ChangeMenuWx.php
  81. 162 0
      application/admin/command/ChangeMenuZhiChi.php
  82. 225 0
      application/admin/command/ChangeQRCode.php
  83. 98 0
      application/admin/command/CheckChannelRefreshToken.php
  84. 161 0
      application/admin/command/CheckPlatform.php
  85. 241 0
      application/admin/command/CheckWechatForbidden.php
  86. 51 0
      application/admin/command/ClearCampaignRedis.php
  87. 132 0
      application/admin/command/ClearUser.php
  88. 86 0
      application/admin/command/ClearUserBlack.php
  89. 1291 0
      application/admin/command/Crud.php
  90. 11 0
      application/admin/command/Crud/stubs/add.stub
  91. 37 0
      application/admin/command/Crud/stubs/controller.stub
  92. 31 0
      application/admin/command/Crud/stubs/controllerindex.stub
  93. 11 0
      application/admin/command/Crud/stubs/edit.stub
  94. 6 0
      application/admin/command/Crud/stubs/html/checkbox.stub
  95. 6 0
      application/admin/command/Crud/stubs/html/radio.stub
  96. 6 0
      application/admin/command/Crud/stubs/html/select.stub
  97. 33 0
      application/admin/command/Crud/stubs/index.stub
  98. 47 0
      application/admin/command/Crud/stubs/javascript.stub
  99. 5 0
      application/admin/command/Crud/stubs/lang.stub
  100. 8 0
      application/admin/command/Crud/stubs/mixins/checkbox.stub

+ 10 - 0
.babelrc

@@ -0,0 +1,10 @@
+{
+  "presets": [
+    [
+      "@babel/preset-env"
+    ]
+  ],
+  "plugins": [
+    "syntax-dynamic-import"
+  ]
+}

+ 3 - 0
.bowerrc

@@ -0,0 +1,3 @@
+{
+  "directory" : "public/assets/libs"
+}

+ 130 - 0
.env

@@ -0,0 +1,130 @@
+[app]
+return_filter_recent_num=5
+debug=true
+trace=true
+callback_tpl_cache=10
+callback_tpl_threshold=5
+book_click_expire=100
+
+
+[url]
+debug=flase
+[api]
+base_uri="http://192.168.0.20:9090"
+service_on=0
+external_uri=http://192.168.0.233:9098/statistic/portal/
+service_esdata_uri=http://192.168.0.233:9089
+
+[database]
+hostname="127.0.0.1"
+database="test_cps"
+username="root"
+password="03fbade231e78032"
+debug=true
+admin_hostname="127.0.0.1"
+admin_deploy=0
+admin_rw_separate=false
+
+[polardb]
+hostname="127.0.0.1"
+database="polardb"
+username="root"
+password="03fbade231e78032"
+hostport="3306"
+
+[db]
+shard_num=256
+shard_database="test_cps_shard_$mod"
+shard_list="0-127.0.0.1:3306:127.0.0.1:3306;127.0.0.1:3306:127.0.0.1:3306"
+user_num=512
+user_database="test_cps_user_$mod"
+user_list="0-255:127.0.0.1:3306;256-511:127.0.0.1:3306"
+shelf_num=512
+shelf_database="cps_shelf_$mod"
+shelf_list="0-255:192.168.0.121:3306:192.168.0.122:3306;256-511:192.168.0.121:3307:192.168.0.122:3307"
+
+[redis]
+auto="127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV"
+book="127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV"
+changebook='127.0.0.1:6379;127.0.0.1:6379;127.0.0.1:6379'
+list="0=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;1=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;2=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;3=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;4=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;5=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;6=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV"
+rules="2=BURC:,RUV_T:,RCT:,PCT:,DCT:,UCT:,KAV:,KAN:,KCV:,KCN:,KCR:,KAM:,KDM:,KIP:,CU_UV:,CU_UV_T:,CU_MONEY_T:,TP_UV:,TP_UV_T:,TP_MONEY_T:,CNR_D:,KL_C_USER_NEW:,KL_C_USER_LIMIT:,KL_C_USER_OLD_A:,KL_U_ORDER_T:,KL_U_ORDER_C:,KL_C_CITY:,collect_custom_list_new,collect_ref_pv_list_new;3=site;4=OPHOSTLIST;5=PAYHOST;6=ENTRYHOST"
+modkeylist="7=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;8=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;9=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;10=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;11=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;12=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;13=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;14=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV;15=127.0.0.1:6379:wrfg6OTNaXTqd96H7TK7bYIV"
+modkeyrules="SEB:,HC:,FFSN:,U-R:,U-B:,UOTD:,UR:,ZR:,SBIK:,BEHD:,BCT:,ANI:,ALI:,U-BC:,A-T-C:,A-T-P:,A-T-G:,A-I:,R-I:,A-R:,A-R-R:,BCA:,APPGUIDE:,ACOU:,BCCT:,BCN:,RI:,AA:,AG:,AE:,SU:,ZRP:,URP:,UTK:,OPH:,WXP:,PF:,CIEN:,P-A:,P:,edited_chapter:,B:,KL_U_CHAPTER:,GOODS_ID:,BL:,BBL:,BLPC:,CRANK:,RANK:,CBBL:,SED-RB:,CP:,FRB:,CC:,K:,NERB:,HS:,SRB:,SSP:,UPHONE:,RBID:,CHS:,CBL:,GUIDE:"
+change=1
+password="wrfg6OTNaXTqd96H7TK7bYIV"
+
+[ssdb]
+list="0=192.168.0.154:18888:273813e5f4041f6dce947bd06b737dac;1=192.168.0.154:28888:273813e5f4041f6dce947bd06b737dac"
+rules="1=IG:,IU:,HP:"
+
+[wechat]
+log_level="debug"
+work_secret="HbHdaLWlsCjaB8ua7AhJmrWZPreY3CSrzfzFux-E-7o"
+work_agent_id=1000004
+work_party_id=4
+work_sync_secret="HbHdaLWlsCjaB8ua7AhJmrWZPreY3CSrzfzFux-E-7o"
+work_sync_agent_id=1000004
+work_sync_party_id=4
+
+work_domain_corp_id="ww1077922c5e8c1221"
+work_domain_secret="nWb-8gGPGVe_iJzaZTXdzAkGF2X2yTrtLWkonMZoj9o"
+work_domain_agent_id=1000002
+work_domain_party_id=2
+
+
+[log]
+host="http://log.dev.koread.cn"
+
+[apm]
+bonree=1
+
+[pay]
+timeout=0
+sandbox=false
+
+[rabbitmq]
+host="192.168.0.154"
+port=5672
+login="rabbitmquser"
+password="rabbitmqpass"
+vhost="/test_154"
+pay_cannel_expire=60
+
+[rabbitmq-dot]
+host="192.168.0.154"
+port=5672
+login="rabbitmquser"
+password="rabbitmqpass"
+vhost="/test_154"
+pay_cannel_expire=60
+#host="106.3.147.78"
+#port=5672
+#login="mqadmin"
+#password="zmfLqyjA7Fj1joAhwqYy"
+#vhost="/ms_shell"
+[collect]
+merge_yesterday=1
+merge_today=1
+
+[dbcollect]
+hostname="127.0.0.1"
+hostport="3306"
+username="test_cps_user"
+password="test_cps_userpass123456"
+database="test_cps_collect"
+tables="custom_url,custom_url_collect,autoreply_collect,match_day_collect"
+deploy=0
+
+[abwechat]
+platform_id=1
+
+[elasticsearch]
+user_es_host="http://192.168.0.93:9200"
+user_es_index="cps_advanced_mass_messag_xigua"
+
+[kafka]
+topic_reply=server_cps_reply_154
+topic_reply_send=topic_reply_send_154
+topic_work_wechat=topic_reply_wechat_154
+servers_reply='192.168.0.90:9092'

+ 131 - 0
.envbkk

@@ -0,0 +1,131 @@
+[app]
+return_filter_recent_num=5
+debug=true
+trace=true
+callback_tpl_cache=10
+callback_tpl_threshold=5
+book_click_expire=100
+
+
+[url]
+debug=flase
+[api]
+base_uri="http://192.168.0.20:9090"
+service_on=0
+external_uri=http://192.168.0.233:9098/statistic/portal/
+service_esdata_uri=http://192.168.0.233:9089
+
+[database]
+hostname="192.168.0.147"
+database="test_cps"
+username="test_cps_user"
+password="test_cps_userpass123456"
+debug=true
+admin_hostname="192.168.0.147"
+admin_deploy=0
+admin_rw_separate=false
+
+[polardb]
+hostname="192.168.0.147"
+database="polardb"
+username="test_cps_user"
+password="test_cps_userpass123456"
+hostport="3306"
+
+[db]
+shard_num=256
+shard_database="test_cps_shard_$mod"
+shard_list="0-191:192.168.0.147:3307:192.168.0.147:3307;192-255:192.168.0.147:3306:192.168.0.147:3306"
+user_num=512
+user_database="test_cps_user_$mod"
+user_list="0-255:192.168.0.121:3306;256-511:192.168.0.121:3307"
+shelf_num=512
+shelf_database="cps_shelf_$mod"
+shelf_list="0-255:192.168.0.121:3306:192.168.0.122:3306;256-511:192.168.0.121:3307:192.168.0.122:3307"
+
+
+[redis]
+auto="192.168.0.154:16380:mima"
+book="192.168.0.22:17000:wrfg6OTNaXTqd96H7TK7bYIV"
+changebook='192.168.1.80:16380;192.168.1.80:16381;192.168.1.80:16382'
+list="0=192.168.0.154:6379:mima;1=192.168.0.154:16381:mima;2=192.168.0.154:16382:mima;3=192.168.0.154:16383:mima;4=192.168.0.154:16384:mima;5=192.168.0.154:16385:mima;6=192.168.0.154:16386:mima"
+rules="2=BURC:,RUV_T:,RCT:,PCT:,DCT:,UCT:,KAV:,KAN:,KCV:,KCN:,KCR:,KAM:,KDM:,KIP:,CU_UV:,CU_UV_T:,CU_MONEY_T:,TP_UV:,TP_UV_T:,TP_MONEY_T:,CNR_D:,KL_C_USER_NEW:,KL_C_USER_LIMIT:,KL_C_USER_OLD_A:,KL_U_ORDER_T:,KL_U_ORDER_C:,KL_C_CITY:,collect_custom_list_new,collect_ref_pv_list_new;3=site;4=OPHOSTLIST;5=PAYHOST;6=ENTRYHOST"
+modkeylist="7=192.168.0.154:16383:mima;8=192.168.0.154:16384:mima;9=192.168.0.154:16385:mima;10=192.168.0.154:16386:mima;11=192.168.0.154:16387:mima;12=192.168.0.154:16388:mima;13=192.168.0.154:16389:mima;14=192.168.0.154:16390:mima;15=192.168.0.154:16391:mima"
+modkeyrules="SEB:,HC:,FFSN:,U-R:,U-B:,UOTD:,UR:,ZR:,SBIK:,BEHD:,BCT:,ANI:,ALI:,U-BC:,A-T-C:,A-T-P:,A-T-G:,A-I:,R-I:,A-R:,A-R-R:,BCA:,APPGUIDE:,ACOU:,BCCT:,BCN:,RI:,AA:,AG:,AE:,SU:,ZRP:,URP:,UTK:,OPH:,WXP:,PF:,CIEN:,P-A:,P:,edited_chapter:,B:,KL_U_CHAPTER:,GOODS_ID:,BL:,BBL:,BLPC:,CRANK:,RANK:,CBBL:,SED-RB:,CP:,FRB:,CC:,K:,NERB:,HS:,SRB:,SSP:,UPHONE:,RBID:,CHS:,CBL:,GUIDE:"
+change=1
+password="wrfg6OTNaXTqd96H7TK7bYIV"
+
+[ssdb]
+list="0=192.168.0.154:18888:273813e5f4041f6dce947bd06b737dac;1=192.168.0.154:28888:273813e5f4041f6dce947bd06b737dac"
+rules="1=IG:,IU:,HP:"
+
+[wechat]
+log_level="debug"
+work_secret="HbHdaLWlsCjaB8ua7AhJmrWZPreY3CSrzfzFux-E-7o"
+work_agent_id=1000004
+work_party_id=4
+work_sync_secret="HbHdaLWlsCjaB8ua7AhJmrWZPreY3CSrzfzFux-E-7o"
+work_sync_agent_id=1000004
+work_sync_party_id=4
+
+work_domain_corp_id="ww1077922c5e8c1221"
+work_domain_secret="nWb-8gGPGVe_iJzaZTXdzAkGF2X2yTrtLWkonMZoj9o"
+work_domain_agent_id=1000002
+work_domain_party_id=2
+
+
+[log]
+host="http://log.dev.koread.cn"
+
+[apm]
+bonree=1
+
+[pay]
+timeout=0
+sandbox=false
+
+[rabbitmq]
+host="192.168.0.154"
+port=5672
+login="rabbitmquser"
+password="rabbitmqpass"
+vhost="/test_154"
+pay_cannel_expire=60
+
+[rabbitmq-dot]
+host="192.168.0.154"
+port=5672
+login="rabbitmquser"
+password="rabbitmqpass"
+vhost="/test_154"
+pay_cannel_expire=60
+#host="106.3.147.78"
+#port=5672
+#login="mqadmin"
+#password="zmfLqyjA7Fj1joAhwqYy"
+#vhost="/ms_shell"
+[collect]
+merge_yesterday=1
+merge_today=1
+
+[dbcollect]
+hostname="192.168.0.147"
+hostport="3306"
+username="test_cps_user"
+password="test_cps_userpass123456"
+database="test_cps_collect"
+tables="custom_url,custom_url_collect,autoreply_collect,match_day_collect"
+deploy=0
+
+[abwechat]
+platform_id=1
+
+[elasticsearch]
+user_es_host="http://192.168.0.93:9200"
+user_es_index="cps_advanced_mass_messag_xigua"
+
+[kafka]
+topic_reply=server_cps_reply_154
+topic_reply_send=topic_reply_send_154
+topic_work_wechat=topic_reply_wechat_154
+servers_reply='192.168.0.90:9092'

+ 12 - 0
.eslintrc.json

@@ -0,0 +1,12 @@
+{
+  "parserOptions": {
+    "ecmaVersion": 6,
+    "sourceType": "module",
+    "ecmaFeatures": {
+      "jsx": true
+    }
+  },
+  "rules": {
+    "semi": "error"
+  }
+}

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+/nbproject/
+/public/uploads/
+/public/ueditor/
+/public/assets/cppartner
+.idea
+*.log
+!.gitkeep
+.env-*
+node_modules
+/public/*.txt
+!/public/robots.txt
+!/public/robotsqy.txt
+!/public/2353326441.txt
+!/public/MP_verify_2Xro2SzIIcDbyvpL.txt
+/application/admin/command/Test*
+node_modules
+.DS_Store
+.vscode

+ 1 - 0
.htaccess

@@ -0,0 +1 @@
+ 

+ 42 - 0
.travis.yml

@@ -0,0 +1,42 @@
+sudo: false
+
+language: php
+
+branches:
+  only:
+    - stable
+
+cache:
+  directories:
+    - $HOME/.composer/cache
+
+before_install:
+  - composer self-update
+
+install:
+  - composer install --no-dev --no-interaction --ignore-platform-reqs
+  - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Core.zip .
+  - composer require --update-no-dev --no-interaction "topthink/think-image:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-migration:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-captcha:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-mongo:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-worker:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-helper:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-queue:^1.0"
+  - composer require --update-no-dev --no-interaction "topthink/think-angular:^1.0"
+  - composer require --dev --update-no-dev --no-interaction "topthink/think-testing:^1.0"
+  - zip -r --exclude='*.git*' --exclude='*.zip' --exclude='*.travis.yml' ThinkPHP_Full.zip .
+
+script:
+  - php think unit
+
+deploy:
+  provider: releases
+  api_key:
+    secure: TSF6bnl2JYN72UQOORAJYL+CqIryP2gHVKt6grfveQ7d9rleAEoxlq6PWxbvTI4jZ5nrPpUcBUpWIJHNgVcs+bzLFtyh5THaLqm39uCgBbrW7M8rI26L8sBh/6nsdtGgdeQrO/cLu31QoTzbwuz1WfAVoCdCkOSZeXyT/CclH99qV6RYyQYqaD2wpRjrhA5O4fSsEkiPVuk0GaOogFlrQHx+C+lHnf6pa1KxEoN1A0UxxVfGX6K4y5g4WQDO5zT4bLeubkWOXK0G51XSvACDOZVIyLdjApaOFTwamPcD3S1tfvuxRWWvsCD5ljFvb2kSmx5BIBNwN80MzuBmrGIC27XLGOxyMerwKxB6DskNUO9PflKHDPI61DRq0FTy1fv70SFMSiAtUv9aJRT41NQh9iJJ0vC8dl+xcxrWIjU1GG6+l/ZcRqVx9V1VuGQsLKndGhja7SQ+X1slHl76fRq223sMOql7MFCd0vvvxVQ2V39CcFKao/LB1aPH3VhODDEyxwx6aXoTznvC/QPepgWsHOWQzKj9ftsgDbsNiyFlXL4cu8DWUty6rQy8zT2b4O8b1xjcwSUCsy+auEjBamzQkMJFNlZAIUrukL/NbUhQU37TAbwsFyz7X0E/u/VMle/nBCNAzgkMwAUjiHM6FqrKKBRWFbPrSIixjfjkCnrMEPw=
+  file:
+    - ThinkPHP_Core.zip
+    - ThinkPHP_Full.zip
+  skip_cleanup: true
+  on:
+    tags: true

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2017 Karson
+
+   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.

+ 79 - 0
README.md

@@ -0,0 +1,79 @@
+FastAdmin是一款基于ThinkPHP5+Bootstrap的极速后台开发框架。
+===============
+
+
+## **主要特性**
+
+* 基于`Auth`验证的权限管理系统
+    * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
+    * 支持单管理员多角色
+    * 支持目录和控制器结构一键生成权限节点
+* 完善的前端功能组件开发
+    * 基于`AdminLTE`二次开发
+    * 基于`Bootstrap`开发,自适应手机、平板、PC
+    * 基于`RequireJS`进行JS模块管理,按需加载
+    * 基于`Less`进行样式开发
+    * 基于`Bower`进行前端组件包管理
+* 数据库表一键生成`CRUD`,包括控制器、模型、视图、JS、语言包
+* 一键压缩打包JS和CSS文件
+* 强大的插件扩展功能,在线安装卸载插件
+* 多语言支持,服务端及客户端支持
+* 无缝整合又拍云、七牛上传等云存储功能
+* 第三方登录(QQ、微信、微博)整合
+* Ucenter整合
+
+## **安装使用**
+
+http://doc.fastadmin.net
+
+## **在线演示**
+
+http://demo.fastadmin.net
+
+用户名:admin
+
+密 码:123456
+
+提 示:演示站数据无法进行删除和修改,只能新增,完整体验请下载源码安装体验
+
+## **界面截图**
+![控制台](https://git.oschina.net/uploads/images/2017/0411/113717_e99ff3e7_10933.png "控制台")
+
+## **问题反馈**
+
+在使用中有任何问题,请使用以下联系方式联系我们
+
+交流社区: http://forum.fastadmin.net
+
+QQ群: [636393962](https://jq.qq.com/?_wv=1027&k=487PNBb)
+
+Email: (karsonzhang#163.com, 把#换成@)
+
+weibo: [@karsonzhang](https://weibo.com/karsonzhang)
+
+Github: https://github.com/karsonzhang/fastadmin
+
+Git@OSC: https://git.oschina.net/karson/fastadmin
+
+## **特别鸣谢**
+
+感谢以下的项目,排名不分先后
+
+ThinkPHP:http://www.thinkphp.cn
+
+AdminLTE:https://almsaeedstudio.com
+
+Bootstrap:http://getbootstrap.com
+
+jQuery:http://jquery.com
+
+
+## 版权信息
+
+FastAdmin遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2018 by FastAdmin (http://www.fastadmin.net)
+
+All rights reserved。

+ 0 - 0
TPL_CALLBACK_adfsdf


+ 1 - 0
addons/.gitkeep

@@ -0,0 +1 @@
+

+ 31 - 0
addons/alisms/Alisms.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace addons\alisms;
+
+use think\Addons;
+
+/**
+ * Alisms
+ */
+class Alisms extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+}

+ 49 - 0
addons/alisms/config.php

@@ -0,0 +1,49 @@
+<?php
+
+return array (
+  0 => 
+  array (
+    'name' => 'key',
+    'title' => '应用key',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your key',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  1 => 
+  array (
+    'name' => 'secret',
+    'title' => '密钥secret',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your secret',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  2 => 
+  array (
+    'name' => 'sign',
+    'title' => '签名',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your sign',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+);

+ 49 - 0
addons/alisms/controller/Index.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace addons\alisms\controller;
+
+use think\addons\Controller;
+
+/**
+ * 二维码生成
+ *
+ */
+class Index extends Controller
+{
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+    }
+
+    // 
+    public function index()
+    {
+        return $this->view->fetch();
+    }
+
+    public function send()
+    {
+        $mobile = $this->request->post('mobile');
+        $template = $this->request->post('template');
+        $sign = $this->request->post('sign');
+        $param = (array) json_decode($this->request->post('param'));
+        $alisms = new \addons\alisms\library\Alisms();
+        $ret = $alisms->mobile($mobile)
+                ->template($template)
+                ->sign($sign)
+                ->param($param)
+                ->send();
+        if ($ret)
+        {
+            $this->success("发送成功");
+        }
+        else
+        {
+            $this->error("发送失败!失败原因:" . $alisms->getError());
+        }
+    }
+
+}

+ 7 - 0
addons/alisms/info.ini

@@ -0,0 +1,7 @@
+name = alisms
+title = 阿里短信发送
+intro = 阿里短信发送插件
+author = Karson
+website = http://www.fastadmin.net
+version = 1.0.0
+state = 1

+ 178 - 0
addons/alisms/library/Alisms.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace addons\alisms\library;
+
+use think\Config;
+
+/**
+ * 阿里大于SMS短信发送
+ */
+class Alisms
+{
+
+    private $_params = [];
+    public $error = '';
+    protected $config = [];
+
+    public function __construct($options = [])
+    {
+        if ($config = get_addon_config('alisms'))
+        {
+            $this->config = array_merge($this->config, $config);
+        }
+        $this->config = array_merge($this->config, is_array($options) ? $options : []);
+    }
+
+    /**
+     * 单例
+     * @param array $options 参数
+     * @return Alisms
+     */
+    public static function instance($options = [])
+    {
+        if (is_null(self::$instance))
+        {
+            self::$instance = new static($options);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * 设置签名
+     * @param string $sign
+     * @return Alisms
+     */
+    public function sign($sign = '')
+    {
+        $this->_params['SignName'] = $sign;
+        return $this;
+    }
+
+    /**
+     * 设置参数
+     * @param array $param
+     * @return Alisms
+     */
+    public function param(array $param = [])
+    {
+        foreach ($param as $k => &$v)
+        {
+            $v = (string) $v;
+        }
+        unset($v);
+        $this->_params['TemplateParam'] = json_encode($param);
+        return $this;
+    }
+
+    /**
+     * 设置模板
+     * @param string $code 短信模板
+     * @return Alisms
+     */
+    public function template($code = '')
+    {
+        $this->_params['TemplateCode'] = $code;
+        return $this;
+    }
+
+    /**
+     * 接收手机
+     * @param string $mobile 手机号码
+     * @return Alisms
+     */
+    public function mobile($mobile = '')
+    {
+        $this->_params['PhoneNumbers'] = $mobile;
+        return $this;
+    }
+
+    /**
+     * 立即发送
+     * @return boolean
+     */
+    public function send()
+    {
+        $this->error = '';
+        $params = $this->_params();
+        $params['Signature'] = $this->_signed($params);
+        $response = $this->_curl($params);
+        if ($response !== FALSE)
+        {
+            $res = (array) json_decode($response, TRUE);
+            if (isset($res['Code']) && $res['Code'] == 'OK')
+                return TRUE;
+            $this->error = isset($res['Message']) ? $res['Message'] : 'InvalidResult';
+        }
+        else
+        {
+            $this->error = 'InvalidResult';
+        }
+        return FALSE;
+    }
+
+    /**
+     * 获取错误信息
+     * @return array
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    private function _params()
+    {
+        return array_merge([
+            'AccessKeyId'      => $this->config['key'],
+            'SignName'         => isset($this->config['sign']) ? $this->config['sign'] : '',
+            'Action'           => 'SendSms',
+            'Format'           => 'JSON',
+            'Version'      => '2017-05-25',
+            'SignatureVersion' => '1.0',
+            'SignatureMethod'  => 'HMAC-SHA1',
+            'SignatureNonce'   => uniqid(),
+            'Timestamp'        => gmdate('Y-m-d\TH:i:s\Z'),
+                ], $this->_params);
+    }
+
+    private function percentEncode($string)
+    {
+        $string = urlencode($string);
+        $string = preg_replace('/\+/', '%20', $string);
+        $string = preg_replace('/\*/', '%2A', $string);
+        $string = preg_replace('/%7E/', '~', $string);
+        return $string;
+    }
+
+    private function _signed($params)
+    {
+        $sign = $this->config['secret'];
+        ksort($params);
+        $canonicalizedQueryString = '';
+        foreach ($params as $key => $value)
+        {
+            $canonicalizedQueryString .= '&' . $this->percentEncode($key) . '=' . $this->percentEncode($value);
+        }
+        $stringToSign = 'GET&%2F&' . $this->percentencode(substr($canonicalizedQueryString, 1));
+        $signature = base64_encode(hash_hmac('sha1', $stringToSign, $sign . '&', true));
+        return $signature;
+    }
+
+    private function _curl($params)
+    {
+        $uri = 'http://dysmsapi.aliyuncs.com/?' . http_build_query($params);
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
+        curl_setopt($ch, CURLOPT_URL, $uri);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
+        curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.98 Safari/537.36");
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
+        $reponse = curl_exec($ch);
+        curl_close($ch);
+        return $reponse;
+    }
+
+}

+ 63 - 0
addons/alisms/view/index/index.html

@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
+        <title>Alisms短信发送示例</title>
+        <link href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
+        <link href="//cdn.fastadmin.net/assets/css/frontend.min.css" rel="stylesheet">
+
+        <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
+        <!--[if lt IE 9]>
+          <script src="//cdn.bootcss.com/html5shiv/3.7.3/html5shiv.min.js"></script>
+          <script src="//cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
+        <![endif]-->
+    </head>
+    <body>
+        <div class="container">
+            <div class="well" style="margin-top:30px;">
+                <form class="form-horizontal" action="{:addon_url('alisms/index/send')}">
+                    <fieldset>
+                        <legend>阿里大于短信发送</legend>
+                        <div class="form-group">
+                            <label class="col-lg-2 control-label">手机号</label>
+                            <div class="col-lg-10">
+                                <input type="text" class="form-control" name="mobile" placeholder="手机号">
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <label class="col-lg-2 control-label">消息模板</label>
+                            <div class="col-lg-10">
+                                <input type="text" class="form-control" name="template" placeholder="消息模板">
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <label class="col-lg-2 control-label">签名</label>
+                            <div class="col-lg-10">
+                                <input type="text" class="form-control" name="sign" placeholder="消息模板(可以留空)">
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <label class="col-lg-2 control-label">消息参数</label>
+                            <div class="col-lg-10">
+                                <textarea name="param" class="form-control" cols="30" rows="10" placeholder="必须是JSON字符串"></textarea>
+                            </div>
+                        </div>
+                        <div class="form-group">
+                            <div class="col-lg-10 col-lg-offset-2">
+                                <button type="submit" class="btn btn-primary">发送</button>
+                                <button type="reset" class="btn btn-default">重置</button>
+                            </div>
+                        </div>
+                    </fieldset>
+                </form>
+            </div>
+        </div>
+        <script src="//cdn.bootcss.com/jquery/2.1.4/jquery.min.js"></script>
+        <script src="//cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
+        <script type="text/javascript">
+            $(function () {
+
+            });
+        </script>
+    </body>
+</html>

+ 46 - 0
addons/loginbg/Loginbg.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace addons\loginbg;
+
+use think\Addons;
+
+/**
+ * 登录背景图插件
+ */
+class Loginbg extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+    public function loginInit(\think\Request &$request)
+    {
+        $config = $this->getConfig();
+        if ($config['mode'] == 'random' || $config['mode'] == 'daily')
+        {
+            $index = $config['mode'] == 'random' ? mt_rand(1, 4000) : date("Ymd") % 4000;
+            $background = "http://img.infinitynewtab.com/wallpaper/" . $index . ".jpg";
+        }
+        else
+        {
+            $background = cdnurl($config['image']);
+        }
+        \think\View::instance()->assign('background', $background);
+    }
+
+}

+ 37 - 0
addons/loginbg/config.php

@@ -0,0 +1,37 @@
+<?php
+
+return array (
+  0 => 
+  array (
+    'name' => 'mode',
+    'title' => '模式',
+    'type' => 'radio',
+    'content' => 
+    array (
+      'fixed' => '固定',
+      'random' => '每次随机',
+      'daily' => '每日切换',
+    ),
+    'value' => 'fixed',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  1 => 
+  array (
+    'name' => 'image',
+    'title' => '固定背景图',
+    'type' => 'image',
+    'content' => 
+    array (
+    ),
+    'value' => Config("site.loginbg")?:'/assets/img/bg.jpg',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+);

+ 16 - 0
addons/loginbg/controller/Index.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace addons\loginbg\controller;
+
+use think\addons\Controller;
+
+class Index extends Controller
+{
+
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+
+}
+

+ 7 - 0
addons/loginbg/info.ini

@@ -0,0 +1,7 @@
+name = loginbg
+title = 登录背景图
+intro = 可自定义后台登录背景图
+author = Karson
+website = http://www.fastadmin.net
+version = 1.0.0
+state = 1

+ 31 - 0
addons/summernote/Summernote.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace addons\summernote;
+
+use think\Addons;
+
+/**
+ * Summernote插件
+ */
+class Summernote extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+}

+ 59 - 0
addons/summernote/bootstrap.js

@@ -0,0 +1,59 @@
+require(['form', 'upload'], function (Form, Upload) {
+    var _bindevent = Form.events.bindevent;
+    Form.events.bindevent = function (form) {
+        _bindevent.apply(this, [form]);
+        try {
+            //绑定summernote事件
+            if ($(".summernote,.editor", form).size() > 0) {
+                require(['summernote'], function () {
+                    $(".summernote,.editor", form).summernote({
+                        height: 250,
+                        lang: 'zh-CN',
+                        fontNames: [
+                            'Arial', 'Arial Black', 'Serif', 'Sans', 'Courier',
+                            'Courier New', 'Comic Sans MS', 'Helvetica', 'Impact', 'Lucida Grande',
+                            "Open Sans", "Hiragino Sans GB", "Microsoft YaHei",
+                            '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆',
+                        ],
+                        fontNamesIgnoreCheck: [
+                            "Open Sans", "Microsoft YaHei",
+                            '微软雅黑', '宋体', '黑体', '仿宋', '楷体', '幼圆'
+                        ],
+                        toolbar: [
+                            ['style', ['style', 'undo', 'redo']],
+                            ['font', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
+                            ['fontname', ['color', 'fontname', 'fontsize']],
+                            ['para', ['ul', 'ol', 'paragraph', 'height']],
+                            ['table', ['table', 'hr']],
+                            ['insert', ['link', 'picture', 'video']],
+                            ['view', ['fullscreen', 'codeview', 'help']]
+                        ],
+                        dialogsInBody: true,
+                        callbacks: {
+                            onChange: function (contents) {
+                                $(this).val(contents);
+                                $(this).trigger('change');
+                            },
+                            onInit: function () {
+                                $('.note-toolbar').css('z-index', 10); //修复浮动层遮挡下拉浮窗问题
+                            },
+                            onImageUpload: function (files) {
+                                var that = this;
+                                //依次上传图片
+                                for (var i = 0; i < files.length; i++) {
+                                    Upload.api.send(files[i], function (data) {
+                                        var url = Fast.api.cdnurl(data.url);
+                                        $(that).summernote("insertImage", url, 'filename');
+                                    });
+                                }
+                            }
+                        }
+                    });
+                });
+            }
+        } catch (e) {
+
+        }
+
+    };
+});

+ 5 - 0
addons/summernote/config.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+    
+];

+ 16 - 0
addons/summernote/controller/Index.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace addons\summernote\controller;
+
+use think\addons\Controller;
+
+class Index extends Controller
+{
+
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+
+}
+

+ 7 - 0
addons/summernote/info.ini

@@ -0,0 +1,7 @@
+name = summernote
+title = Summernote插件
+intro = 修改后台默认编辑器为Summernote
+author = Karson
+website = http://www.fastadmin.net
+version = 1.0.1
+state = 1

+ 93 - 0
addons/wechat/Wechat.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace addons\wechat;
+
+use app\common\library\Menu;
+use think\Addons;
+
+/**
+ * 微信插件
+ */
+class Wechat extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        $menu = [
+            [
+                'name'    => 'wechat',
+                'title'   => '微信管理',
+                'icon'    => 'fa fa-wechat',
+                'sublist' => [
+                    [
+                        'name'    => 'wechat/autoreply',
+                        'title'   => '自动回复管理',
+                        'icon'    => 'fa fa-reply-all',
+                        'sublist' => [
+                            ['name' => 'wechat/autoreply/index', 'title' => '查看'],
+                            ['name' => 'wechat/autoreply/add', 'title' => '添加'],
+                            ['name' => 'wechat/autoreply/edit', 'title' => '修改'],
+                            ['name' => 'wechat/autoreply/del', 'title' => '删除'],
+                            ['name' => 'wechat/autoreply/multi', 'title' => '批量更新'],
+                        ]
+                    ],
+                    [
+                        'name'    => 'wechat/config',
+                        'title'   => '配置管理',
+                        'icon'    => 'fa fa-cog',
+                        'sublist' => [
+                            ['name' => 'wechat/config/index', 'title' => '查看'],
+                            ['name' => 'wechat/config/add', 'title' => '添加'],
+                            ['name' => 'wechat/config/edit', 'title' => '修改'],
+                            ['name' => 'wechat/config/del', 'title' => '删除'],
+                            ['name' => 'wechat/config/multi', 'title' => '批量更新'],
+                        ]
+                    ],
+                    [
+                        'name'    => 'wechat/menu',
+                        'title'   => '菜单管理',
+                        'icon'    => 'fa fa-list',
+                        'sublist' => [
+                            ['name' => 'wechat/menu/index', 'title' => '查看'],
+                            ['name' => 'wechat/menu/add', 'title' => '添加'],
+                            ['name' => 'wechat/menu/edit', 'title' => '修改'],
+                            ['name' => 'wechat/menu/del', 'title' => '删除'],
+                            ['name' => 'wechat/menu/sync', 'title' => '同步'],
+                            ['name' => 'wechat/menu/multi', 'title' => '批量更新'],
+                        ]
+                    ],
+                    [
+                        'name'    => 'wechat/response',
+                        'title'   => '资源管理',
+                        'icon'    => 'fa fa-list-alt',
+                        'sublist' => [
+                            ['name' => 'wechat/response/index', 'title' => '查看'],
+                            ['name' => 'wechat/response/add', 'title' => '添加'],
+                            ['name' => 'wechat/response/edit', 'title' => '修改'],
+                            ['name' => 'wechat/response/del', 'title' => '删除'],
+                            ['name' => 'wechat/response/select', 'title' => '选择'],
+                            ['name' => 'wechat/response/multi', 'title' => '批量更新'],
+                        ]
+                    ]
+                ]
+            ]
+        ];
+        Menu::create($menu);
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        Menu::delete('wechat');
+        return true;
+    }
+
+}

+ 109 - 0
addons/wechat/application/admin/controller/wechat/Autoreply.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace app\admin\controller\wechat;
+
+use app\common\controller\Backend;
+use app\common\model\AutoreplyCollect;
+use app\common\model\WechatResponse;
+
+/**
+ * 微信自动回复管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Autoreply extends Backend
+{
+
+    protected $model = null;
+    protected $noNeedRight = ['check_text_unique'];
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('WechatAutoreply');
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //设置过滤方法
+        $this->request->filter(['strip_tags']);
+        if ($this->request->isAjax())
+        {
+            //如果发送的来源是Selectpage,则转发到Selectpage
+            if ($this->request->request('pkey_name'))
+            {
+                return $this->selectpage();
+            }
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->count();
+
+            $list = $this->model
+                ->where($where)
+                ->order($sort, $order)
+                ->limit($offset, $limit)
+                ->select();
+
+
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }
+
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = NULL)
+    {
+        $row = $this->model->get(['id' => $ids]);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        if ($this->request->isPost())
+        {
+            $params = $this->request->post("row/a");
+            if ($params)
+            {
+                $row->save($params);
+                $this->success();
+            }
+            $this->error();
+        }
+        $response = WechatResponse::get(['eventkey' => $row['eventkey']]);
+        $this->view->assign("response", $response);
+        $this->view->assign("row", $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 判断文本是否唯一
+     * @internal
+     */
+    public function check_text_unique()
+    {
+        $row = $this->request->post("row/a");
+        $except = $this->request->post("except");
+        $text = isset($row['text']) ? $row['text'] : '';
+        if ($this->model->where('text', $text)->where(function($query) use($except) {
+                    if ($except)
+                    {
+                        $query->where('text', '<>', $except);
+                    }
+                })->count() == 0)
+        {
+            return json(['ok' => '']);
+        }
+        else
+        {
+            return json(['error' => __('Text already exists')]);
+        }
+    }
+
+}

+ 152 - 0
addons/wechat/application/admin/controller/wechat/Config.php

@@ -0,0 +1,152 @@
+<?php
+
+namespace app\admin\controller\wechat;
+
+use app\common\controller\Backend;
+use think\Controller;
+use think\Request;
+
+/**
+ * 微信配置管理
+ *
+ * @icon fa fa-circle-o
+ */
+class Config extends Backend
+{
+
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('WechatConfig');
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost())
+        {
+            $params = $this->request->post("row/a");
+            if ($params)
+            {
+                foreach ($params as $k => &$v)
+                {
+                    $v = is_array($v) ? implode(',', $v) : $v;
+                }
+
+                if ($params['mode'] == 'json')
+                {
+                    //JSON字段
+                    $fieldarr = $valuearr = [];
+                    $field = $this->request->post('field/a');
+                    $value = $this->request->post('value/a');
+                    foreach ($field as $k => $v)
+                    {
+                        if ($v != '')
+                        {
+                            $fieldarr[] = $field[$k];
+                            $valuearr[] = $value[$k];
+                        }
+                    }
+                    $params['value'] = json_encode(array_combine($fieldarr, $valuearr), JSON_UNESCAPED_UNICODE);
+                }
+                unset($params['mode']);
+                try
+                {
+                    //是否采用模型验证
+                    if ($this->modelValidate)
+                    {
+                        $name = basename(str_replace('\\', '/', get_class($this->model)));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : true) : $this->modelValidate;
+                        $this->model->validate($validate);
+                    }
+                    $result = $this->model->save($params);
+                    if ($result !== false)
+                    {
+                        $this->success();
+                    }
+                    else
+                    {
+                        $this->error($this->model->getError());
+                    }
+                }
+                catch (\think\exception\PDOException $e)
+                {
+                    $this->error($e->getMessage());
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = NULL)
+    {
+        $row = $this->model->get($ids);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        if ($this->request->isPost())
+        {
+            $params = $this->request->post("row/a");
+            if ($params)
+            {
+                foreach ($params as $k => &$v)
+                {
+                    $v = is_array($v) ? implode(',', $v) : $v;
+                }
+
+                if ($params['mode'] == 'json')
+                {
+                    //JSON字段
+                    $fieldarr = $valuearr = [];
+                    $field = $this->request->post('field/a');
+                    $value = $this->request->post('value/a');
+                    foreach ($field as $k => $v)
+                    {
+                        if ($v != '')
+                        {
+                            $fieldarr[] = $field[$k];
+                            $valuearr[] = $value[$k];
+                        }
+                    }
+                    $params['value'] = json_encode(array_combine($fieldarr, $valuearr), JSON_UNESCAPED_UNICODE);
+                }
+                unset($params['mode']);
+                try
+                {
+                    //是否采用模型验证
+                    if ($this->modelValidate)
+                    {
+                        $name = basename(str_replace('\\', '/', get_class($this->model)));
+                        $validate = is_bool($this->modelValidate) ? ($this->modelSceneValidate ? $name . '.add' : true) : $this->modelValidate;
+                        $row->validate($validate);
+                    }
+                    $result = $row->save($params);
+                    if ($result !== false)
+                    {
+                        $this->success();
+                    }
+                    else
+                    {
+                        $this->error($row->getError());
+                    }
+                }
+                catch (think\exception\PDOException $e)
+                {
+                    $this->error($e->getMessage());
+                }
+            }
+            $this->error(__('Parameter %s can not be empty', ''));
+        }
+        $this->view->assign("row", $row);
+        $this->view->assign("value", (array) json_decode($row->value, true));
+        return $this->view->fetch();
+    }
+
+}

+ 106 - 0
addons/wechat/application/admin/controller/wechat/Menu.php

@@ -0,0 +1,106 @@
+<?php
+
+namespace app\admin\controller\wechat;
+
+use app\common\controller\Backend;
+use app\common\model\WechatResponse;
+use EasyWeChat\Foundation\Application;
+use think\Exception;
+
+/**
+ * 菜单管理
+ *
+ * @icon fa fa-list-alt
+ */
+class Menu extends Backend
+{
+
+    protected $wechatcfg = NULL;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->wechatcfg = \app\common\model\WechatConfig::get(['name' => 'menu']);
+    }
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        $responselist = array();
+        $all = WechatResponse::all();
+        foreach ($all as $k => $v)
+        {
+            $responselist[$v['eventkey']] = $v['title'];
+        }
+        $this->view->assign('responselist', $responselist);
+        $this->view->assign('menu', (array) json_decode($this->wechatcfg->value, TRUE));
+        return $this->view->fetch();
+    }
+
+    /**
+     * 修改
+     */
+    public function edit($ids = NULL)
+    {
+        $menu = $this->request->post("menu");
+        $menu = (array) json_decode($menu, TRUE);
+        $this->wechatcfg->value = json_encode($menu, JSON_UNESCAPED_UNICODE);
+        $this->wechatcfg->save();
+        $this->success();
+    }
+
+    /**
+     * 同步
+     */
+    public function sync($ids = NULL)
+    {
+        $app = new Application(get_addon_config('wechat'));
+        try
+        {
+            $hasError = false;
+            $menu = json_decode($this->wechatcfg->value, TRUE);
+            foreach ($menu as $k => $v)
+            {
+                if (isset($v['sub_button']))
+                {
+                    foreach ($v['sub_button'] as $m => $n)
+                    {
+                        if (isset($n['key']) && !$n['key'])
+                        {
+                            $hasError = true;
+                            break 2;
+                        }
+                    }
+                }
+                else if (isset($v['key']) && !$v['key'])
+                {
+                    $hasError = true;
+                    break;
+                }
+            }
+            if (!$hasError)
+            {
+                $ret = $app->menu->add($menu);
+                if ($ret->errcode == 0)
+                {
+                    $this->success();
+                }
+                else
+                {
+                    $this->error($ret->errmsg);
+                }
+            }
+            else
+            {
+                $this->error(__('Invalid parameters'));
+            }
+        }
+        catch (Exception $e)
+        {
+            $this->error($e->getMessage());
+        }
+    }
+
+}

+ 83 - 0
addons/wechat/application/admin/controller/wechat/Response.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace app\admin\controller\wechat;
+
+use app\common\controller\Backend;
+use addons\wechat\library\Wechat;
+
+/**
+ * 资源管理
+ *
+ * @icon fa fa-list-alt
+ */
+class Response extends Backend
+{
+
+    protected $model = null;
+    protected $searchFields = 'id,title';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('WechatResponse');
+    }
+
+    /**
+     * 选择素材
+     */
+    public function select()
+    {
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加
+     */
+    public function add()
+    {
+        if ($this->request->isPost())
+        {
+            $params = $this->request->post("row/a");
+            $params['eventkey'] = isset($params['eventkey']) && $params['eventkey'] ? $params['eventkey'] : uniqid();
+            $params['content'] = json_encode($params['content']);
+            $params['createtime'] = time();
+            if ($params)
+            {
+                $this->model->save($params);
+                $this->success();
+                $this->content = $params;
+            }
+            $this->error();
+        }
+        $appConfig = Wechat::appConfig();
+        $this->view->applist = $appConfig;
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑
+     */
+    public function edit($ids = NULL)
+    {
+        $row = $this->model->get($ids);
+        if (!$row)
+            $this->error(__('No Results were found'));
+        if ($this->request->isPost())
+        {
+            $params = $this->request->post("row/a");
+            $params['eventkey'] = isset($params['eventkey']) && $params['eventkey'] ? $params['eventkey'] : uniqid();
+            $params['content'] = json_encode($params['content']);
+            if ($params)
+            {
+                $row->save($params);
+                $this->success();
+            }
+            $this->error();
+        }
+        $this->view->assign("row", $row);
+        $appConfig = Wechat::appConfig();
+        $this->view->applist = $appConfig;
+        return $this->view->fetch();
+    }
+
+}

+ 8 - 0
addons/wechat/application/admin/lang/zh-cn/wechat/autoreply.php

@@ -0,0 +1,8 @@
+<?php
+
+return [
+    'Text'                => '文本',
+    'Event key'           => '响应标识',
+    'Remark'              => '备注',
+    'Text already exists' => '文本已经存在',
+];

+ 10 - 0
addons/wechat/application/admin/lang/zh-cn/wechat/config.php

@@ -0,0 +1,10 @@
+<?php
+
+return [
+    'name'       => '配置名称',
+    'value'      => '配置值',
+    'Json key'   => '键',
+    'Json value' => '值',
+    'createtime' => '创建时间',
+    'updatetime' => '更新时间'
+];

+ 8 - 0
addons/wechat/application/admin/lang/zh-cn/wechat/response.php

@@ -0,0 +1,8 @@
+<?php
+
+return [
+    'Resource title' => '资源标题',
+    'Event key'      => '事件标识',
+    'Text'           => '文本',
+    'App'            => '应用',
+];

+ 51 - 0
addons/wechat/application/admin/view/wechat/autoreply/add.html

@@ -0,0 +1,51 @@
+<link href="{:$site['cdnurl']}/assets/css/wechat/menu.css?v={:$site['version']}" rel="stylesheet">
+<style>
+    .clickbox {margin:0;text-align: left;}
+    .create-click {
+        margin-left:0;
+    }
+</style>
+<form id="add-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[title]" value=""  id="c-title" class="form-control" data-rule="required" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-text" class="control-label col-xs-12 col-sm-2">{:__('Text')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[text]" value=""  id="c-text" class="form-control" data-rule="required; remote(wechat/autoreply/check_text_unique)" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Event key')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="hidden" name="row[eventkey]" id="c-eventkey" class="form-control" value="" data-rule="required" readonly />
+            <div class="clickbox">
+                <span class="create-click"><a href="{:url('wechat.response/select')}" id="select-resources"><i class="weixin-icon big-add-gray"></i><strong>选择现有资源</strong></a></span>
+                <span class="create-click"><a href="{:url('wechat.response/add')}" id="add-resources"><i class="weixin-icon big-add-gray"></i><strong>添加新资源</strong></a></span>
+            </div>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-remark" class="control-label col-xs-12 col-sm-2">{:__('Remark')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[remark]" value=""  id="c-remark" class="form-control" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            {:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')])}
+        </div>
+    </div>
+    <div class="form-group hide layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+
+</form>

+ 51 - 0
addons/wechat/application/admin/view/wechat/autoreply/edit.html

@@ -0,0 +1,51 @@
+<link href="{:$site['cdnurl'] ?>/assets/css/wechat/menu.css?v={$site.version}" rel="stylesheet">
+<style>
+    .clickbox {margin:0;text-align: left;}
+    .create-click {
+        margin-left:0;
+    }
+</style>
+<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[title]" value="{$row.title}"  id="c-title" class="form-control" data-rule="required" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-text" class="control-label col-xs-12 col-sm-2">{:__('Text')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[text]" value="{$row.text}"  id="c-text" class="form-control" data-rule="required; remote(wechat/autoreply/check_text_unique, except={$row.text})" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-content" class="control-label col-xs-12 col-sm-2">{:__('Content')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <div class="clickbox">
+                <input type="hidden" name="row[eventkey]" id="c-eventkey" class="form-control" value="{$row.eventkey}" data-rule="required" readonly />
+                <span class="create-click"><a href="wechat/response/select" id="select-resources"><i class="weixin-icon big-add-gray"></i><strong>选择现有资源</strong></a><div class="keytitle">资源名:{:$response['title']}</div></span>
+                <span class="create-click"><a href="wechat/response/add" id="add-resources"><i class="weixin-icon big-add-gray"></i><strong>添加新资源</strong></a></span>
+            </div>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-remark" class="control-label col-xs-12 col-sm-2">{:__('Remark')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" name="row[remark]" value="{$row.remark}"  id="c-remark" class="form-control" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            {:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')], $row['status'])}
+        </div>
+    </div>
+    <div class="form-group hide layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 28 - 0
addons/wechat/application/admin/view/wechat/autoreply/index.html

@@ -0,0 +1,28 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar()}
+                        <div class="dropdown btn-group {:$auth->check('wechat/autoreply/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> <?= __('More') ?></a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('wechat/autoreply/edit')}" 
+                           data-operate-del="{:$auth->check('wechat/autoreply/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 45 - 0
addons/wechat/application/admin/view/wechat/config/add.html

@@ -0,0 +1,45 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <input type="hidden" name="row[mode]" value="textarea" />
+    <div class="form-group">
+        <label for="c-name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" value="">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-value" class="control-label col-xs-12 col-sm-2">{:__('Value')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <p>
+                <a href="javascript:;" class="btn btn-info btn-jsoneditor"><i class="fa fa-pencil"></i> {:__('Json editor')}</a>
+                <a href="javascript:;" class="btn btn-primary btn-insertlink"><i class="fa fa-link"></i> {:__('Insert link')}</a>
+            </p>
+            <textarea id="c-value" class="form-control " rows="15" name="row[value]"></textarea>
+            <dl class="fieldlist hide" rel="1">
+                <dd>
+                    <ins>{:__('Json key')}</ins>
+                    <ins>{:__('Json value')}</ins>
+                </dd>
+                <dd>
+                    <input type="text" name="field[0]" class="form-control" id="field-0" value="" size="10" required />
+                    <input type="text" name="value[0]" class="form-control" id="value-0" value="" size="40" required />
+                    <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span>
+                    <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span>
+                </dd>
+                <dd><a href="javascript:;" class="append btn btn-sm btn-success"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+            </dl>
+        </div>
+    </div>
+    <div class="form-group hide layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 47 - 0
addons/wechat/application/admin/view/wechat/config/edit.html

@@ -0,0 +1,47 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+    <input type="hidden" name="row[mode]" value="textarea" />
+    <div class="form-group">
+        <label for="c-name" class="control-label col-xs-12 col-sm-2">{:__('Name')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-name" data-rule="required" class="form-control" name="row[name]" type="text" value="{$row.name}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-title" class="control-label col-xs-12 col-sm-2">{:__('Title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input id="c-title" data-rule="required" class="form-control" name="row[title]" type="text" value="{$row.title}">
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="c-value" class="control-label col-xs-12 col-sm-2">{:__('Value')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <p>
+                <a href="javascript:;" class="btn btn-info btn-jsoneditor"><i class="fa fa-pencil"></i> {:__('Json editor')}</a>
+                <a href="javascript:;" class="btn btn-primary btn-insertlink"><i class="fa fa-link"></i> {:__('Insert link')}</a>
+            </p>
+            <textarea id="c-value" class="form-control " rows="15" name="row[value]">{$row.value}</textarea>
+            <dl class="fieldlist hide" rel="{$value|count}">
+                <dd>
+                    <ins>{:__('Json key')}</ins>
+                    <ins>{:__('Json value')}</ins>
+                </dd>
+                {foreach $value as $key => $vo}
+                <dd class="form-inline">
+                    <input type="text" name="field[{$key}]" class="form-control" id="field-{$key}" value="{$key}" size="10" />
+                    <input type="text" name="value[{$key}]" class="form-control" id="value-{$key}" value="{:is_array($vo)?'':$vo}" size="40" />
+                    <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span>
+                    <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span>
+                </dd>
+                {/foreach}
+                <dd><a href="javascript:;" class="append btn btn-sm btn-success"><i class="fa fa-plus"></i> {:__('Append')}</a></dd>
+            </dl>
+        </div>
+    </div>
+    <div class="form-group hide layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 28 - 0
addons/wechat/application/admin/view/wechat/config/index.html

@@ -0,0 +1,28 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar()}
+                        <div class="dropdown btn-group {:$auth->check('wechat/config/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('wechat/config/edit')}" 
+                           data-operate-del="{:$auth->check('wechat/config/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 84 - 0
addons/wechat/application/admin/view/wechat/menu/index.html

@@ -0,0 +1,84 @@
+<link href="__CDN__/assets/css/wechat/menu.css?v={$site['version']}" rel="stylesheet">
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div class="weixin-menu-setting clearfix">
+                        <div class="mobile-menu-preview">
+                            <div class="mobile-head-title">{$site.name}</div>
+                            <ul class="menu-list" id="menu-list">
+                                <li class="add-item extra" id="add-item">
+                                    <a href="javascript:;" class="menu-link" title="添加菜单"><i class="weixin-icon add-gray"></i></a>
+                                </li>
+                            </ul>
+                        </div>
+                        <div class="weixin-body">
+                            <div class="weixin-content" style="display:none">
+                                <div class="item-info">
+                                    <form id="form-item" class="form-item" data-value="" >
+                                        <div class="item-head">
+                                            <h4 id="current-item-name">添加子菜单</h4>
+                                            <div class="item-delete"><a href="javascript:;" id="item_delete">删除菜单</a></div>
+                                        </div>
+                                        <div style="margin-top: 20px;">
+                                            <dl>
+                                                <dt id="current-item-option"><span class="is-sub-item">子</span>菜单标题:</dt>
+                                                <dd><div class="input-box"><input id="item_title" name="item-title" type="text" value=""></div></dd>
+                                            </dl>
+                                            <dl class="is-item">
+                                                <dt id="current-item-type"><span class="is-sub-item">子</span>菜单内容:</dt>
+                                                <dd>
+                                                    <input id="type1" type="radio" name="type" value="click"><label for="type1" data-editing="1"><span class="lbl_content">发送消息</span></label>
+                                                    <input id="type2" type="radio" name="type" value="view" ><label for="type2"  data-editing="1"><span class="lbl_content">跳转网页</span></label>
+                                                    <input id="type3" type="radio" name="type" value="scancode_push"><label for="type3" data-editing="1"><span class="lbl_content">扫码推</span></label>
+                                                    <input id="type4" type="radio" name="type" value="scancode_waitmsg"><label for="type4" data-editing="1"><span class="lbl_content">扫码推提示框</span></label>
+                                                    <input id="type5" type="radio" name="type" value="pic_sysphoto"><label for="type5" data-editing="1"><span class="lbl_content">拍照发图</span></label>
+                                                    <input id="type6" type="radio" name="type" value="pic_photo_or_album"><label for="type6" data-editing="1"><span class="lbl_content">拍照相册发图</span></label>
+                                                    <input id="type7" type="radio" name="type" value="pic_weixin"><label for="type7" data-editing="1"><span class="lbl_content">相册发图</span></label>
+                                                    <input id="type8" type="radio" name="type" value="location_select"><label for="type8" data-editing="1"><span class="lbl_content">地理位置选择</span></label>
+                                                </dd>
+                                            </dl>
+                                            <div id="menu-content" class="is-item">
+                                                <div class="viewbox is-view">
+                                                    <p class="menu-content-tips">点击该<span class="is-sub-item">子</span>菜单会跳到以下链接</p>
+                                                    <dl>
+                                                        <dt>页面地址:</dt>
+                                                        <dd><div class="input-box"><input type="text" id="url" name="url"></div>
+                                                        </dd>
+                                                    </dl>
+                                                </div>
+                                                <div class="clickbox is-click" style="display: none;">
+                                                    <input type="hidden" name="key" id="key" value="" />
+                                                    <span class="create-click"><a href="{:url('wechat.response/select')}" id="select-resources"><i class="weixin-icon big-add-gray"></i><strong>选择现有资源</strong></a></span>
+                                                    <span class="create-click"><a href="{:url('wechat.response/add')}" id="add-resources"><i class="weixin-icon big-add-gray"></i><strong>添加新资源</strong></a></span>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </form>
+                                </div>
+
+                            </div>
+                            <div class="no-weixin-content">
+                                点击左侧菜单进行编辑操作
+                            </div>
+                        </div>
+                    </div>
+                    <div class="row">
+                        <div class="col-xs-4 text-center text-danger">
+                            <i class="fa fa-lightbulb-o"></i> <small>可直接拖动菜单排序</small>
+                        </div>
+                        <div class="col-xs-8 text-center"><a href="javascript:;" id="menuSyn" class="btn btn-danger">保存并发布</a></div>
+                    </div>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>
+<script type="text/javascript">
+    var menu = {:json_encode($menu, JSON_UNESCAPED_UNICODE)};
+    var responselist = {:json_encode($responselist, JSON_UNESCAPED_UNICODE)};
+</script>

+ 48 - 0
addons/wechat/application/admin/view/wechat/response/add.html

@@ -0,0 +1,48 @@
+<form id="add-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
+    <div class="form-group">
+        <label for="module" class="control-label col-xs-12 col-sm-2">{:__('Resource title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" class="form-control" id="title" name="row[title]" value="" data-rule="required" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="remark" class="control-label col-xs-12 col-sm-2">{:__('Memo')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea class="form-control" id="remark" name="row[remark]"></textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="radio" name="row[type]" value="text" id="type-text" checked />
+            <label for="type-text">{:__('Text')}</label>
+            <input type="radio" name="row[type]" value="app" id="type-app" />
+            <label for="type-app">{:__('App')}</label>
+        </div>
+    </div>
+    <div id="expand">
+
+    </div>
+    <div class="form-group">
+        <label for="status" class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            {:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')])}
+        </div>
+    </div>
+    <div class="form-group {:input('get.callback')?'':'hidden layer-footer'}">
+        <div class="col-xs-2"></div>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+    <select name="applist" disabled="true" class="hidden">
+        {foreach $applist as $k => $v}
+            <option value="{$k}">{$v.name}</option>
+        {/foreach}
+    </select>
+</form>
+<script>
+    var apps = {:json_encode($applist)};
+    var datas = {};
+</script>

+ 52 - 0
addons/wechat/application/admin/view/wechat/response/edit.html

@@ -0,0 +1,52 @@
+<form id="edit-form" class="form-horizontal form-ajax" role="form" data-toggle="validator" method="POST" action="">
+
+    <div class="form-group">
+        <label for="module" class="control-label col-xs-12 col-sm-2">{:__('Resource title')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type="text" class="form-control" id="title" name="row[title]" value="{$row.title}" data-rule="required" />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="controller" class="control-label col-xs-12 col-sm-2">{:__('Event key')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <input type='text' class="form-control" id="eventkey" name="row[eventkey]" value="{$row.eventkey}" data-rule="required" readonly />
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="remark" class="control-label col-xs-12 col-sm-2">{:__('Memo')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            <textarea class="form-control" id="remark" name="row[remark]">{$row.remark}</textarea>
+        </div>
+    </div>
+    <div class="form-group">
+        <label for="content" class="control-label col-xs-12 col-sm-2">{:__('Type')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            {:build_radios('row[type]', ['text' => __('Text'), 'app' => __('App')], $row['type'])}
+        </div>
+    </div>
+    <div id="expand">
+
+    </div>
+    <div class="form-group">
+        <div class="col-xs-2"></div>
+        <div class="col-xs-12 col-sm-8">
+            {:build_radios('row[status]', ['normal'=>__('Normal'), 'hidden'=>__('Hidden')], $row['status'])}
+        </div>
+    </div>
+    <div class="form-group hidden layer-footer">
+        <div class="col-xs-2"></div>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+    <select name="applist" disabled="true" class="hidden">
+        {foreach $applist as $k => $v}
+            <option value="{$k}">{$v.name}</option>
+        {/foreach}
+    </select>
+</form>
+<script>
+    var apps = {:json_encode($applist)};
+    var datas = {$row.content};
+</script>

+ 21 - 0
addons/wechat/application/admin/view/wechat/response/index.html

@@ -0,0 +1,21 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        {:build_toolbar();}
+                    </div>
+                    <table id="table" class="table table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('wechat/response/edit')}" 
+                           data-operate-del="{:$auth->check('wechat/response/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 16 - 0
addons/wechat/application/admin/view/wechat/response/select.html

@@ -0,0 +1,16 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <table id="table" class="table table-bordered table-hover" width="100%">
+
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 16 - 0
addons/wechat/application/common/model/WechatAutoreply.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+class WechatAutoreply extends Model
+{
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+}

+ 32 - 0
addons/wechat/application/common/model/WechatConfig.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+class WechatConfig extends Model
+{
+
+    // 表名,不含前缀
+    public $name = 'wechat_config';
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    // 追加属性
+    protected $append = [
+    ];
+
+    /**
+     * 读取指定配置名称的值
+     * @param string $name
+     * @return string
+     */
+    public static function value($name)
+    {
+        $item = self::get(['name' => $name]);
+        return $item ? $item->value : '';
+    }
+
+}

+ 16 - 0
addons/wechat/application/common/model/WechatContext.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+class WechatContext extends Model
+{
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+}

+ 16 - 0
addons/wechat/application/common/model/WechatResponse.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+class WechatResponse extends Model
+{
+
+    // 自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+
+}

+ 119 - 0
addons/wechat/config.php

@@ -0,0 +1,119 @@
+<?php
+
+return array (
+  0 => 
+  array (
+    'name' => 'app_id',
+    'title' => 'app_id',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your app_id',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  1 => 
+  array (
+    'name' => 'secret',
+    'title' => 'secret',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your secret',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  2 => 
+  array (
+    'name' => 'token',
+    'title' => 'token',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your token',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  3 => 
+  array (
+    'name' => 'aes_key',
+    'title' => 'aes_key',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'your aes_key',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  4 => 
+  array (
+    'name' => 'debug',
+    'title' => '调试模式',
+    'type' => 'radio',
+    'content' => 
+    array (
+      0 => '否',
+      1 => '是',
+    ),
+    'value' => '1',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  5 => 
+  array (
+    'name' => 'log_level',
+    'title' => '日志记录等级',
+    'type' => 'select',
+    'content' => 
+    array (
+      'debug' => 'debug',
+      'info' => 'info',
+      'notice' => 'notice',
+      'warning' => 'warning',
+      'error' => 'error',
+      'critical' => 'critical',
+      'alert' => 'alert',
+      'emergency' => 'emergency',
+    ),
+    'value' => 'debug',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+  6 => 
+  array (
+    'name' => 'oauth_callback',
+    'title' => '登录回调',
+    'type' => 'string',
+    'content' => 
+    array (
+    ),
+    'value' => 'http://www.yoursite.com/addons/wechat/index/callback',
+    'rule' => 'required',
+    'msg' => '',
+    'tip' => '',
+    'ok' => '',
+    'extend' => '',
+  ),
+);

+ 199 - 0
addons/wechat/controller/Index.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace addons\wechat\controller;
+
+use app\common\model\WechatAutoreply;
+use app\common\model\WechatContext;
+use app\common\model\WechatResponse;
+use app\common\model\WechatConfig;
+use EasyWeChat\Foundation\Application;
+use EasyWeChat\Payment\Order;
+use addons\wechat\library\Wechat as WechatService;
+use addons\wechat\library\Config as ConfigService;
+use think\Log;
+
+/**
+ * 微信接口
+ */
+class Index extends \think\addons\Controller
+{
+
+    public $app = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->app = new Application(ConfigService::load());
+    }
+
+    /**
+     * 
+     */
+    public function index()
+    {
+        $this->error("当前插件暂无前台页面");
+    }
+
+    /**
+     * 微信API对接接口
+     */
+    public function api()
+    {
+        $this->app->server->setMessageHandler(function ($message) {
+
+            $WechatService = new WechatService;
+            $WechatContext = new WechatContext;
+            $WechatResponse = new WechatResponse;
+
+            $openid = $message->FromUserName;
+            $to_openid = $message->ToUserName;
+            $event = $message->Event;
+            $eventkey = $message->EventKey ? $message->EventKey : $message->Event;
+
+            $unknownmessage = WechatConfig::value('default.unknown.message');
+            $unknownmessage = $unknownmessage ? $unknownmessage : "对找到对应指令!";
+
+            switch ($message->MsgType)
+            {
+                case 'event': //事件消息
+                    switch ($event)
+                    {
+                        case 'subscribe'://添加关注
+                            $subscribemessage = WechatConfig::value('default.subscribe.message');
+                            $subscribemessage = $subscribemessage ? $subscribemessage : "欢迎关注我们!";
+                            return $subscribemessage;
+                        case 'unsubscribe'://取消关注
+                            return '';
+                        case 'LOCATION'://获取地理位置
+                            return '';
+                        case 'VIEW': //跳转链接,eventkey为链接
+                            return '';
+                        default:
+                            break;
+                    }
+
+                    $response = $WechatResponse->where(["eventkey" => $eventkey, 'status' => 'normal'])->find();
+                    if ($response)
+                    {
+                        $content = (array) json_decode($response['content'], TRUE);
+                        $context = $WechatContext->where(['openid' => $openid])->find();
+                        $data = ['eventkey' => $eventkey, 'command' => '', 'refreshtime' => time(), 'openid' => $openid];
+                        if ($context)
+                        {
+                            $WechatContext->data($data)->where('id', $context['id'])->update();
+                            $data['id'] = $context['id'];
+                        }
+                        else
+                        {
+                            $id = $WechatContext->data($data)->save();
+                            $data['id'] = $id;
+                        }
+                        $result = $WechatService->response($this, $openid, $content, $data);
+                        if ($result)
+                        {
+                            return $result;
+                        }
+                    }
+                    return $unknownmessage;
+                case 'text': //文字消息
+                case 'image': //图片消息
+                case 'voice': //语音消息
+                case 'video': //视频消息
+                case 'location': //坐标消息
+                case 'link': //链接消息
+                default: //其它消息
+                    //上下文事件处理
+                    $context = $WechatContext->where(['openid' => ['=', $openid], 'refreshtime' => ['>=', time() - 1800]])->find();
+                    if ($context && $context['eventkey'])
+                    {
+                        $response = $WechatResponse->where(['eventkey' => $context['eventkey'], 'status' => 'normal'])->find();
+                        if ($response)
+                        {
+                            $WechatContext->data(array('refreshtime' => time()))->where('id', $context['id'])->update();
+                            $content = (array) json_decode($response['content'], TRUE);
+                            $result = $WechatService->command($this, $openid, $content, $context);
+                            if ($result)
+                            {
+                                return $result;
+                            }
+                        }
+                    }
+                    //自动回复处理
+                    if ($message->MsgType == 'text')
+                    {
+                        $wechat_autoreply = new WechatAutoreply();
+                        $autoreply = $wechat_autoreply->where(['text' => $message->Content, 'status' => 'normal'])->find();
+                        if ($autoreply)
+                        {
+                            $response = $WechatResponse->where(["eventkey" => $autoreply['eventkey'], 'status' => 'normal'])->find();
+                            if ($response)
+                            {
+                                $content = (array) json_decode($response['content'], TRUE);
+                                $context = $WechatContext->where(['openid' => $openid])->find();
+                                $result = $WechatService->response($this, $openid, $content, $context);
+                                if ($result)
+                                {
+                                    return $result;
+                                }
+                            }
+                        }
+                    }
+                    return $unknownmessage;
+            }
+            return ""; //SUCCESS
+        });
+        $response = $this->app->server->serve();
+        // 将响应输出
+        $response->send();
+    }
+
+    /**
+     * 登录回调
+     */
+    public function callback()
+    {
+        
+    }
+
+    /**
+     * 支付回调
+     */
+    public function notify()
+    {
+        Log::record(file_get_contents('php://input'), "notify");
+        $response = $this->app->payment->handleNotify(function($notify, $successful) {
+            // 使用通知里的 "微信支付订单号" 或者 "商户订单号" 去自己的数据库找到订单
+            $orderinfo = Order::findByTransactionId($notify->transaction_id);
+            if ($orderinfo)
+            {
+                //订单已处理
+                return true;
+            }
+            $orderinfo = Order::get($notify->out_trade_no);
+            if (!$orderinfo)
+            { // 如果订单不存在
+                return 'Order not exist.'; // 告诉微信,我已经处理完了,订单没找到,别再通知我了
+            }
+            // 如果订单存在
+            // 检查订单是否已经更新过支付状态,已经支付成功了就不再更新了
+            if ($orderinfo['paytime'])
+            {
+                return true;
+            }
+            // 用户是否支付成功
+            if ($successful)
+            {
+                // 请在这里编写处理成功的处理逻辑
+
+                return true; // 返回处理完成
+            }
+            else
+            { // 用户支付失败
+                return true;
+            }
+        });
+
+        $response->send();
+    }
+
+}

+ 7 - 0
addons/wechat/info.ini

@@ -0,0 +1,7 @@
+name = wechat
+title = 微信管理
+intro = 微信管理插件
+author = Karson
+website = http://www.fastadmin.net
+version = 1.0.0
+state = 1

+ 64 - 0
addons/wechat/install.sql

@@ -0,0 +1,64 @@
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__wechat_autoreply` (
+  `id` int(10) NOT NULL AUTO_INCREMENT,
+  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '标题',
+  `text` varchar(100) NOT NULL DEFAULT '' COMMENT '触发文本',
+  `eventkey` varchar(50) NOT NULL DEFAULT '' COMMENT '响应事件',
+  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '添加时间',
+  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信自动回复表';
+
+BEGIN;
+INSERT INTO `__PREFIX__wechat_autoreply` VALUES ('1', '输入hello', 'hello', '58c7d908c4570', '123', '1493366855', '1493366855', 'normal'), ('2', '输入你好', '你好', '58fdfaa9e1965', 'sad', '1493704976', '1493704976', 'normal');
+COMMIT;
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__wechat_config` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '配置名称',
+  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '配置标题',
+  `value` text NOT NULL COMMENT '配置值',
+  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
+  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信配置表';
+
+BEGIN;
+INSERT INTO `__PREFIX__wechat_config` VALUES ('1', 'menu', '微信菜单', '[{\"name\":\"FastAdmin\",\"sub_button\":[{\"name\":\"官网\",\"type\":\"view\",\"url\":\"http:\\/\\/www.fastadmin.net\"},{\"name\":\"在线演示\",\"type\":\"click\",\"key\":\"\"},{\"name\":\"文档\",\"type\":\"view\",\"url\":\"http:\\/\\/doc.fastadmin.net\"}]},{\"name\":\"在线客服\",\"type\":\"click\",\"key\":\"58cb852984970\"},{\"name\":\"关于我们\",\"type\":\"click\",\"key\":\"58bf944aa0777\"}]', '1497398820', '1500538185'), ('2', 'service', '客服配置', '{\"onlinetime\":\"09:00-18:00\",\"offlinemsg\":\"请在工作时间联系客服!\",\"nosessionmsg\":\"当前没有客服在线!请稍后重试!\",\"waitformsg\":\"请问有什么可以帮到您?\"}', '1497429674', '1497429674'), ('3', 'signin', '连续登录配置', '{\"s1\":\"100\",\"s2\":\"200\",\"s3\":\"300\",\"sn\":\"500\"}', '1497429711', '1497429711');
+COMMIT;
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__wechat_context` (
+  `id` int(10) NOT NULL AUTO_INCREMENT,
+  `openid` varchar(64) NOT NULL DEFAULT '',
+  `type` varchar(30) NOT NULL DEFAULT '' COMMENT '类型',
+  `eventkey` varchar(64) NOT NULL DEFAULT '',
+  `command` varchar(64) NOT NULL DEFAULT '',
+  `message` varchar(255) NOT NULL DEFAULT '' COMMENT '内容',
+  `refreshtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最后刷新时间',
+  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
+  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `openid` (`openid`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信上下文表';
+
+CREATE TABLE IF NOT EXISTS `__PREFIX__wechat_response` (
+  `id` int(10) NOT NULL AUTO_INCREMENT,
+  `title` varchar(100) NOT NULL DEFAULT '' COMMENT '资源名',
+  `eventkey` varchar(128) NOT NULL DEFAULT '' COMMENT '事件',
+  `type` enum('text','image','news','voice','video','music','link','app') NOT NULL DEFAULT 'text' COMMENT '类型',
+  `content` text NOT NULL COMMENT '内容',
+  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
+  `createtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间',
+  `updatetime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间',
+  `status` varchar(30) NOT NULL DEFAULT '' COMMENT '状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `event` (`eventkey`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='微信资源表';
+
+BEGIN;
+INSERT INTO `__PREFIX__wechat_response` VALUES ('1', '签到送积分', '58adaf7876aab', 'app', '{\"app\":\"signin\"}', '', '1487777656', '1487777656', 'normal'), ('2', '关于我们', '58bf944aa0777', 'app', '{\"app\":\"page\",\"id\":\"1\"}', '', '1488950346', '1488950346', 'normal'), ('3', '自动回复1', '58c7d908c4570', 'text', '{\"content\":\"world\"}', '', '1489492232', '1489492232', 'normal'), ('4', '联系客服', '58cb852984970', 'app', '{\"app\":\"service\"}', '', '1489732905', '1489732905', 'normal'), ('5', '自动回复2', '58fdfaa9e1965', 'text', '{\"content\":\"我是FastAdmin!\"}', '', '1493039785', '1493039785', 'normal');
+COMMIT;
+

+ 82 - 0
addons/wechat/library/Config.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace addons\wechat\library;
+
+/**
+ * Wechat配置类
+ */
+class Config
+{
+
+    public function __construct()
+    {
+        
+    }
+
+    public static function load()
+    {
+        $config = get_addon_config('wechat');
+
+        return [
+            /**
+             * Debug 模式,bool 值:true/false
+             *
+             * 当值为 false 时,所有的日志都不会记录
+             */
+            'debug'   => !!$config['debug'],
+            /**
+             * 账号基本信息,请从微信公众平台/开放平台获取
+             */
+            'app_id'  => $config['app_id'], // AppID
+            'secret'  => $config['secret'], // AppSecret
+            'token'   => $config['token'], // Token
+            'aes_key' => $config['aes_key'], // EncodingAESKey,安全模式下请一定要填写!!!
+            /**
+             * 日志配置
+             *
+             * level: 日志级别, 可选为:
+             *         debug/info/notice/warning/error/critical/alert/emergency
+             * permission:日志文件权限(可选),默认为null(若为null值,monolog会取0644)
+             * file:日志文件位置(绝对路径!!!),要求可写权限
+             */
+            'log'     => [
+                'level'      => $config['log_level'],
+                'permission' => 0777,
+                'file'       => ROOT_PATH . '/runtime/log/easywechat.log',
+            ],
+            /**
+             * OAuth 配置
+             *
+             * scopes:公众平台(snsapi_userinfo / snsapi_base),开放平台:snsapi_login
+             * callback:OAuth授权完成后的回调页地址
+             */
+            'oauth'   => [
+                'scopes'   => ['snsapi_userinfo'],
+                'callback' => $config['oauth_callback'],
+            ],
+            /**
+             * 微信支付
+             */
+            'payment' => [
+                'merchant_id' => 'your-mch-id',
+                'key'         => 'key-for-signature',
+                'cert_path'   => 'path/to/your/cert.pem', // XXX: 绝对路径!!!!
+                'key_path'    => 'path/to/your/key', // XXX: 绝对路径!!!!
+            // 'device_info'     => '013467007045764',
+            // 'sub_app_id'      => '',
+            // 'sub_merchant_id' => '',
+            // ...
+            ],
+            /**
+             * Guzzle 全局设置
+             *
+             * 更多请参考: http://docs.guzzlephp.org/en/latest/request-options.html
+             */
+            'guzzle'  => [
+                'timeout' => 3.0, // 超时时间(秒)
+            //'verify' => false, // 关掉 SSL 认证(强烈不建议!!!)
+            ],
+        ];
+    }
+
+}

+ 192 - 0
addons/wechat/library/Wechat.php

@@ -0,0 +1,192 @@
+<?php
+
+namespace addons\wechat\library;
+
+use app\common\model\Page;
+use app\common\model\WechatConfig;
+use EasyWeChat\Message\News;
+use EasyWeChat\Message\Transfer;
+
+/**
+ * Wechat服务类
+ */
+class Wechat
+{
+
+    public function __construct()
+    {
+        
+    }
+
+    public static function appConfig()
+    {
+        return array(
+            'signin'  => array(
+                'name'   => '签到送积分',
+                'config' => array(
+                )
+            ),
+            'blog'    => array(
+                'name'   => '关联博客',
+                'config' => array(
+                    array(
+                        'type'    => 'text',
+                        'caption' => '日志ID',
+                        'field'   => 'id',
+                        'options' => ''
+                    )
+                )
+            ),
+            'article' => array(
+                'name'   => '关联文章',
+                'config' => array(
+                    array(
+                        'type'    => 'text',
+                        'caption' => '文章ID',
+                        'field'   => 'id',
+                        'options' => ''
+                    )
+                )
+            ),
+            'page'    => array(
+                'name'   => '关联单页',
+                'config' => array(
+                    array(
+                        'type'    => 'text',
+                        'caption' => '单页ID',
+                        'field'   => 'id',
+                        'options' => ''
+                    )
+                )
+            ),
+            'service' => array(
+                'name'   => '在线客服',
+                'config' => array(
+                )
+            ),
+        );
+    }
+
+    // 微信输入交互内容指令
+    public function command($obj, $openid, $content, $context)
+    {
+        $response = FALSE;
+        if (isset($content['app']))
+        {
+            switch ($content['app'])
+            {
+                case 'signin':
+                case 'blog':
+                case 'article':
+                case 'page':
+                    break;
+                case 'service':
+                    $service = (array) json_decode(WechatConfig::value('service'), true);
+                    list($begintime, $endtime) = explode('-', $service['onlinetime']);
+                    $session = $obj->app->staff_session;
+                    $staff = $obj->app->staff;
+
+                    $kf_account = $session->get($openid)->kf_account;
+                    $time = time();
+                    if (!$kf_account && ($time < strtotime(date("Y-m-d {$begintime}")) || $time > strtotime(date("Y-m-d {$endtime}"))))
+                    {
+                        return $service['offlinemsg'];
+                    }
+                    if (!$kf_account)
+                    {
+                        $kf_list = $staff->onlines()->kf_online_list;
+                        if ($kf_list)
+                        {
+                            $kfarr = [];
+                            foreach ($kf_list as $k => $v)
+                            {
+                                $kfarr[$v['kf_account']] = $v['accepted_case'];
+                            }
+                            $kfkeys = array_keys($kfarr, min($kfarr));
+                            $kf_account = reset($kfkeys);
+                            $session->create($kf_account, $openid);
+                            $response = $service['waitformsg'];
+                        }
+                        else
+                        {
+                            $response = $service['nosessionmsg'];
+                        }
+                    }
+                    else
+                    {
+                        $server = $obj->app->server;
+                        $server->setMessageHandler(function($message) {
+                            return new Transfer();
+                        });
+                        $response = $server->serve();
+                        $response->send();
+                        exit;
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+        }
+        else
+        {
+            $response = isset($content['content']) ? $content['content'] : $response;
+        }
+        return $response;
+    }
+
+    // 微信点击菜单event指令
+    public function response($obj, $openid, $content, $context)
+    {
+        $response = FALSE;
+        if (isset($content['app']))
+        {
+            switch ($content['app'])
+            {
+                case 'signin':
+                    break;
+                case 'blog':
+                    $id = explode(',', $content['id']);
+                    $bloglist = addons\blog\model\Post::all($id);
+                    $response = [];
+                    foreach ($bloglist as $k => $blog)
+                    {
+                        if ($blog)
+                        {
+                            $news = new News();
+                            $news->title = $blog['title'];
+                            $news->url = addon_url('blog/index/post', ['id' => $blog['id']], true, true);
+                            $news->image = cdnurl($blog['thumb']);
+                            $news->description = $blog['description'];
+                            $response[] = $news;
+                        }
+                    }
+
+                case 'page':
+                    $id = isset($content['id']) ? $content['id'] : 0;
+                    $pageinfo = Page::get($id);
+                    if ($pageinfo)
+                    {
+                        $news = new News();
+                        $news->title = $pageinfo['title'];
+                        $news->url = $pageinfo['url'] ? $pageinfo['url'] : url('index/page/show', ['id' => $pageinfo['id']], true, true);
+                        $news->image = cdnurl($pageinfo['image']);
+                        $news->description = $pageinfo['description'];
+                        return $news;
+                    }
+                    break;
+                case 'service':
+                    $response = $this->command($obj, $openid, $content, $context);
+                    break;
+                default:
+                    break;
+            }
+        }
+        else
+        {
+            $response = isset($content['content']) ? $content['content'] : $response;
+        }
+        return $response;
+    }
+
+}

+ 77 - 0
addons/wechat/public/assets/js/backend/wechat/autoreply.js

@@ -0,0 +1,77 @@
+define(['jquery', 'bootstrap', 'backend', 'form', 'table'], function ($, undefined, Backend, Form, Table) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'wechat/autoreply/index',
+                    add_url: 'wechat/autoreply/add',
+                    edit_url: 'wechat/autoreply/edit',
+                    del_url: 'wechat/autoreply/del',
+                    multi_url: 'wechat/autoreply/multi',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                sortName: 'id',
+                columns: [
+                    [
+                        {field: 'state', checkbox: true, },
+                        {field: 'id', title: __('Id')},
+                        {field: 'title', title: __('Title')},
+                        {field: 'text', title: __('Text')},
+                        {field: 'eventkey', title: __('Event key')},
+                        {field: 'remark', title: __('Remark')},
+                        {field: 'createtime', title: __('Create time'), formatter: Table.api.formatter.datetime},
+                        {field: 'updatetime', title: __('Update time'), formatter: Table.api.formatter.datetime},
+                        {field: 'status', title: __('Status'), formatter: Table.api.formatter.status},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+
+                var refreshkey = function (data) {
+                    $("input[name='row[eventkey]']").val(data.eventkey).trigger("change");
+                    Layer.closeAll();
+                    var keytitle = data.title;
+                    var cont = $(".clickbox .create-click:first");
+                    $(".keytitle", cont).remove();
+                    if (keytitle) {
+                        cont.append('<div class="keytitle">' + __('Event key') + ':' + keytitle + '</div>');
+                    }
+                };
+                $(document).on('click', "#select-resources", function () {
+                    var key = $("input[name='row[eventkey]']").val();
+                    parent.Backend.api.open($(this).attr("href") + "?key=" + key, __('Select'), {callback: refreshkey});
+                    return false;
+                });
+
+                $(document).on('click', "#add-resources", function () {
+                    parent.Backend.api.open($(this).attr("href") + "?key=", __('Add'), {callback: refreshkey});
+                    return false;
+                });
+            }
+        }
+
+    };
+    return Controller;
+});

+ 97 - 0
addons/wechat/public/assets/js/backend/wechat/config.js

@@ -0,0 +1,97 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'wechat/config/index',
+                    add_url: 'wechat/config/add',
+                    edit_url: 'wechat/config/edit',
+                    del_url: 'wechat/config/del',
+                    multi_url: 'wechat/config/multi',
+                    table: 'wechat_config',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: 'id',
+                sortName: 'id',
+                columns: [
+                    [
+                        {field: 'state', checkbox: true},
+                        {field: 'id', title: __('Id')},
+                        {field: 'name', title: __('Name')},
+                        {field: 'title', title: __('Title')},
+                        {field: 'createtime', title: __('Createtime'), formatter: Table.api.formatter.datetime},
+                        {field: 'updatetime', title: __('Updatetime'), formatter: Table.api.formatter.datetime},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+                $(document).on('click', ".btn-jsoneditor", function () {
+                    $("#c-value").toggle();
+                    $(".fieldlist").toggleClass("hide");
+                    $(".btn-insertlink").toggle();
+                    $("input[name='row[mode]']").val($("#c-value").is(":visible") ? "textarea" : "json");
+                });
+                $(document).on('click', ".btn-insertlink", function () {
+                    var textarea = $("textarea[name='row[value]']");
+                    var cursorPos = textarea.prop('selectionStart');
+                    var v = textarea.val();
+                    var textBefore = v.substring(0, cursorPos);
+                    var textAfter = v.substring(cursorPos, v.length);
+
+                    Layer.prompt({title: '请输入显示的文字', formType: 3}, function (text, index) {
+                        Layer.close(index);
+                        Layer.prompt({title: '请输入跳转的链接URL(包含http)', formType: 3}, function (link, index) {
+                            text = text == '' ? link : text;
+                            textarea.val(textBefore + '<a href="' + link + '">' + text + '</a>' + textAfter);
+                            Layer.close(index);
+                        });
+                    });
+                });
+                $("input[name='row[type]']:checked").trigger("click");
+
+                $(document).on("click", ".fieldlist .append", function () {
+                    var rel = parseInt($(this).closest("dl").attr("rel")) + 1;
+                    $(this).closest("dl").attr("rel", rel);
+                    $('<dd><input type="text" name="field[' + rel + ']" class="form-control" id="field-' + rel + '" value="" size="10" /> <input type="text" name="value[' + rel + ']" class="form-control" id="value-' + rel + '" value="" size="40" /> <span class="btn btn-sm btn-danger btn-remove"><i class="fa fa-times"></i></span> <span class="btn btn-sm btn-primary btn-dragsort"><i class="fa fa-arrows"></i></span></dd>').insertBefore($(this).parent());
+                });
+                $(document).on("click", ".fieldlist dd .btn-remove", function () {
+                    $(this).parent().remove();
+                });
+                //拖拽排序
+                require(['dragsort'], function () {
+                    //绑定拖动排序
+                    $("dl.fieldlist").dragsort({
+                        itemSelector: 'dd',
+                        dragSelector: ".btn-dragsort",
+                        dragEnd: function () {
+
+                        },
+                        placeHolderTemplate: "<dd></dd>"
+                    });
+                });
+            }
+        }
+    };
+    return Controller;
+});

+ 293 - 0
addons/wechat/public/assets/js/backend/wechat/menu.js

@@ -0,0 +1,293 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'sortable'], function ($, undefined, Backend, Table, Form, Sortable) {
+    var Controller = {
+        index: function () {
+            String.prototype.subByte = function (start, bytes) {
+                for (var i = start; bytes > 0; i++) {
+                    var code = this.charCodeAt(i);
+                    bytes -= code < 256 ? 1 : 2;
+                }
+                return this.slice(start, i + bytes)
+            };
+            var init_menu = function (menu) {
+                var str = "";
+                var items = menu;
+                var type = action = "";
+                for (i in items) {
+                    if (items[i]['sub_button'] != undefined) {
+                        type = action = "";
+                    } else {
+                        type = items[i]['type'];
+                        if (items[i]['url'] != undefined)
+                            action = "url|" + items[i]['url'];
+                        if (items[i]['key'] != undefined)
+                            action = "key|" + items[i]['key'];
+                    }
+                    str += '<li id="menu-' + i + '" class="menu-item" data-type="' + type + '" data-action="' + action + '" data-name="' + items[i]['name'] + '"> <a href="javascript:;" class="menu-link"> <i class="icon-menu-dot"></i> <i class="weixin-icon sort-gray"></i> <span class="title">' + items[i]['name'] + '</span> </a>';
+                    var tem = '';
+                    if (items[i]['sub_button'] != undefined) {
+                        var sub_menu = items[i]['sub_button'];
+                        for (j in sub_menu) {
+                            type = sub_menu[j]['type'];
+                            if (sub_menu[j]['url'] != undefined)
+                                action = "url|" + sub_menu[j]['url'];
+                            if (sub_menu[j]['key'] != undefined)
+                                action = "key|" + sub_menu[j]['key'];
+                            tem += '<li id="sub-menu-' + j + '" class="sub-menu-item" data-type="' + type + '" data-action="' + action + '" data-name="' + sub_menu[j]['name'] + '"> <a href="javascript:;"> <i class="weixin-icon sort-gray"></i><span class="sub-title">' + sub_menu[j]['name'] + '</span></a> </li>';
+                        }
+                    }
+                    str += '<div class="sub-menu-box" style="' + (i != 0 ? 'display:none;' : '') + '"> <ul class="sub-menu-list">' + tem + '<li class=" add-sub-item"><a href="javascript:;" title="添加子菜单"><span class=" "><i class="weixin-icon add-gray"></i></span></a></li> </ul> <i class="arrow arrow-out"></i> <i class="arrow arrow-in"></i></div>';
+                    str += '</li>';
+                }
+                $("#add-item").before(str);
+            };
+            var refresh_type = function () {
+                if ($('input[name=type]:checked').val() == 'view') {
+                    $(".is-view").show();
+                    $(".is-click").hide();
+                } else {
+                    $(".is-view").hide();
+                    $(".is-click").show();
+                }
+            };
+            //初始化菜单
+            init_menu(menu);
+            //拖动排序
+            new Sortable($("#menu-list")[0], {draggable: 'li.menu-item'});
+            $(".sub-menu-list").each(function () {
+                new Sortable(this, {draggable: 'li.sub-menu-item'});
+            });
+            //添加主菜单
+            $(document).on('click', '#add-item', function () {
+                var menu_item_total = $(".menu-item").size();
+                if (menu_item_total < 3) {
+                    var item = '<li class="menu-item" data-type="click" data-action="key|" data-name="添加菜单" > <a href="javascript:;" class="menu-link"> <i class="icon-menu-dot"></i> <i class="weixin-icon sort-gray"></i> <span class="title">添加菜单</span> </a> <div class="sub-menu-box" style=""> <ul class="sub-menu-list"><li class=" add-sub-item"><a href="javascript:;" title="添加子菜单"><span class=" "><i class="weixin-icon add-gray"></i></span></a></li> </ul> <i class="arrow arrow-out"></i> <i class="arrow arrow-in"></i> </div></li>';
+                    var itemDom = $(item);
+                    itemDom.insertBefore(this);
+                    itemDom.trigger("click");
+                    $(".sub-menu-box", itemDom).show();
+                    new Sortable($(".sub-menu-list", itemDom)[0], {draggable: 'li.sub-menu-item'});
+                }
+            });
+            $(document).on('change', 'input[name=type]', function () {
+                refresh_type();
+            });
+            $(document).on('click', '#item_delete', function () {
+                var current = $("#menu-list li.current");
+                var prev = current.prev("li[data-type]");
+                var next = current.next("li[data-type]");
+
+                if (prev.size() == 0 && next.size() == 0 && $(".sub-menu-box", current).size() == 0) {
+                    last = current.closest(".menu-item");
+                } else if (prev.size() > 0 || next.size() > 0) {
+                    last = prev.size() > 0 ? prev : next;
+                } else {
+                    last = null;
+                    $(".weixin-content").hide();
+                    $(".no-weixin-content").show();
+                }
+                $("#menu-list li.current").remove();
+                if (last) {
+                    last.trigger('click');
+                } else {
+                    $("input[name='item-title']").val('');
+                }
+                updateChangeMenu();
+            });
+
+            //更新修改与变动
+            var updateChangeMenu = function () {
+                var title = $("input[name='item-title']").val();
+                var type = $("input[name='type']:checked").val();
+                var key = value = '';
+                if (type == 'view') {
+                    key = 'url';
+                } else {
+                    key = 'key';
+                }
+                value = $("input[name='" + key + "']").val();
+
+                if (key == 'key') {
+                    var keytitle = typeof responselist[value] != 'undefined' ? responselist[value] : '';
+                    var cont = $(".is-click .create-click:first");
+                    $(".keytitle", cont).remove();
+                    cont.append('<div class="keytitle">资源名:' + keytitle + '</div>');
+                }
+                var currentItem = $("#menu-list li.current");
+                if (currentItem.size() > 0) {
+                    currentItem.attr('data-type', type);
+                    currentItem.attr('data-action', key + "|" + value);
+                    if (currentItem.siblings().size() == 4) {
+                        $(".add-sub-item").show();
+                    } else if (false) {
+
+                    }
+                    currentItem.children("a").find("span").text(title.subByte(0, 16));
+                    $("input[name='item-title']").val(title);
+                    currentItem.attr('data-name', title);
+                    $('#current-item-name').text(title);
+                }
+                menuUpdate();
+            }
+            //更新菜单数据
+            var menuUpdate = function () {
+                $.post("wechat/menu/edit", {menu: JSON.stringify(getMenuList())}, function (data) {
+                    if (data['code'] == 1) {
+                    } else {
+                        Toastr.error(__('Operation failed'));
+                    }
+                }, 'json');
+            };
+            //获取菜单数据
+            var getMenuList = function () {
+                var menus = new Array();
+                var sub_button = new Array();
+                var menu_i = 0;
+                var sub_menu_i = 0;
+                var item;
+                $("#menu-list li").each(function (i) {
+                    item = $(this);
+                    var name = item.attr('data-name');
+                    var type = item.attr('data-type');
+                    var action = item.attr('data-action');
+                    if (name != null) {
+                        actions = action.split('|');
+                        if (item.hasClass('menu-item')) {
+                            sub_menu_i = 0;
+                            if (item.find('.sub-menu-item').size() > 0) {
+                                menus[menu_i] = {"name": name, "sub_button": "sub_button"}
+                            } else {
+                                if (actions[0] == 'url')
+                                    menus[menu_i] = {"name": name, "type": type, "url": actions[1]};
+                                else
+                                    menus[menu_i] = {"name": name, "type": type, "key": actions[1]};
+                            }
+                            if (menu_i > 0) {
+                                if (menus[menu_i - 1]['sub_button'] == "sub_button")
+                                    menus[menu_i - 1]['sub_button'] = sub_button;
+                                else
+                                    menus[menu_i - 1]['sub_button'];
+                            }
+                            sub_button = new Array();
+                            menu_i++;
+                        } else {
+                            if (actions[0] == 'url')
+                                sub_button[sub_menu_i++] = {"name": name, "type": type, "url": actions[1]};
+                            else
+                                sub_button[sub_menu_i++] = {"name": name, "type": type, "key": actions[1]};
+                        }
+                    }
+                });
+                if (sub_button.length > 0) {
+                    var len = menus.length;
+                    menus[len - 1]['sub_button'] = sub_button;
+                }
+                return menus;
+            }
+            //添加子菜单
+            $(document).on('click', ".add-sub-item", function () {
+                var sub_menu_item_total = $(this).parent().find(".sub-menu-item").size();
+                if (sub_menu_item_total < 5) {
+                    var item = '<li class="sub-menu-item" data-type="click" data-action="key|" data-name="添加子菜单"><a href="javascript:;"><span class=" "><i class="weixin-icon sort-gray"></i><span class="sub-title">添加子菜单</span></span></a></li>';
+                    var itemDom = $(item);
+                    itemDom.insertBefore(this);
+                    itemDom.trigger("click");
+                    if (sub_menu_item_total == 4) {
+                        $(this).hide();
+                    }
+                }
+                return false;
+            });
+            //主菜单子菜单点击事件
+            $(document).on('click', ".menu-item, .sub-menu-item", function () {
+                if ($(this).hasClass("sub-menu-item")) {
+                    $("#menu-list li").removeClass('current');
+                    $(".is-item").show();
+                    $(".is-sub-item").show();
+                } else {
+                    $("#menu-list li").removeClass('current');
+                    $("#menu-list > li").not(this).find(".sub-menu-box").hide();
+                    $(".sub-menu-box", this).toggle();
+                    //如果当前还没有子菜单
+                    if ($(".sub-menu-item", this).size() == 0) {
+                        $(".is-item").show();
+                        $(".is-sub-item").show();
+                    } else {
+                        $(".is-item").hide();
+                        $(".is-sub-item").hide();
+                    }
+                }
+                $(this).addClass('current');
+                var type = $(this).attr('data-type');
+                var action = $(this).attr('data-action');
+                var title = $(this).attr('data-name');
+
+                actions = action.split('|');
+                $("input[name=type][value='" + type + "']").prop("checked", true);
+                $("input[name='item-title']").val(title);
+                $('#current-item-name').text(title);
+                if (actions[0] == 'url') {
+                    $('input[name=key]').val('');
+                } else {
+                    $('input[name=url]').val('');
+                }
+                $("input[name='" + actions[0] + "']").val(actions[1]);
+                if (actions[0] == 'key') {
+                    var keytitle = typeof responselist[actions[1]] != 'undefined' ? responselist[actions[1]] : '';
+                    var cont = $(".is-click .create-click:first");
+                    $(".keytitle", cont).remove();
+                    if (keytitle) {
+                        cont.append('<div class="keytitle">资源名:' + keytitle + '</div>');
+                    }
+                } else {
+
+                }
+
+                $(".weixin-content").show();
+                $(".no-weixin-content").hide();
+
+                refresh_type();
+
+                return false;
+            });
+            $("form").on('change', "input,textarea", function () {
+                updateChangeMenu();
+            });
+            $(document).on('click', "#menuSyn", function () {
+                $.post("wechat/menu/sync", {}, function (ret) {
+                    var msg = ret.hasOwnProperty("msg") && ret.msg != "" ? ret.msg : "";
+                    if (ret.code == 1) {
+                        Backend.api.toastr.success('菜单同步更新成功,生效时间看微信官网说明,或者你重新关注微信号!');
+                    } else {
+                        Backend.api.toastr.error(msg ? msg : __('Operation failed'));
+                    }
+                }, 'json');
+            });
+            var refreshkey = function (data) {
+                responselist[data.eventkey] = data.title;
+                $("input[name=key]").val(data.eventkey).trigger("change");
+                Layer.closeAll();
+            };
+            $(document).on('click', "#select-resources", function () {
+                var key = $("#key").val();
+                Backend.api.open($(this).attr("href") + "?key=" + key, __('Select'), {
+                    callback: refreshkey
+                });
+                return false;
+            });
+
+            $(document).on('click', "#add-resources", function () {
+                Backend.api.open($(this).attr("href") + "?key=" + key, __('Add'), {
+                    callback: refreshkey
+                });
+                return false;
+            });
+        },
+        add: function () {
+            Form.api.bindevent($("form[role=form]"));
+        },
+        edit: function () {
+            Form.api.bindevent($("form[role=form]"));
+        }
+    };
+    return Controller;
+});

+ 180 - 0
addons/wechat/public/assets/js/backend/wechat/response.js

@@ -0,0 +1,180 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'adminlte'], function ($, undefined, Backend, Table, Form, Adminlte) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'wechat/response/index',
+                    add_url: 'wechat/response/add',
+                    edit_url: 'wechat/response/edit',
+                    del_url: 'wechat/response/del',
+                    multi_url: 'wechat/response/multi',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                sortName: 'id',
+                columns: [
+                    [
+                        {field: 'state', checkbox: true, },
+                        {field: 'id', title: 'ID'},
+                        {field: 'type', title: __('Type')},
+                        {field: 'title', title: __('Resource title')},
+                        {field: 'eventkey', title: __('Event key')},
+                        {field: 'status', title: __('Status'), formatter: Table.api.formatter.status, operate:false},
+                        {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        select: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: 'wechat/response/index',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                sortName: 'id',
+                columns: [
+                    [
+                        {field: 'state', checkbox: true, },
+                        {field: 'id', title: 'ID'},
+                        {field: 'type', title: __('Type')},
+                        {field: 'title', title: __('Title')},
+                        {field: 'event', title: __('Event')},
+                        {field: 'status', title: __('Status'), formatter: Table.api.formatter.status, operate:false},
+                        {field: 'operate', title: __('Operate'), events: {
+                                'click .btn-chooseone': function (e, value, row, index) {
+                                    Fast.api.close(row);
+                                },
+                            }, formatter: function () {
+                                return '<a href="javascript:;" class="btn btn-danger btn-chooseone btn-xs"><i class="fa fa-check"></i> ' + __('Choose') + '</a>';
+                            }}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Form.api.bindevent($("form[role=form]"), function (data) {
+                Fast.api.close(data);
+            });
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Form.api.bindevent($("form[role=form]"));
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                var getAppFileds = function (app) {
+                    var app = apps[app];
+                    var appConfig = app['config'];
+                    var str = '';
+                    for (i in appConfig) {
+                        if (appConfig[i]['type'] == 'text' || appConfig[i]['type'] == 'textarea') {
+                            var pattern_str = 'pattern ="required" ';
+                            var alt = '';
+                            if (undefined != appConfig[i]['alt'])
+                                alt = appConfig[i]['alt'];
+                            if (undefined != appConfig[i]['pattern'])
+                                pattern_str = 'pattern ="' + appConfig[i]['pattern'] + '" ';
+                            if (appConfig[i]['type'] == 'textarea') {
+                                str += '<div class="form-group"><label for="content" class="control-label col-xs-12 col-sm-2">' + appConfig[i]['caption'] + ':</label><div class="col-xs-12 col-sm-8"><textarea class="form-control" name="row[content][' + appConfig[i]['field'] + ']" ' + pattern_str + ' alt="' + alt + '" data-rule="required"></textarea> </div> </div>';
+                            } else {
+                                str += '<div class="form-group"><label for="content" class="control-label col-xs-12 col-sm-2">' + appConfig[i]['caption'] + ':</label><div class="col-xs-12 col-sm-8"><input class="form-control" name="row[content][' + appConfig[i]['field'] + ']" type="text" ' + pattern_str + ' alt="' + alt + '" data-rule="required"> </div> </div>';
+                            }
+                        } else {
+                            var options = appConfig[i]['options'];
+                            options = options.split(',');
+                            var option_str = '';
+                            if (appConfig[i]['type'] == 'select') {
+                                for (o in options) {
+                                    var option = options[o];
+                                    var item = option.split(':');
+                                    option_str += '<option value="' + item[0] + '">' + item[1] + '</option>';
+                                }
+                                option_str = '<select class="form-control" name="row[content][' + appConfig[i]['field'] + ']">' + option_str + '</select>';
+                            } else if (appConfig[i]['type'] == 'checkbox') {
+                                for (o in options) {
+                                    var option = options[o];
+                                    var item = option.split(':');
+                                    option_str += '<input type="checkbox" name="row[content][' + appConfig[i]['field'] + '][]" value="' + item[0] + '"> <label>' + item[1] + '</label> ';
+                                }
+
+                            } else if (appConfig[i]['type'] == 'radio') {
+                                for (o in options) {
+                                    var option = options[o];
+                                    var item = option.split(':');
+                                    option_str += '<input type="radio" name="row[content][' + appConfig[i]['field'] + ']" value="' + item[0] + '"> <label>' + item[1] + '</label> ';
+                                }
+                            }
+                            str += '<div class="form-group"><label for="content" class="control-label col-xs-12 col-sm-2">' + appConfig[i]['caption'] + ':</label><div class="col-xs-12 col-sm-8">' + option_str + ' </div> </div>';
+                        }
+
+                    }
+                    return str;
+                };
+                $(document).on('change', "#app", function () {
+                    var app = $(this).val();
+                    $("#appfields").html(getAppFileds(app));
+                    if (datas.app == app) {
+                        delete(datas.app);
+                        var form = $("form.form-ajax");
+                        $.each(datas, function (i, j) {
+                            form.field("row[content][" + i + "]" + ($("input[name='row[content][" + i + "][]']", form).size() > 0 ? '[]' : ''), j);
+                        });
+                    }
+                });
+                $(document).on('click', "input[name='row[type]']", function () {
+                    var type = $(this).val();
+                    if (type == 'text') {
+                        $("#expand").html('<div class="form-group"><label for="content" class="control-label col-xs-12 col-sm-2">文本内容:</label><div class="col-xs-12 col-sm-8"><textarea class="form-control" name="row[content][content]" data-rule="required"></textarea> <a href="javascript:;" class="btn-insertlink">插入链接</a></div></div>');
+                        console.log($($("form.form-ajax row[content][content]")));
+                        $("form[role='form']").field("row[content][content]", datas.content);
+                    } else if (type == 'app') {
+                        $("#expand").html('<div class="form-group"><label for="content" class="control-label col-xs-12 col-sm-2">应用:</label><div class="col-xs-12 col-sm-8"><select class="form-control" name="row[content][app]" id="app">' + $("select[name=applist]").html() + '</select></div></div><div id="appfields"><div>');
+                        $("form[role='form']").field("row[content][app]", datas.app);
+                        $("#app").trigger('change');
+                    }
+                });
+                $(document).on('click', ".btn-insertlink", function () {
+                    var textarea = $("textarea[name='row[content][content]']");
+                    var cursorPos = textarea.prop('selectionStart');
+                    var v = textarea.val();
+                    var textBefore = v.substring(0, cursorPos);
+                    var textAfter = v.substring(cursorPos, v.length);
+
+                    Layer.prompt({title: '请输入显示的文字', formType: 3}, function (text, index) {
+                        Layer.close(index);
+                        Layer.prompt({title: '请输入跳转的链接URL(包含http)', formType: 3}, function (link, index) {
+                            text = text.replace(/(^\s*)|(\s*$)/g, "");
+                            link = link.replace(/(^\s*)|(\s*$)/g, "");
+                            text = text == '' ? link : text;
+                            textarea.val(textBefore + '<a href="' + link + '">' + text + '</a>' + textAfter);
+                            Layer.close(index);
+                        });
+                    });
+                });
+                $("input[name='row[type]']:checked").trigger("click");
+            }
+        }
+    };
+    return Controller;
+});

+ 1 - 0
application/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 32 - 0
application/Limit.php

@@ -0,0 +1,32 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Bear
+ * Date: 2019/12/12
+ * Time: 下午3:37
+ */
+
+namespace app;
+
+class Limit
+{
+    public static function checkLimit()
+    {
+        $uri = $_SERVER['REQUEST_URI'] ?? '';
+        if (preg_match('/\\/api\\/wechat\\/mpapi\\/appid\\/(\w+)/', $uri, $match)) {
+            $appid = $match[1];
+            $cachefile = sys_get_temp_dir() . '/TPL_CALLBACK_' . $appid;
+            if (file_exists($cachefile)) {
+                $content = trim(file_get_contents($cachefile));
+                if (time() - (int)$content < 10) {
+                    echo json_encode(['err' => 1, 'msg' => 'appid limited']);
+                    exit;
+                } else {
+                    @unlink($cachefile);
+                }
+            }
+        }
+    }
+}
+
+Limit::checkLimit();

+ 16 - 0
application/admin/behavior/AdminLog.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace app\admin\behavior;
+
+class AdminLog
+{
+
+    public function run(&$params)
+    {
+        if (request()->isPost())
+        {
+            \app\common\model\AdminLog::record();
+        }
+    }
+
+}

+ 114 - 0
application/admin/command/AdUserDiff.php

@@ -0,0 +1,114 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Elton
+ * Date: 2020/6/29
+ * Time: 10:32
+ */
+
+namespace app\admin\command;
+
+use app\admin\service\LogService;
+use app\common\library\Redis;
+use app\common\model\ReferralDayCollect;
+use app\main\constants\AdConstants;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+use think\console\input\Argument;
+use think\Config;
+use think\Db;
+use think\Log;
+use think\Request;
+
+class AdUserDiff extends Command
+{
+    protected function configure()
+    {
+        $this->setName('AdUserDiff')
+            ->setDescription('筛选出当天没有点击福利广告的用户');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin');
+        LogService::info('开始执行广告用户筛选:'.date('Y-m-d H:i:s', time()));
+
+        // 1. 获取可用的福利广告
+        $adPlans = model('AdManage')
+            ->join('ad_user_group', 'ad_manage.user_group_id = ad_user_group.id', 'left')
+            ->where('ad_type', '=', '2')
+            ->where('show_endtime', '>=', time())
+            ->where('state', '=', '1')
+            ->where("!ISNULL(user_group_id)")
+            ->where('ad_user_group.group_type', '<>', '0')
+            ->field('ad_manage.*,  ad_user_group.group_name, ad_user_group.group_type')
+            ->order('ad_manage.weight', 'desc')
+            ->order('ad_manage.createtime', 'desc')
+            ->select();
+
+        if ($adPlans) {
+            // 计算过期时间 24点过期
+            $tomorrow_time = strtotime(date('Y-m-d 00:00:00', strtotime("+1 day")));
+            $ttl = $tomorrow_time - time();
+            $channels_index = [];
+
+            // 2. 遍历所有的福利广告, 通过广告ID, 获取有哪些渠道$all_channels =>  AD:JU:9:index
+            foreach ($adPlans as $k => $plan) {
+                $plan_id = $plan->id;
+                $plan_key = AdConstants::AD_JU_KEY . $plan_id . ':index';
+                $channels_arr = explode(',', Redis::instance()->get($plan_key));
+                $channels_index = array_merge($channels_index, $channels_arr);
+                if($channels_arr){
+                    foreach ($channels_arr as $channel_key => $channel_id){
+                        // 3. 遍历 $all_channels 获取渠道中所有用户 $uid , 将所有 $uid 按渠道存入一个大集合中。
+                        $channel_key = AdConstants::AD_JU_KEY . $plan_id . ':' . $channel_id;
+                        try{
+                            $channer_user_redis_key = AdConstants::AD_JU_USER . ':' . $channel_id;
+                            Redis::instance()->sUnionStore($channer_user_redis_key, $channel_key);
+                        }catch (\Exception $exception){
+                            $output->write($exception->getMessage());
+                        }
+
+                        // 4. 获取 Redis 中所有点击过广告的用户
+                        $channel_welfare_key = AdConstants::AD_WELFARE . $channel_id;
+
+                        // 5. 将两个大集合做差集比较,将差集存入数据库, JOB通过查询数据库,发送消息
+                        //$diff_data = Redis::instance()->sDiff($channer_user_redis_key, $channel_welfare_key);
+                        Redis::instance()->sDiffStore(AdConstants::AD_WELFARE_PUSH.$channel_id, $channer_user_redis_key, $channel_welfare_key);
+                        Redis::instance()->expire(AdConstants::AD_WELFARE_PUSH.$channel_id, $ttl);
+                    }
+                }
+            }
+            Redis::instance()->set(AdConstants::AD_WELFARE_PUSH_INDEX, implode(',', $channels_index));
+            Redis::instance()->expire(AdConstants::AD_WELFARE_PUSH_INDEX, $ttl);
+
+            LogService::info('广告用户筛选完成:' . date('Y-m-d H:i:s', time()));
+        }else{
+            LogService::info('广告用户筛选完成,没有匹配的广告计划:' . date('Y-m-d H:i:s', time()));
+        }
+    }
+
+    /**
+     * 造点测试数据
+     */
+    /*private function _fakeRedisData()
+    {
+        $plan_id = 5;
+        $channel_arr = [1734, 1735];
+
+        $user_list[1734] = [1, 32930, 1222, 94093];
+        $user_list[1735] = [2238, 9489584, 77377];
+        $redis_index_key = AdConstants::AD_JU_KEY . $plan_id . ':index';
+        Redis::instance()->set($redis_index_key, implode(',', $channel_arr));
+
+        foreach ($channel_arr as $channel_id) {
+            foreach ($user_list[$channel_id] as $user_id)
+            {
+                $redis_channel_key = AdConstants::AD_JU_KEY . $plan_id . ':' . $channel_id;
+                Redis::instance()->sAdd($redis_channel_key, $user_id);
+            }
+        }
+    }*/
+
+}

+ 230 - 0
application/admin/command/Addon.php

@@ -0,0 +1,230 @@
+<?php
+
+namespace app\admin\command;
+
+use think\addons\AddonException;
+use think\addons\Service;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+use think\Exception;
+
+class Addon extends Command
+{
+
+    protected function configure()
+    {
+        $this
+                ->setName('addon')
+                ->addOption('name', 'a', Option::VALUE_REQUIRED, 'addon name', null)
+                ->addOption('action', 'c', Option::VALUE_REQUIRED, 'action(create/enable/disable/install/uninstall/refresh)', 'create')
+                ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
+                ->setDescription('Addon manager');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $name = $input->getOption('name') ?: '';
+        $action = $input->getOption('action') ?: '';
+        //强制覆盖
+        $force = $input->getOption('force');
+
+        include dirname(__DIR__) . DS . 'common.php';
+
+        if (!$name)
+        {
+            throw new Exception('Addon name could not be empty');
+        }
+        if (!$action || !in_array($action, ['create', 'disable', 'enable', 'install', 'uninstall', 'refresh']))
+        {
+            throw new Exception('Please input correct action name');
+        }
+        
+        // 查询一次SQL,判断连接是否正常
+        Db::execute("SELECT 1");
+        
+        $addonDir = ADDON_PATH . $name;
+        switch ($action)
+        {
+            case 'create':
+                //非覆盖模式时如果存在则报错
+                if (is_dir($addonDir) && !$force)
+                {
+                    throw new Exception("addon already exists!\nIf you need to create again, use the parameter --force=true ");
+                }
+                //如果存在先移除
+                if (is_dir($addonDir))
+                {
+                    rmdirs($addonDir);
+                }
+                mkdir($addonDir);
+                $data = [
+                    'name'           => $name,
+                    'addon'          => $name,
+                    'addonClassName' => ucfirst($name)
+                ];
+                $this->writeToFile("addon", $data, $addonDir . DS . ucfirst($name) . '.php');
+                $this->writeToFile("config", $data, $addonDir . DS . 'config.php');
+                $this->writeToFile("info", $data, $addonDir . DS . 'info.ini');
+                $output->info("Create Successed!");
+                break;
+            case 'disable':
+            case 'enable':
+                try
+                {
+                    //调用启用、禁用的方法
+                    Service::$action($name, 0);
+                }
+                catch (AddonException $e)
+                {
+                    if ($e->getCode() != -3)
+                    {
+                        throw new Exception($e->getMessage());
+                    }
+                    //如果有冲突文件则提醒
+                    $data = $e->getData();
+                    foreach ($data['conflictlist'] as $k => $v)
+                    {
+                        $output->warning($v);
+                    }
+                    $output->info("Are you sure you want to " . ($action == 'enable' ? 'override' : 'delete') . " all those files?  Type 'yes' to continue: ");
+                    $line = fgets(STDIN);
+                    if (trim($line) != 'yes')
+                    {
+                        throw new Exception("Operation is aborted!");
+                    }
+                    //调用启用、禁用的方法
+                    Service::$action($name, 1);
+                }
+                catch (Exception $e)
+                {
+                    throw new Exception($e->getMessage());
+                }
+                $output->info(ucfirst($action) . " Successed!");
+                break;
+            case 'install':
+                //非覆盖模式时如果存在则报错
+                if (is_dir($addonDir) && !$force)
+                {
+                    throw new Exception("addon already exists!\nIf you need to install again, use the parameter --force=true ");
+                }
+                //如果存在先移除
+                if (is_dir($addonDir))
+                {
+                    rmdirs($addonDir);
+                }
+                try
+                {
+                    Service::install($name, 0);
+                }
+                catch (AddonException $e)
+                {
+                    if ($e->getCode() != -3)
+                    {
+                        throw new Exception($e->getMessage());
+                    }
+                    //如果有冲突文件则提醒
+                    $data = $e->getData();
+                    foreach ($data['conflictlist'] as $k => $v)
+                    {
+                        $output->warning($v);
+                    }
+                    $output->info("Are you sure you want to override all those files?  Type 'yes' to continue: ");
+                    $line = fgets(STDIN);
+                    if (trim($line) != 'yes')
+                    {
+                        throw new Exception("Operation is aborted!");
+                    }
+                    Service::install($name, 1);
+                }
+                catch (Exception $e)
+                {
+                    throw new Exception($e->getMessage());
+                }
+
+                $output->info("Install Successed!");
+                break;
+            case 'uninstall':
+                //非覆盖模式时如果存在则报错
+                if (!$force)
+                {
+                    throw new Exception("If you need to uninstall addon, use the parameter --force=true ");
+                }
+                try
+                {
+                    Service::uninstall($name, 0);
+                }
+                catch (AddonException $e)
+                {
+                    if ($e->getCode() != -3)
+                    {
+                        throw new Exception($e->getMessage());
+                    }
+                    //如果有冲突文件则提醒
+                    $data = $e->getData();
+                    foreach ($data['conflictlist'] as $k => $v)
+                    {
+                        $output->warning($v);
+                    }
+                    $output->info("Are you sure you want to delete all those files?  Type 'yes' to continue: ");
+                    $line = fgets(STDIN);
+                    if (trim($line) != 'yes')
+                    {
+                        throw new Exception("Operation is aborted!");
+                    }
+                    Service::uninstall($name, 1);
+                }
+                catch (Exception $e)
+                {
+                    throw new Exception($e->getMessage());
+                }
+
+                $output->info("Uninstall Successed!");
+                break;
+            case 'refresh':
+                Service::refresh();
+                $output->info("Refresh Successed!");
+                break;
+            default :
+                break;
+        }
+    }
+
+    /**
+     * 写入到文件
+     * @param string $name
+     * @param array $data
+     * @param string $pathname
+     * @return mixed
+     */
+    protected function writeToFile($name, $data, $pathname)
+    {
+        $search = $replace = [];
+        foreach ($data as $k => $v)
+        {
+            $search[] = "{%{$k}%}";
+            $replace[] = $v;
+        }
+        $stub = file_get_contents($this->getStub($name));
+        $content = str_replace($search, $replace, $stub);
+
+        if (!is_dir(dirname($pathname)))
+        {
+            mkdir(strtolower(dirname($pathname)), 0755, true);
+        }
+        return file_put_contents($pathname, $content);
+    }
+
+    /**
+     * 获取基础模板
+     * @param string $name
+     * @return string
+     */
+    protected function getStub($name)
+    {
+        return __DIR__ . '/Addon/stubs/' . $name . '.stub';
+    }
+
+}

+ 45 - 0
application/admin/command/Addon/stubs/addon.stub

@@ -0,0 +1,45 @@
+<?php
+
+namespace addons\{%name%};
+
+use think\Addons;
+
+/**
+ * 插件
+ */
+class {%addonClassName%} extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+    /**
+     * 实现钩子方法
+     * @return mixed
+     */
+    public function testhook($param)
+    {
+        // 调用钩子时候的参数信息
+        print_r($param);
+        // 当前插件的配置信息,配置信息存在当前目录的config.php文件中,见下方
+        print_r($this->getConfig());
+        // 可以返回模板,模板文件默认读取的为插件目录中的文件。模板名不能为空!
+        //return $this->fetch('view/info');
+    }
+
+}

+ 40 - 0
application/admin/command/Addon/stubs/config.stub

@@ -0,0 +1,40 @@
+<?php
+
+return [
+    [
+        //配置唯一标识
+        'name'    => 'usernmae',
+        //显示的标题
+        'title'   => '用户名',
+        //类型
+        'type'    => 'string',
+        //数据字典
+        'content' => [
+        ],
+        //值
+        'value'   => '',
+        //验证规则 
+        'rule'    => 'required',
+        //错误消息
+        'msg'     => '',
+        //提示消息
+        'tip'     => '',
+        //成功消息
+        'ok'      => '',
+        //扩展信息
+        'extend'  => ''
+    ],
+    [
+        'name'    => 'password',
+        'title'   => '密码',
+        'type'    => 'string',
+        'content' => [
+        ],
+        'value'   => '',
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '',
+        'ok'      => '',
+        'extend'  => ''
+    ],
+];

+ 7 - 0
application/admin/command/Addon/stubs/info.stub

@@ -0,0 +1,7 @@
+name = {%name%}
+title = 插件名称
+intro = FastAdmin插件
+author = yourname
+website = http://www.fastadmin.net
+version = 1.0.0
+state = 1

+ 44 - 0
application/admin/command/BaseCommand.php

@@ -0,0 +1,44 @@
+<?php
+/**
+ * Created by: PhpStorm
+ * User: lytian
+ * Date: 2020/4/13
+ * Time: 13:37
+ */
+
+namespace app\admin\command;
+
+
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+use think\Env;
+use think\Request;
+
+class BaseCommand extends Command
+{
+    /**
+     * 初始化
+     *
+     * @param Input  $input  An InputInterface instance
+     * @param Output $output An OutputInterface instance
+     */
+    protected function initialize(Input $input, Output $output)
+    {
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+
+        //主从配置引入
+        if (Env::get('database.admin_deploy') == 1) {
+            Config::set("database.hostname", Env::get("database.admin_hostname"));
+            Config::set("database.hostport", Env::get("database.admin_hostport"));
+            Config::set("database.deploy", Env::get("database.admin_deploy", 1));
+            Config::set("database.rw_separate", Env::get("database.admin_rw_separate", true));
+            Config::set("database.master_num", Env::get("database.admin_master_num", 1));
+            Config::set("database.slave_no", Env::get("database.admin_slave_no", ""));
+        }
+
+        $arr = model('Config')->getConfigSiteArr();
+        Config::set('site', $arr);
+    }
+}

+ 129 - 0
application/admin/command/BookCollectSum.php

@@ -0,0 +1,129 @@
+<?php
+
+
+namespace app\admin\command;
+
+use app\common\library\Redis;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+use think\Log;
+use think\Request;
+
+class BookCollectSum extends BaseCommand
+{
+    protected function configure()
+    {
+        $this->setName('BookCollectSum')->setDescription('BookCollect数据汇总');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+
+        $redis = Redis::instance();
+
+        $yesterday = date('Ymd', strtotime('-1 days'));
+
+        Log::info('BookCollectSum: 渠道列表RedisKey:' . "BC-CL:" . $yesterday);
+
+        try {
+            //获取渠道列表
+            if (!$channel_list = $redis->smembers("BC-CL:" . $yesterday)) {
+                Log::info("BookCollectSum: RedisKey:BC-CL:{$yesterday} Date:{$yesterday} 渠道列表为空");
+                $output->info("BookCollectSum: RedisKey:BC-CL:{$yesterday} Date:{$yesterday} 渠道列表为空");
+                return;
+            }
+            foreach ($channel_list as $channel_id) {
+                //获取渠道书籍列表
+                Log::info('BookCollectSum: 渠道书籍列表RedisKey:' . "BC-BL:{$channel_id}:" . $yesterday);
+                $output->info('BookCollectSum: 渠道书籍列表RedisKey:' . "BC-BL:{$channel_id}:" . $yesterday);
+                if (!$book_list = $redis->smembers("BC-BL:{$channel_id}:" . $yesterday)) {
+                    Log::info("BookCollectSum: RedisKey:BC-BL:{$channel_id}:{$yesterday} Date:{$yesterday} ChannelID:{$channel_id} 书籍列表为空,跳过统计");
+                    $output->info("BookCollectSum: RedisKey:BC-BL:{$channel_id}:{$yesterday} Date:{$yesterday} ChannelID:{$channel_id} 书籍列表为空,跳过统计");
+                    continue;
+                }
+                foreach ($book_list as $book_id) {
+                    Log::info('BookCollectSum: 书籍RedisKey:' . "BC:{$book_id}:{$channel_id}:{$yesterday}");
+                    $output->info('BookCollectSum: 书籍RedisKey:' . "BC:{$book_id}:{$channel_id}:{$yesterday}");
+                    $data = json_decode($redis->get("BC:{$book_id}:{$channel_id}:{$yesterday}"), true);
+                    Log::info('BookCollectSum: 书籍统计正文  key:' . "BC:{$book_id}:{$channel_id}:{$yesterday} data:" . json_encode($data));
+                    $output->info('BookCollectSum: 书籍统计正文  key:' . "BC:{$book_id}:{$channel_id}:{$yesterday} data:" . json_encode($data));
+                    $count_map = [
+                        'type' => '3',
+                        'admin_id' => $channel_id,
+                        'book_id' => $book_id,
+                    ];
+                    $count_data = model('BookCollect')->where($count_map)->find();
+                    if ($count_data) {
+                        Log::info("BookCollect Source Data: admin_id:{$channel_id} book_id:{$book_id} data:" . json_encode($count_data->toArray()));
+                        $output->info("BookCollect Source Data: admin_id:{$channel_id} book_id:{$book_id} data:" . json_encode($count_data->toArray()));
+                        $update_data['uv'] = $count_data['uv'];
+                        $update_data['pv'] = $count_data['pv'];
+                        $update_data['recharge_suc_users'] = $count_data['recharge_suc_users'];
+                        $update_data['recharge_users'] = $count_data['recharge_users'];
+                        $update_data['recharge_money'] = $count_data['recharge_money'] + ($data['recharge_money'] ?? 0) * 1 / 100;
+                        $update_data['recharge_num'] = $count_data['recharge_num'] + ($data['recharge_num'] ?? 0);
+                        $update_data['spending_count_kandian'] = $count_data['spending_count_kandian'] + ($data['spending_count_kandian'] ?? 0);
+                        $update_data['spending_recharge_kandian'] = $count_data['spending_recharge_kandian'] + ($data['spending_recharge_kandian'] ?? 0);
+                        $update_data['spending_free_kandian'] = $count_data['spending_free_kandian'] + ($data['spending_free_kandian'] ?? 0);
+                        $update_data['spending_users'] = $count_data['spending_users'] + ($data['spending_users'] ?? 0);
+                        $update_data['spending_num'] = $count_data['spending_num'] + ($data['spending_num'] ?? 0);
+                        Log::info("BookCollect Update Data: AdminId:{$channel_id} BookId:{$book_id} Data:" . json_encode($update_data));
+                        $output->info("BookCollect Update Data: AdminId:{$channel_id} BookId:{$book_id} Data:" . json_encode($update_data));
+                        if ($is_update = $count_data->where($count_map)->update($update_data)) {
+                            Log::info('BookCollect update sql success');
+                            $output->info('BookCollect update sql success');
+                        } else {
+                            Log::error('BookCollect update sql failed');
+                            $output->error('BookCollect update sql failed');
+                        }
+                    } else {
+                        $insert_data = [
+                            'type' => '3',
+                            'admin_id' => $channel_id,
+                            'book_id' => $book_id,
+                            'createdate' => '20180101',
+                            'uv' => 0,
+                            'pv' => 0,
+                            'recharge_users' => 0,
+                            'recharge_suc_users' => 0,
+                            'recharge_money' => ($data['recharge_money'] ?? 0) * 1 / 100,
+                            'recharge_num' => ($data['recharge_num'] ?? 0),
+                            'spending_count_kandian' => ($data['spending_count_kandian'] ?? 0),
+                            'spending_recharge_kandian' => ($data['spending_recharge_kandian'] ?? 0),
+                            'spending_free_kandian' => ($data['spending_free_kandian'] ?? 0),
+                            'spending_users' => ($data['spending_users'] ?? 0),
+                            'spending_num' => ($data['spending_num'] ?? 0),
+                            'createtime' => time(),
+                            'updatetime' => time()
+                        ];
+                        Log::info("BookCollect Source Insert Data: AdminId:{$channel_id} BookId:{$book_id} Data:" . json_encode($insert_data));
+                        $output->error("BookCollect Source Insert Data: AdminId:{$channel_id} BookId:{$book_id} Data:" . json_encode($insert_data));
+                        if ($is_insert = model('BookCollect')->insert($insert_data)) {
+                            Log::info('BookCollect insert sql success');
+                            $output->info('BookCollect insert sql success');
+                        } else {
+                            Log::error('BookCollect insert sql failed');
+                            $output->error('BookCollect insert sql failed');
+                        }
+                    }
+                }
+            }
+        } catch (\Exception $exception) {
+            $msg = 'BookCollect '.json_encode([
+                    "code" => $exception->getCode(),
+                    "file" => $exception->getFile(),
+                    "line" => $exception->getLine(),
+                    "msg" => $exception->getMessage(),
+                    "trace" => $exception->getTraceAsString(),
+                ], JSON_UNESCAPED_UNICODE);
+            Log::error($msg);
+            $output->error($msg);
+        }
+        Log::info('BookCollect over');
+        $output->info('BookCollect over');
+    }
+
+
+}

+ 45 - 0
application/admin/command/BookEnum.php

@@ -0,0 +1,45 @@
+<?php
+
+
+namespace app\admin\command;
+
+use app\common\model\Book;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+use think\Db;
+use think\Request;
+
+class BookEnum extends Command
+{
+
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('BookEnum')->setDescription('遍历 book mysql 与 book change redis');
+    }
+    protected function execute(Input $input, Output $output)
+    {
+
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+
+        try {
+            /** @var Book $bookModel */
+            $bookModel = model('book');
+            Db::table('book')->field('id,name,realname')->chunk(100, function ($bookList) use ($bookModel, $output) {
+                foreach ($bookList as $book) {
+                    $bookInfo = $bookModel::getBookInfo($book['id']);
+                    $output->info("mysql book:" . json_encode($book,
+                            JSON_UNESCAPED_UNICODE) . " redis book:" . json_encode($bookInfo, JSON_UNESCAPED_UNICODE));
+                }
+            });
+        } catch (\Throwable $Th) {
+            $output->info('error msg:' . $Th->getMessage().' trace:'.$Th->getTraceAsString());
+        }
+        $output->info('book enum over');
+    }
+
+
+}

+ 51 - 0
application/admin/command/BookRelationInit.php

@@ -0,0 +1,51 @@
+<?php
+
+
+namespace app\admin\command;
+
+use app\common\library\Redis;
+use app\common\service\BookRelationService;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\Log;
+use think\Request;
+
+class BookRelationInit extends Command
+{
+
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('BookRelationInit');
+    }
+    protected function execute(Input $input, Output $output)
+    {
+
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+
+        $redis = Redis::instance();
+
+        try {
+            $ids = model('book_relation')->field('book_id')->group('book_id')->select();
+            if ( empty($ids)){
+                Log::info('table: book_relation empty' );
+            }
+            foreach ($ids as $k => $v) {
+                $str = BookRelationService::instance()->getBookRelationById($v['book_id']);
+                if ( $redis->Exists( 'B:'.$v['book_id']) ){
+                    $redis->hSet( 'B:'.$v['book_id'], 'relation_id', $str );
+                    Log::info('book_relation redis update:'.$v['book_id'].' relations:'.$str );
+                }
+            }
+        } catch (\Throwable $Th) {
+            Log::info('error:'.$Th->getMessage());
+        }
+        Log::info('BookRelationInit over');
+    }
+
+
+}

+ 62 - 0
application/admin/command/BuildTestUrl.php

@@ -0,0 +1,62 @@
+<?php
+
+namespace app\admin\command;
+
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Request;
+
+class BuildTestUrl extends Command
+{
+    const CPS_FILE_PATH = '/tmp/cps.txt';
+    const H5_FILE_PATH = '/tmp/h5.txt';
+
+
+    protected function configure()
+    {
+        $this->setName('BuildTestUrl')
+            ->addOption('path', 'p', Option::VALUE_OPTIONAL, '书籍id文件的路径')
+            ->setDescription('生成章节页面测试url');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin');
+        $bookIdFilePath = $input->getOption('path');
+        echo $bookIdFilePath;
+        $handle = fopen($bookIdFilePath, 'r');
+
+        while (!feof($handle)) {
+            $buffer = fgets($handle, 4096);
+            $bookId = trim($buffer);
+            if (empty($bookId)) {
+                break;
+            }
+
+            $aChapter = $this->getChapterList($bookId);
+            array_walk($aChapter, function ($cid) use ($bookId) {
+                $cpsUrl = "https://wx79bfdb38ba28b749.zsjwn.cn/index/book/chapter?book_id=$bookId&chapter_id=$cid\r\n";
+                file_put_contents(self::CPS_FILE_PATH, $cpsUrl, FILE_APPEND);
+
+                $h5Url = "http://m.kkyd.cn/book.html?bookId=$bookId&chapterId=$cid\r\n";
+                file_put_contents(self::H5_FILE_PATH, $h5Url, FILE_APPEND);
+            });
+
+        }
+
+        echo "执行完成\r\n";
+        echo "CPS链接文件路径:".self::CPS_FILE_PATH."\r\n";
+        echo "h5链接文件路径:".self::H5_FILE_PATH."\r\n";
+
+    }
+
+    protected function getChapterList($bookId)
+    {
+        $list = model('Book')->getChapterList($bookId, 1, -1);
+        $list = $list['data']['data'] ?? [];
+        $list = array_column($list, 'id');
+        return $list;
+    }
+}

+ 91 - 0
application/admin/command/CampaignAward.php

@@ -0,0 +1,91 @@
+<?php
+
+namespace app\admin\command;
+
+use app\common\library\Redis;
+use app\common\service\CampaignService;
+use app\main\constants\CampaignConstants;
+use ReflectionClass;
+use ReflectionMethod;
+use think\Cache;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\Exception;
+use think\Request;
+
+class CampaignAward extends Command
+{
+
+
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('CampaignAward')
+            ->addArgument('date', Argument::REQUIRED, "时间");
+    }
+    protected function execute(Input $input, Output $output)
+    {
+        $date = $input->getArgument('date');
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+        if ( !$date ){
+            $date = date( 'Ymd', time()-3600*24*8 );
+        }
+        $data = model('CampaignMatch')->where([
+            'match_date' => $date,
+        ])->select();
+        if ( !empty($data) ){
+            $activeId = $data[0]->campaign_id;
+            $active = $this->getActivityById($activeId);
+            //每个场次的基数
+            $baseNum =$active['elementary_num']*$active['elementary_need']+$active['intermediate_num']*$active['intermediate_need']+$active['advanced_num']*$active['advanced_need'];
+            echo '基数'.$baseNum;
+            //实际报名和成功的人数
+            $signNum = 0;
+            $successNum = 0;
+            foreach ($data as $k =>$v){
+                $signNum += $v['kandian']*$v['participator_num'];
+                $successNum += $v['kandian']*$v['success_num'];
+            }
+            echo '实际打卡'.$signNum;
+            echo '实际成功'.$successNum;
+            //官方补贴+基数+实际报名人数
+            $signNum += $baseNum+$active['subsidy_need'];
+            echo '报名:'.$signNum;
+            //基数+实际成功的人数
+            $successNum += $baseNum;
+            echo '成功:'.$successNum;
+            //升值倍数
+            $awardPercent = !empty($successNum) ?round($signNum/$successNum,2) : 1;
+            echo '系数'.$awardPercent;
+            foreach ($data as $k=>$v) {
+                $update[$k]['id'] = $v['id'];
+                $update[$k]['award'] = intval($v['kandian']*$awardPercent);
+            }
+            model('CampaignMatch')->isUpdate()->saveAll($update);
+            Redis::instance()->del(CampaignConstants::getLastAwardMatch());
+            Redis::instance()->del(CampaignConstants::getCampaignMatchRedisKey($date,$active['elementary_need']));
+            Redis::instance()->del(CampaignConstants::getCampaignMatchRedisKey($date,$active['intermediate_need']));
+            Redis::instance()->del(CampaignConstants::getCampaignMatchRedisKey($date,$active['advanced_need']));
+            echo '计算成功';exit;
+        }
+        echo '数据为空';
+    }
+    protected  function getActivityById($activeId){
+
+        $activiy = model('CampaignRead')
+            ->where([
+                'id'=>$activeId
+            ])
+            ->find();
+        $activiy = is_array($activiy) ? $activiy : $activiy->toArray();
+        return $activiy;
+    }
+
+}

+ 131 - 0
application/admin/command/CampaignStatistics.php

@@ -0,0 +1,131 @@
+<?php
+
+
+namespace app\admin\command;
+
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\Db;
+use think\Log;
+use think\Request;
+
+/**
+ * 消耗活动统计订单的脚本(废弃-订单表将迁移至polardb)
+ * Class CampaignStatistics
+ * @package app\admin\command
+ */
+class CampaignStatistics extends Command
+{
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('CampaignStatistics')
+            ->addArgument('date', Argument::REQUIRED, "时间");
+    }
+    protected function execute(Input $input, Output $output)
+    {
+        $date = $input->getArgument('date');
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+        if ( !$date ){
+            $date = date( 'Ymd', time()-3600*24*8 );
+        }
+        $sql = "select user_id,money,FROM_UNIXTIME(createtime) as ordertime,createtime from orders_extend e JOIN orders o on e.order_id=o.id where finishtime>0 and FROM_UNIXTIME(createtime,'%Y%m%d')=".$date;
+        $data = model('orders_extend')->query($sql);
+        $title = 'user_id,money,ordertime,ordertime,isCheckIn,isCharge'.PHP_EOL;
+        file_put_contents(LOG_PATH.'/order'.$date.'.csv',$title,FILE_APPEND);
+        if ( !empty($data) ){
+            foreach ($data as $k=>$v){
+                $str = '';
+                $userMatch = $this->dbConnect($v['user_id'],'user')->table('user_match')
+                    ->field('id,createtime')
+                    ->where('user_id','eq',$v['user_id'] )
+                    ->where('match_date','eq',$date )
+                    ->find();
+                $recharge = $this->dbConnect($v['user_id'],'shard')->table('consume')->where('createtime','<',$v['createtime'])->find();
+                $v['isCheckIn'] = empty($userMatch) ? 0 : $userMatch['createtime'];
+                $v['isRcharge'] = empty($recharge) ? 0 : date('Y-m-d H:i:s',$recharge['createtime'] );
+               foreach($v as $kk =>$vv ){
+                   $str .= $vv.',';
+               }
+              $str = trim($str,',').PHP_EOL;
+                file_put_contents(LOG_PATH.'/order'.$date.'.csv',$str,FILE_APPEND);
+                log::info('消耗活动订单和报名对比:'.json_encode($v));
+            }
+            echo 'success';
+        }else{
+            echo '没有消耗活动类型订单';
+        }
+
+    }
+    /**
+     * 获取数据库连接
+     * @param $param 编号
+     * @param string $deploy 业务
+     * @return \think\db\Connection
+     * @throws \think\Exception
+     */
+    private function dbConnect($param, $deploy)
+    {
+        $db_config = $this->get_db_deploy($param, $deploy);
+        if (empty($this->dbConnects[$db_config['database']])) {
+            Log::info(sprintf('打开数据库连接,database:%s', $db_config['database']));
+            $this->dbConnects[$db_config['database']] = Db::connect($db_config);
+        }
+        return $this->dbConnects[$db_config['database']];
+    }
+
+    /**
+     * 关闭数据库连接
+     */
+    private function closeDbConnect()
+    {
+        foreach ($this->dbConnects as $database => $dbConnect) {
+            Log::info(sprintf('关闭数据库连接,database:%s', $database));
+            $dbConnect->close();
+        }
+        $this->dbConnects = [];
+    }
+
+    /**
+     * 获取db分库的配置参数
+     *
+     * @param string|int $param 取模值
+     * @param string $deploy 分库前缀
+     * @return array
+     */
+    function get_db_deploy($param, $deploy = 'shard')
+    {
+        $db = Config::get('db');
+        $mod = $param % $db[$deploy . '_num'];
+        $mod = abs($mod);
+        $list = explode(';', $db[$deploy . '_list']);
+        foreach ($list as $item) {
+            $con = explode(':', $item); // 0=0-191库编号 1=192.168.1.149主IP 2=3306主端口 3=192.168.1.150从IP 4=3306从端口
+            if (count($con) >= 3) {
+                $c = explode('-', $con[0]); //库编号  0开始 1结束
+                if (count($c) >= 2) {
+                    if ($c[0] <= $mod && $mod <= $c[1]) {
+                        $database = Config::get('database');
+                        if ($database['deploy'] == 1 && count($con) >= 5) { //开启主从 & 带主从配置
+                            $database['hostname'] = $con[1] . ',' . $con[3]; //192.168.1.149,192.168.1.150
+                            $database['hostport'] = $con[2] . ',' . $con[4]; //3306,3306
+                        } else { //只有主库
+                            $database['hostname'] = $con[1];
+                            $database['hostport'] = $con[2];
+                        }
+                        $database['database'] = str_replace('$mod', $mod, $db[$deploy . '_database']);
+                        return $database;
+                    }
+                }
+            }
+        }
+        Log::error("分库获取失败!");
+        return [];
+    }
+}

+ 253 - 0
application/admin/command/ChangeMenu.php

@@ -0,0 +1,253 @@
+<?php
+
+/*
+ * 检测公众号是否被封
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\common\library\WeChatObject;
+use EasyWeChat\Factory;
+use Symfony\Component\Cache\Simple\RedisCache;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use app\common\model\AdminConfig;
+use app\common\model\Config as dbconfig;
+use think\Log;
+use think\Exception;
+use think\Request;
+
+
+class ChangeMenu extends Command
+{
+    protected $message = '';
+
+    protected function configure()
+    {
+        $this->setName('ChangeMenu')
+            ->addOption("find","f",Option::VALUE_REQUIRED,'查找要替换的菜单域名')
+            ->addOption("replace","r",Option::VALUE_REQUIRED,'替换菜单域名为当前域名')
+            ->addOption("delay","d",Option::VALUE_REQUIRED,'休眠时间')
+            ->addOption('channel',"c",Option::VALUE_REQUIRED,'替换的渠道OR配号代理商例: 1 or 1,2,3,4 or 1-100 or 1-20&30-40')
+            ->addOption('params','p',Option::VALUE_REQUIRED,'添加参数例: type=1 or type=1&name=ss')
+            ->setDescription('替换微信菜单地址');
+    }
+
+    protected function execute(Input $input, Output $output){
+        $channel_map = null;
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+        //查找替换
+        $find = $input->getOption('find');
+        //替换地址
+        $replace = $input->getOption('replace');
+        //获取休眠时间
+        $sleep = $input->getOption('delay');
+        //获取渠道ID
+        $channel_id = $this->getInputChannelParams($input);
+        //获取参数
+        parse_str(trim($input->getOption('params'),'&'),$params);
+        if($channel_id){
+            $channel_map['admin_id'] = ['in',$channel_id];
+        }
+        $output->info('ChangeMenu -----------------------------------> Start');
+        $output->info('Params: find = '.$find);
+        $output->info('Params: replace = '.$replace);
+        $output->info('Params: sleep = '.$sleep);
+        $output->info('Params: channel_id = '.$channel_id);
+        $output->info('Params: params = '.var_export($params,true));
+        Log::info("ChangeMenu->Params: find:{$find},replace:{$replace},sleep:{$sleep},channel_id:{$channel_id},params:".var_export($params,true));
+        if(!$adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,refresh_token,wx_menu')->select()){
+            $output->error('ChangeMenu->Error:渠道OR配号代理商为空');
+            return;
+        }
+        $output->info('ChangeMenu->SQL:'.model('AdminConfig')->getLastSql());
+        Log::info('ChangeMenu->SQL:'.model('AdminConfig')->getLastSql());
+        foreach($adminConfig as $channel){
+            //微信菜单为空时跳过
+            if(empty($channel['wx_menu'])){
+                $output->info('ChangeMenu->Source->Menu:'.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Menu Is NULL");
+                continue;
+            }
+            Log::info('ChangeMenu->Source->Menu:'.var_export($channel['wx_menu'],true));
+            //为查找替换时,值不匹配时处理
+            $channel['wx_menu'] = json_encode($channel['wx_menu'],JSON_UNESCAPED_UNICODE);
+            if(!empty($find) && !strpos($channel['wx_menu'],$find)){
+                $output->info('ChangeMenu->Find->Replace: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mismatch");
+                Log::info('ChangeMenu->Find->Replace: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mismatch");
+                continue;
+            }
+            //为查找替换时,替换指定地址
+            if($find && $replace){
+                $channel['wx_menu'] = str_replace($find,$replace,$channel['wx_menu']);
+            }
+            $menu = json_decode($channel['wx_menu'],true);
+
+            foreach($menu as $key => &$val){
+                if(isset($val['url'])){
+                    //直接替换顶级域名
+                    if(empty($find) && $replace){
+                        if(preg_match("/\/\/wx[A-Za-z0-9]+./i", $val['url'])){
+                            $val['url'] = $this->replaceDomain($val['url'],$replace);
+                        }
+//                        //检测入口域名
+//                        if($isEntry = $this->checkIsEntryHost($val['url'])){
+//                            $val['url'] = $this->replaceDomain($val['url'],$replace,$isEntry);
+//                        }
+                    }
+                    //参数不为空时,追加参数
+                    if($params){
+                        $val['url'] = $this->addUrlParams($val['url'],$params);
+                    }
+                }
+                if(isset($val['sub_button']) && !empty($val['sub_button'])){
+                    foreach($val['sub_button'] as $sub_k => &$sub_v){
+                        if(isset($sub_v['url'])){
+                            //直接替换顶级域名
+                            if(empty($find) && $replace) {
+                                if (preg_match("/\/\/wx[A-Za-z0-9]+./i", $sub_v['url'])) {
+                                    $sub_v['url'] = $this->replaceDomain($sub_v['url'], $replace);
+                                }
+//                                //检测入口域名
+//                                if($isEntry = $this->checkIsEntryHost($sub_v['url'])){
+//                                    $sub_v['url'] = $this->replaceDomain($sub_v['url'], $replace,$isEntry);
+//                                }
+                            }
+                            //参数不为空时,追加参数
+                            if($params){
+                                $sub_v['url'] = $this->addUrlParams($sub_v['url'],$params);
+                            }
+                        }
+                    }
+                }
+            }
+            $channel['wx_menu'] = $menu;
+            Log::info('ChangeMenu->Replace->Menu:'.var_export($channel['wx_menu'],true));
+            try {
+                //更新菜单
+                $admin = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                if(empty($admin['refresh_token'])){
+                    Log::info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} refresh_token Empty!!");
+                    continue;
+                }
+                $wechat = new WeChatObject($admin);
+                $officialAccount = $wechat->getOfficialAccount();
+//                $officialAccount->menu->delete(); //删除全部菜单 主要是掌中云个性化菜单
+                $ret = $officialAccount->menu->create($channel['wx_menu']);
+                if ($ret['errcode'] == 0) {
+                    $output->info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                    Log::info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                    //更新数据
+                    $update = model('AdminConfig')->update(['wx_menu' => $channel['wx_menu']], ["admin_id" => $channel['admin_id']]);
+                    if ($update) {
+                        $output->info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                        Log::info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                    } else {
+                        $output->info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                        Log::error('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                    }
+                } else {
+                    $output->info('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                    Log::error('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                }
+                //休眠
+                if (!empty($sleep) && is_numeric($sleep)) {
+                    sleep($sleep);
+                }
+            }catch (\Exception $exception){
+                Log::error('ChangeMenu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+            }
+        }
+        $output->info('ChangeMenu -----------------------------------> End');
+    }
+
+    /**
+     * 检测是否是入口域名
+     * @param $url
+     * @return bool
+     */
+    protected function checkIsEntryHost($url){
+        $entryHost = model('Entryhost')->getHosts();
+        foreach($entryHost as $host){
+            if (preg_match("/{$host}/i", $url)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 替换顶级域名
+     * @param $source_url
+     * @param $replace
+     * @param $isEntry
+     * @return string
+     */
+    protected function replaceDomain($source_url,$replace,$isEntry = false){
+        $url = '';
+        $url_arr = parse_url($source_url);
+        if($isEntry){
+            $url .= $url_arr['scheme'].'://'.$replace.($url_arr['path'] ?? '');
+        }else{
+            $app_id = explode('.',$url_arr['host'])[0];
+            $url .= $url_arr['scheme'].'://'.$app_id.'.'.$replace.($url_arr['path'] ?? '');
+        }
+        if(isset($url_arr['query']) && !empty($url_arr['query'])){
+            $url .= '?'.$url_arr['query'];
+        }
+        return $url;
+    }
+
+    /**
+     * URL地址追加参数
+     * @param $url
+     * @param $params
+     * @return string
+     */
+    protected function addUrlParams($url,$params){
+        $url_arr = parse_url($url);
+        parse_str(($url_arr['query'] ?? ''),$url_params);
+        $url_params = array_merge($url_params,$params);
+        $url_params = array_filter($url_params,function($val){if($val != '')return true;});
+        $url = $url_arr['scheme'].'://'.$url_arr['host'].($url_arr['path']??'');
+        if($url_params){
+            $url .= '?'.http_build_query($url_params);
+        }
+        return $url;
+    }
+
+    /**
+     * 获取渠道ID
+     * @param Input $input
+     * @return string
+     */
+    protected function getInputChannelParams(Input $input){
+        $channel = $input->getOption('channel');
+        $channelIds = [];
+        if($channel_params = explode('&',$channel)){
+            foreach($channel_params as $val){
+                if($index = explode('-',$val)){
+                    if(isset($index[0]) && isset($index[1]) && !empty($index[0]) && !empty($index[1]) && ($index[0] < $index[1])){
+                        for($i = $index[0];$i<=$index[1];$i++){
+                            if(!in_array($i,$channelIds)){
+                                array_push($channelIds,$i);
+                            }
+                        }
+                    }else{
+                        if(isset($index[0]) && !empty($index[0])){
+                            if($ids = explode(',',$index[0])){
+                                $channelIds = array_merge($channelIds,$ids);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return implode(',',$channelIds);
+    }
+}

+ 163 - 0
application/admin/command/ChangeMenuKefu.php

@@ -0,0 +1,163 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Elton
+ * Date: 2019/7/31
+ * Time: 11:19
+ *
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\WeChatObject;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\Log;
+use think\Request;
+use think\response\Json;
+
+class ChangeMenuKefu extends Command
+{
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('ChangeMenuKefu')
+            ->addArgument('channel_id', Argument::OPTIONAL, '需要更新的渠道OR配号代理商ID,不传值则处理所有')
+            ->addArgument('nums', Argument::OPTIONAL, 'Limit条数限制')
+            ->setDescription('更新微信菜单(联系客服)');
+    }
+
+    /**
+     * 执行命令:
+     * php think ChangeMenuZhiChi          # 更新所有微信菜单
+     * php think ChangeMenuZhiChi 48 1     # 仅更新渠道商ID为48的微信菜单
+     * php think ChangeMenuZhiChi 48 100   # 更新渠道商ID 大于或等于48的微信菜单,更新100条
+     *
+     * @param Input $input
+     * @param Output $output
+     * @return int|null|void
+     */
+    protected function execute(Input $input, Output $output)
+    {
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+
+        $channel_map = null;
+        $channel_map['wx_menu'] = ['neq', ''];
+        $output->writeln("开始执行刷新菜单:" . date('Y-m-d H:i:s', time()));
+
+        $channel_id = $input->getArgument('channel_id');
+        if(empty($channel_id) || !is_numeric($channel_id)){
+            $channel_id = '';
+        }
+        if($channel_id){
+            $channel_map['admin_id'] = ['>=',$channel_id];
+        }
+
+        $nums = is_numeric($input->getArgument('nums')) ? $input->getArgument('nums') : 0;
+
+        $output->writeln("参数channel_id:".$channel_id);
+        $output->writeln("参数nums:".$nums);
+
+        if($nums){
+            $adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,ophost_id,refresh_token,wx_menu')->limit($nums)->select();
+        } else{
+            $adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,ophost_id,refresh_token,wx_menu')->select();
+        }
+
+        //1.查找到所有配置了微信菜单的渠道商OR配号代理商
+        if(!$adminConfig){
+            $output->error('ChangeMenuWx->Error:没有找到可用数据');
+            return;
+        }
+        $output->info('ChangeMenuWx->SQL:'.model('AdminConfig')->getLastSql());
+        Log::info('ChangeMenuWx->SQL:'.model('AdminConfig')->getLastSql());
+
+
+        try{
+            foreach ($adminConfig as $channel)
+            {
+                // 获取公众号配置信息
+                $ophost = model('Ophost')->getInfoById($channel['ophost_id']);
+                $url_pre = $channel['appid'].'.'.$ophost['host'];
+
+                //将新的菜单结构推送给微信
+                $nMenu = $this->rePackageMenu($url_pre, $channel['wx_menu']);
+
+                $admin = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                if(empty($admin['refresh_token'])){
+                    Log::info('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} refresh_token Empty!!");
+                    continue;
+                }
+                $wechat = new WeChatObject($admin);
+                $officialAccount = $wechat->getOfficialAccount();
+                try {
+                    $ret = $officialAccount->menu->create($nMenu);
+                } catch (\Exception $exception) {
+                    Log::error('ChangeMenuWx->WeChat->Menu:' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail");
+                    Log::error('Because:' . $exception->getMessage());
+                }
+                if(isset($ret)){
+                    Log::info('ChangeMenuWx->WeChat->Menu: ret: '.print_r(json_encode($ret), true));
+                    if ($ret['errcode'] == 0) {
+                        $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                        Log::info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                        //4.维护admin_config表中的wx_menu字段
+                        $update = model('AdminConfig')->update(['wx_menu' => $nMenu], ["admin_id" => $channel['admin_id']]);
+                        if ($update) {
+                            $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                            Log::info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                        } else {
+                            $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                            Log::error('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                        }
+                    } else {
+                        $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                        Log::error('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                    }
+                }
+                // 暂停50毫秒
+                usleep(50000);
+            }
+        }catch (\Exception $exception){
+            $output->writeln('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+            Log::error('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+        }
+
+        $output->writeln("刷新菜单完成:" . date('Y-m-d H:i:s', time()));
+    }
+
+
+    /**
+     * 重新组装微信菜单数据
+     * @param $url_pre
+     * @param array $oldMenu
+     * @return array
+     */
+    public function rePackageMenu($url_pre, array $oldMenu)
+    {
+        $tmpMenu = [];
+        foreach ($oldMenu as $item)
+        {
+            if(!isset($item['sub_button'])){
+                array_push($tmpMenu, $item);
+            }else{
+                foreach ($item['sub_button'] as $key => $subitem) {
+                    if($subitem['name'] == '联系客服'){
+                        unset($subitem['url']);
+                        $subitem['key'] = 'manager_contact';
+                        $subitem['type'] = 'click';
+                        $item['sub_button'][$key] = $subitem;
+                    }
+                }
+                array_push($tmpMenu, $item);
+            }
+        }
+        return $tmpMenu;
+    }
+}

+ 141 - 0
application/admin/command/ChangeMenuSign.php

@@ -0,0 +1,141 @@
+<?php
+
+/*
+ * 替换微信菜单内的签到链接为签到交互
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\common\library\WeChatObject;
+use EasyWeChat\Factory;
+use Symfony\Component\Cache\Simple\RedisCache;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use app\common\model\AdminConfig;
+use app\common\model\Config as dbconfig;
+use think\Log;
+use think\Exception;
+use think\Request;
+
+
+class ChangeMenuSign extends Command
+{
+    protected $message = '';
+
+    protected function configure()
+    {
+        $this->setName('ChangeMenuSign')
+            ->addOption("adminid","a",Option::VALUE_REQUIRED,'要单独处理的adminid 支持英文逗号分隔的多个')
+            ->addOption("delay","d",Option::VALUE_REQUIRED,'休眠时间,单位秒')
+            ->setDescription('替换微信菜单内的签到链接为签到交互');
+    }
+
+    protected function execute(Input $input, Output $output){
+        $channel_map = [];
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+
+        $admin_id = $input->getOption('adminid');
+        //获取休眠时间
+        $sleep = $input->getOption('delay');
+        $output->info('ChangeMenuSign -----------------------------------> Start');
+
+        if (!empty($admin_id)) {
+            $channel_map['admin_id'] = ['in', $admin_id];
+        }
+        if(!$adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,refresh_token,wx_menu')->select()){
+            $output->error('ChangeMenuSign->Error:渠道OR配号代理商为空');
+            return;
+        }
+        $output->info('ChangeMenuSign->SQL:'.model('AdminConfig')->getLastSql());
+        Log::info('ChangeMenuSign->SQL:'.model('AdminConfig')->getLastSql());
+        foreach($adminConfig as $channel){
+            //微信菜单为空时跳过
+            if(empty($channel['wx_menu'])){
+                $output->info('ChangeMenuSign->Source->Menu:'.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Menu Is NULL");
+                continue;
+            }
+            Log::info('ChangeMenuSign->Source->Menu:'.var_export($channel['wx_menu'],true));
+            $menu = $channel['wx_menu'];
+            $hasSign = false;
+            foreach($menu as $key => &$val){
+                if(isset($val['key']) && $val['key'] == '签到'){
+                    $hasSign = true;
+                }
+                if(isset($val['url'])){
+                    if(stripos($val['url'],'/index/user/sign') !== false){
+                        $hasSign = true;
+                        $val = [
+                            'key' => '签到',
+                            'name' => '每日签到',
+                            'type' => 'click'
+                        ];
+                    }
+                }
+                if(isset($val['sub_button']) && !empty($val['sub_button'])){
+                    foreach($val['sub_button'] as $sub_k => &$sub_v){
+                        if(isset($sub_v['url'])){
+                            if(stripos($sub_v['url'],'/index/user/sign') !== false){
+                                $hasSign = true;
+                                $sub_v = [
+                                    'key' => '签到',
+                                    'name' => '每日签到',
+                                    'type' => 'click'
+                                ];
+                            }
+                        }
+                    }
+                    if ($key == 2 && !$hasSign && count($val['sub_button']) < 5) {
+                        array_unshift($val['sub_button'],[
+                            'key' => '签到',
+                            'name' => '每日签到',
+                            'type' => 'click'
+                        ]);
+                    }
+                }
+            }
+            $channel['wx_menu'] = $menu;
+            Log::info('ChangeMenuSign->Replace->Menu:'.var_export($channel['wx_menu'],true));
+            try {
+                //更新菜单
+                $admin = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                if(empty($admin['refresh_token'])){
+                    Log::info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} refresh_token Empty!!");
+                    continue;
+                }
+                $wechat = new WeChatObject($admin);
+                $officialAccount = $wechat->getOfficialAccount();
+//                $officialAccount->menu->delete(); //删除全部菜单 主要是掌中云个性化菜单
+                $ret = $officialAccount->menu->create($channel['wx_menu']);
+                if ($ret['errcode'] == 0) {
+                    $output->info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                    Log::info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                    //更新数据
+                    $update = model('AdminConfig')->update(['wx_menu' => $channel['wx_menu']], ["admin_id" => $channel['admin_id']]);
+                    if ($update) {
+                        $output->info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                        Log::info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                    } else {
+                        $output->info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                        Log::error('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                    }
+                } else {
+                    $output->info('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                    Log::error('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                }
+                //休眠
+                if (!empty($sleep) && is_numeric($sleep)) {
+                    sleep($sleep);
+                }
+            }catch (\Exception $exception){
+                Log::error('ChangeMenuSign->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:".$exception->getLine().' ' . $exception->getMessage());
+            }
+        }
+        $output->info('ChangeMenuSign -----------------------------------> End');
+    }
+}

+ 156 - 0
application/admin/command/ChangeMenuSignDaiShu.php

@@ -0,0 +1,156 @@
+<?php
+
+/*
+ * 替换微信菜单内的签到链接为签到交互 将第一个菜单继续阅读改为签到
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\common\library\WeChatObject;
+use EasyWeChat\Factory;
+use Symfony\Component\Cache\Simple\RedisCache;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use app\common\model\AdminConfig;
+use app\common\model\Config as dbconfig;
+use think\Log;
+use think\Exception;
+use think\Request;
+
+
+class ChangeMenuSignDaiShu extends Command
+{
+    protected $message = '';
+
+    protected function configure()
+    {
+        $this->setName('ChangeMenuSignDaiShu')
+            ->addOption("adminid","a",Option::VALUE_REQUIRED,'要单独处理的adminid 支持英文逗号分隔的多个')
+            ->addOption("delay","d",Option::VALUE_REQUIRED,'休眠时间,单位秒')
+            ->setDescription('替换微信菜单内的签到链接为签到交互');
+    }
+
+    protected function execute(Input $input, Output $output){
+        $channel_map = [];
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+
+        $admin_id = $input->getOption('adminid');
+        //获取休眠时间
+        $sleep = $input->getOption('delay');
+        $output->info('ChangeMenuSignDaiShu -----------------------------------> Start');
+
+        if (!empty($admin_id)) {
+            $channel_map['admin_id'] = ['in', $admin_id];
+        }
+        if(!$adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,refresh_token,wx_menu')->select()){
+            $output->error('ChangeMenuSignDaiShu->Error:渠道OR配号代理商为空');
+            return;
+        }
+        $output->info('ChangeMenuSignDaiShu->SQL:'.model('AdminConfig')->getLastSql());
+        Log::info('ChangeMenuSignDaiShu->SQL:'.model('AdminConfig')->getLastSql());
+        foreach($adminConfig as $channel){
+            //微信菜单为空时跳过
+            if(empty($channel['wx_menu'])){
+                $output->info('ChangeMenuSignDaiShu->Source->Menu:'.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Menu Is NULL");
+                continue;
+            }
+            Log::info('ChangeMenuSignDaiShu->Source->Menu:'.var_export($channel['wx_menu'],true));
+            $menu = $channel['wx_menu'];
+            $hasJmp = false;
+            foreach($menu as $key => $val){
+                if($key == 0) {
+                    if (!isset($val['url']) || stripos($val['url'], '/index/user/recent') === false) {
+                        $output->info('ChangeMenuSignDaiShu->Source->Menu:' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} 第一个菜单不是继续阅读,自动跳过");
+                        $hasJmp = true;
+                    }
+                }
+                if (!$hasJmp) {
+                    if ((isset($val['url']) && stripos($val['url'], '/index/user/sign') !== false)
+                        || (isset($val['key']) && $val['key'] == '签到')
+                    ) { // 第一级菜单是签到链接 或者 是签到交互 ,自动跳过
+                        $output->info('ChangeMenuSignDaiShu->Source->Menu:' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} 一级菜单有签到,自动跳过");
+                        $hasJmp = true;
+                    }
+                }
+            }
+            if($hasJmp){
+                continue;
+            }
+            foreach($menu as $key => &$val){
+                if($key == 0){
+                    $url = $val['url']; //保存继续阅读的url
+                    $val = [ //修改第一个大菜单的格式
+                        'name' => '继续阅读',
+                        'sub_button' => [
+                            [
+                                'key' => '签到',
+                                'name' => '签到送币',
+                                'type' => 'click'
+                            ],
+                            [
+                                'url' => $url,
+                                'name' => '继续阅读',
+                                'type' => 'view'
+                            ]
+                        ]
+                    ];
+                }else{
+                    if(isset($val['sub_button']) && !empty($val['sub_button'])){
+                        foreach ($val['sub_button'] as $sub_k => &$sub_v) {
+                            if ((isset($sub_v['key']) && $sub_v['key'] == '签到')
+                                || (isset($sub_v['url']) && stripos($sub_v['url'], '/index/user/sign') !== false)
+                            ) { //子菜单中有签到交互或者签到链接,直接删除
+                                unset($val['sub_button'][$sub_k]);
+                            }
+                        }
+                        $val['sub_button'] = array_values($val['sub_button']);
+                    }
+                }
+            }
+            $channel['wx_menu'] = $menu;
+            Log::info('ChangeMenuSignDaiShu->Replace->Menu:'.var_export($channel['wx_menu'],true));
+            try {
+                //更新菜单
+                $admin = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                if(empty($admin['refresh_token'])){
+                    Log::info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} refresh_token Empty!!");
+                    continue;
+                }
+                $adminInfo = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                $wechat = new WeChatObject($adminInfo);
+                $officialAccount = $wechat->getOfficialAccount();
+//                $officialAccount->menu->delete(); //删除全部菜单 主要是掌中云个性化菜单
+                $ret = $officialAccount->menu->create($channel['wx_menu']);
+                if ($ret['errcode'] == 0) {
+                    $output->info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                    Log::info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                    //更新数据
+                    $update = model('AdminConfig')->update(['wx_menu' => $channel['wx_menu']], ["admin_id" => $channel['admin_id']]);
+                    if ($update) {
+                        $output->info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                        Log::info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                    } else {
+                        $output->info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                        Log::error('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                    }
+                } else {
+                    $output->info('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                    Log::error('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                }
+                //休眠
+                if (!empty($sleep) && is_numeric($sleep)) {
+                    sleep($sleep);
+                }
+            }catch (\Exception $exception){
+                Log::error('ChangeMenuSignDaiShu->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:".$exception->getLine().' ' . $exception->getMessage());
+            }
+        }
+        $output->info('ChangeMenuSignDaiShu -----------------------------------> End');
+    }
+}

+ 218 - 0
application/admin/command/ChangeMenuWx.php

@@ -0,0 +1,218 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Elton
+ * Date: 2019/5/21
+ * Time: 11:19
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\WeChatObject;
+use app\common\utility\WeChatMenu;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\input\Option;
+use think\console\Output;
+use think\Log;
+use think\Request;
+use think\response\Json;
+
+class ChangeMenuWx extends Command
+{
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('ChangeMenuWx')
+            ->addArgument('channel_id', Argument::OPTIONAL, '需要更新的渠道OR配号代理商ID,不传值则处理所有')
+            ->addArgument('nums', Argument::OPTIONAL, 'Limit条数限制')
+            ->setDescription('更新微信菜单');
+    }
+
+    /**
+     * 执行命令:
+     * php think ChangeMenuWx          # 更新所有微信菜单
+     * php think ChangeMenuWx 48 1     # 仅更新渠道商ID为48的微信菜单
+     * php think ChangeMenuWx 48 100   # 更新渠道商ID 大于或等于48的微信菜单,更新100条
+     *
+     * @param Input $input
+     * @param Output $output
+     * @return int|null|void
+     */
+    protected function execute(Input $input, Output $output)
+    {
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+
+        $channel_map = null;
+        $channel_map['wx_menu'] = ['neq', ''];
+        $output->writeln("开始执行刷新菜单:" . date('Y-m-d H:i:s', time()));
+
+        $channel_id = $input->getArgument('channel_id');
+        if(empty($channel_id) || !is_numeric($channel_id)){
+            $channel_id = '';
+        }
+        if($channel_id){
+            $channel_map['admin_id'] = ['>=',$channel_id];
+        }
+
+        $nums = is_numeric($input->getArgument('nums')) ? $input->getArgument('nums') : 0;
+
+        $output->writeln("参数channel_id:".$channel_id);
+        $output->writeln("参数nums:".$nums);
+
+        //0.获取默认配置的菜单
+        $default_menu_obj = model('config')->where(['name'=>'wx_menu'])->field('value')->find();
+        $default_menu_arr = json_decode($default_menu_obj->value, true);
+
+        if($nums){
+            $adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,refresh_token,wx_menu')->limit($nums)->select();
+        } else{
+            $adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,refresh_token,wx_menu')->select();
+        }
+
+        //1.查找到所有配置了微信菜单的渠道商OR配号代理商
+        if(!$adminConfig){
+            $output->error('ChangeMenuWx->Error:没有找到可用数据');
+            return;
+        }
+        $output->info('ChangeMenuWx->SQL:'.model('AdminConfig')->getLastSql());
+        Log::info('ChangeMenuWx->SQL:'.model('AdminConfig')->getLastSql());
+
+        try{
+            foreach ($adminConfig as $channel)
+            {
+                //2.检查是否是默认配置的微信菜单,只有是默认的微信菜单才做修改,渠道商自己维护过的菜单不做修改
+                //$diff_arr = array_diff_key($default_menu_arr, $channel['wx_menu']);
+                $diff_value = $this->isConsistent($default_menu_arr, $channel['wx_menu']);
+
+                if($diff_value){
+                    // 如果当前渠道配置的默认菜单一致,则修改
+
+                    //3.将新的菜单结构推送给微信
+                    $nMenu = $this->rePackageMenu($channel['wx_menu']);
+
+                    $admin = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                    if(empty($admin['refresh_token'])){
+                        Log::info('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} refresh_token Empty!!");
+                        continue;
+                    }
+                    $wechat = new WeChatObject($admin);
+                    $officialAccount = $wechat->getOfficialAccount();
+                    try {
+                        $ret = $officialAccount->menu->create($nMenu);
+                    } catch (\Exception $exception) {
+                        Log::info('ChangeMenuWx->WeChat->Menu:' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail");
+                        Log::info('Because:' . $exception->getMessage());
+                    }
+
+                    Log::info('ChangeMenuWx->WeChat->Menu: ret: '.print_r(json_encode($ret), true));
+
+                    if ($ret['errcode'] == 0) {
+                        $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                        Log::info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                        //4.维护admin_config表中的wx_menu字段
+                        $update = model('AdminConfig')->update(['wx_menu' => $nMenu], ["admin_id" => $channel['admin_id']]);
+                        if ($update) {
+                            $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                            Log::info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                        } else {
+                            $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                            Log::error('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                        }
+                    } else {
+                        $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                        Log::error('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                    }
+
+                    // 暂停50毫秒
+                    usleep(50000);
+                }
+            }
+        }catch (\Exception $exception){
+            $output->writeln('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+            Log::error('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+        }
+
+        $output->writeln("刷新菜单完成:" . date('Y-m-d H:i:s', time()));
+    }
+
+
+    /**
+     * 重新组装微信菜单数据
+     * @param array $oldMenu
+     * @return Json
+     */
+    public function rePackageMenu(array $oldMenu)
+    {
+        $newMenu = [];
+        $tmpMenu = [];
+
+        foreach ($oldMenu as $item)
+        {
+            if(!isset($item['sub_button'])){
+                array_push($tmpMenu, $item);
+            }else{
+                foreach ($item['sub_button'] as $subitem){
+                    array_push($tmpMenu, $subitem);
+                }
+            }
+        }
+
+        foreach ($tmpMenu as $menu){
+            if($menu['name'] == '继续阅读' || $menu['name'] == '阅读记录'){
+                $newMenu[0] = $menu;
+            }elseif($menu['name'] == '签到送币' || $menu['name'] == '每日签到'){
+                $newMenu[2] = $menu;
+            }else{
+                $newMenu[1]['name'] = '进入书城';
+                $newMenu[1]['sub_button'][] = $menu;
+            }
+        }
+        ksort($newMenu);
+        return $newMenu;
+    }
+
+
+    /**
+     * 判断菜单元素是否一致
+     * @param array $defalt_menu_arr    默认配置的菜单
+     * @param array $b_arr              渠道商实际菜单
+     * @return bool
+     */
+    public function isConsistent(array $defalt_menu_arr, array $b_arr)
+    {
+
+        $defalt_name_arr = array_column($defalt_menu_arr, 'name');
+        $b_name_arr = array_column($b_arr, 'name');
+        $diff = array_diff_assoc($defalt_name_arr, $b_name_arr);
+        if(!empty($diff)){
+            return false;
+        }else{
+            foreach ($defalt_menu_arr as $key => $default_item)
+            {
+                if(isset($default_item['sub_button'])){
+                    $default_sub_btn_arr = array_column($default_item['sub_button'], 'name');
+                    $b_sub_btn_arr = isset($b_arr[$key]['sub_button']) ? array_column($b_arr[$key]['sub_button'], 'name') : array();
+
+                    $diff_sub_btn = array_diff_assoc($default_sub_btn_arr, $b_sub_btn_arr);
+
+                    if($diff_sub_btn || count($default_sub_btn_arr) != count($b_sub_btn_arr)){
+                        return false;
+                    }
+                }else{
+                    if(isset($b_arr[$key]['sub_button'])){
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
+}

+ 162 - 0
application/admin/command/ChangeMenuZhiChi.php

@@ -0,0 +1,162 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Elton
+ * Date: 2019/7/31
+ * Time: 11:19
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\WeChatObject;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\Log;
+use think\Request;
+use think\response\Json;
+
+class ChangeMenuZhiChi extends Command
+{
+    /**
+     * 配置指令
+     */
+    protected function configure()
+    {
+        $this->setName('ChangeMenuZhiChi')
+            ->addArgument('channel_id', Argument::OPTIONAL, '需要更新的渠道OR配号代理商ID,不传值则处理所有')
+            ->addArgument('nums', Argument::OPTIONAL, 'Limit条数限制')
+            ->setDescription('更新微信菜单(智齿客服)');
+    }
+
+    /**
+     * 执行命令:
+     * php think ChangeMenuZhiChi          # 更新所有微信菜单
+     * php think ChangeMenuZhiChi 48 1     # 仅更新渠道商ID为48的微信菜单
+     * php think ChangeMenuZhiChi 48 100   # 更新渠道商ID 大于或等于48的微信菜单,更新100条
+     *
+     * @param Input $input
+     * @param Output $output
+     * @return int|null|void
+     */
+    protected function execute(Input $input, Output $output)
+    {
+        //cli模式下无法获取到当前的项目模块,手动指定一下
+        Request::instance()->module('admin');
+
+        $channel_map = null;
+        $channel_map['wx_menu'] = ['neq', ''];
+        $output->writeln("开始执行刷新菜单:" . date('Y-m-d H:i:s', time()));
+
+        $channel_id = $input->getArgument('channel_id');
+        if(empty($channel_id) || !is_numeric($channel_id)){
+            $channel_id = '';
+        }
+        if($channel_id){
+            $channel_map['admin_id'] = ['>=',$channel_id];
+        }
+
+        $nums = is_numeric($input->getArgument('nums')) ? $input->getArgument('nums') : 0;
+
+        $output->writeln("参数channel_id:".$channel_id);
+        $output->writeln("参数nums:".$nums);
+
+        if($nums){
+            $adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,ophost_id,refresh_token,wx_menu')->limit($nums)->select();
+        } else{
+            $adminConfig = model('AdminConfig')->where($channel_map)->field('admin_id,appid,ophost_id,refresh_token,wx_menu')->select();
+        }
+
+        //1.查找到所有配置了微信菜单的渠道商OR配号代理商
+        if(!$adminConfig){
+            $output->error('ChangeMenuWx->Error:没有找到可用数据');
+            return;
+        }
+        $output->info('ChangeMenuWx->SQL:'.model('AdminConfig')->getLastSql());
+        Log::info('ChangeMenuWx->SQL:'.model('AdminConfig')->getLastSql());
+
+
+        try{
+            foreach ($adminConfig as $channel)
+            {
+                // 获取公众号配置信息
+                $ophost = model('Ophost')->getInfoById($channel['ophost_id']);
+                $url_pre = $channel['appid'].'.'.$ophost['host'];
+
+                //将新的菜单结构推送给微信
+                $nMenu = $this->rePackageMenu($url_pre, $channel['wx_menu']);
+
+                $admin = model('AdminConfig')->getAdminInfoAll($channel['admin_id']);
+                if(empty($admin['refresh_token'])){
+                    Log::info('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} refresh_token Empty!!");
+                    continue;
+                }
+                $wechat = new WeChatObject($admin);
+                $officialAccount = $wechat->getOfficialAccount();
+                try {
+                    $ret = $officialAccount->menu->create($nMenu);
+                } catch (\Exception $exception) {
+                    Log::error('ChangeMenuWx->WeChat->Menu:' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail");
+                    Log::error('Because:' . $exception->getMessage());
+                }
+                if(isset($ret)){
+                    Log::info('ChangeMenuWx->WeChat->Menu: ret: '.print_r(json_encode($ret), true));
+                    if ($ret['errcode'] == 0) {
+                        $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                        Log::info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Success");
+                        //4.维护admin_config表中的wx_menu字段
+                        $update = model('AdminConfig')->update(['wx_menu' => $nMenu], ["admin_id" => $channel['admin_id']]);
+                        if ($update) {
+                            $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                            Log::info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Success");
+                        } else {
+                            $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                            Log::error('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} Mysql Update Fail");
+                        }
+                    } else {
+                        $output->info('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                        Log::error('ChangeMenuWx->WeChat->Menu: ' . date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']} WeChat Update Fail Error:" . $ret['errmsg']);
+                    }
+                }
+                // 暂停50毫秒
+                usleep(50000);
+            }
+        }catch (\Exception $exception){
+            $output->writeln('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+            Log::error('ChangeMenuWx->WeChat->Menu: '.date('Y-m-d H:i:s') . " admin_id:{$channel['admin_id']}  WeChat Update Fail Error:" . $exception->getMessage());
+        }
+
+        $output->writeln("刷新菜单完成:" . date('Y-m-d H:i:s', time()));
+    }
+
+
+    /**
+     * 重新组装微信菜单数据
+     * @param $url_pre
+     * @param array $oldMenu
+     * @return array
+     */
+    public function rePackageMenu($url_pre, array $oldMenu)
+    {
+        $tmpMenu = [];
+        foreach ($oldMenu as $item)
+        {
+            if(!isset($item['sub_button'])){
+                array_push($tmpMenu, $item);
+            }else{
+                foreach ($item['sub_button'] as $key => $subitem) {
+                    if($subitem['name'] == '联系客服'){
+                        unset($subitem['key']);
+                        $subitem['url'] = 'http://'.$url_pre.'/index/user/visitzhichi';
+                        $subitem['type'] = 'view';
+                        $item['sub_button'][$key] = $subitem;
+                    }
+                }
+                array_push($tmpMenu, $item);
+            }
+        }
+        return $tmpMenu;
+    }
+}

+ 225 - 0
application/admin/command/ChangeQRCode.php

@@ -0,0 +1,225 @@
+<?php
+
+
+namespace app\admin\command;
+
+use app\common\library\Redis;
+use app\common\library\WeChatObject;
+use Symfony\Component\Cache\Simple\RedisCache;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use EasyWeChat\Factory;
+use think\Log;
+use think\Request;
+
+class ChangeQRCode extends Command{
+
+    /**
+     * @var array $imgSize 图片规格
+     */
+    private $imgSize = [
+        1 => [
+            'size' => 143, //二维码宽度
+            'x' => 376, //水印x
+            'y' => 35 //水印y
+        ],
+        2 => [
+            'size' => 147, //二维码宽度
+            'x' => 374, //水印x
+            'y' => 33 //水印y
+        ],
+        3 => [
+            'size' => 135, //二维码宽度
+            'x' => 372, //水印x
+            'y' => 46 //水印y
+        ],
+        4 => [
+            'size' => 179, //二维码宽度
+            'x' => 356, //水印x
+            'y' => 15 //水印y
+        ],
+        5 => [
+            'size' => 179, //二维码宽度
+            'x' => 356, //水印x
+            'y' => 15 //水印y
+        ],
+        6 => [
+            'size' => 179, //二维码宽度
+            'x' => 356, //水印x
+            'y' => 15 //水印y
+        ]
+    ];
+
+    protected function configure()
+    {
+        $this
+            ->setName('changeQRCode')
+            ->addOption('type', 't', Option::VALUE_OPTIONAL, '处理自定义二维码错误问题', 'modify')
+            ->setDescription('处理自定义二维码错误问题');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+
+        $type = $input->getOption('type');
+        switch ($type) {
+            case 'modify':
+                $this->modify($input, $output);
+                break;
+            default:
+                $output->writeln("Type: {$type}  无法识别的类型");
+        }
+    }
+
+    /**
+     * @param Input $input
+     * @param Output $output
+     */
+    private function modify(Input $input, Output $output){
+        $output->info("开始处理自定义二维码错误问题.");
+        try{
+            if(!$source = model('CustomQrcode')->where(['type'=>'2'])->select()){
+                $output->info('没有要处理的自定义二维码错误数据');
+            }
+            foreach($source as $key => $val){
+                //处理合成图片
+                if(strpos($val['url'], 'mp.weixin.qq.com') === false){
+                    $tmp = explode('_',basename($val['url'],".png"));
+                    $template_id = $tmp[2];
+                    if(!$adminConfig = model('AdminConfig')->where(['admin_id'=>$val['admin_id']])->find()){
+                        $output->error(sprintf('未处理 Channel_id:%d CustomQRCode_id:%d 渠道配置信息获取失败',$val['admin_id'],$val['id']));
+                        continue;
+                    }
+                    if(empty($adminConfig['appid']) || empty($adminConfig['json']) ){
+                        $output->error(sprintf('未处理 Channel_id:%d CustomQRCode_id:%d 渠道微信没有授权',$val['admin_id'],$val['id']));
+                        continue;
+                    }
+                    //获取微信永久二维码
+                    $adminConfig = model('AdminConfig')->getAdminInfoAll($val['admin_id']);
+                    $wechat = new WeChatObject($adminConfig);
+                    $officialAccount = $wechat->getOfficialAccount();
+                    $result = $officialAccount->qrcode->forever($val['index']);
+                    if(empty($result) || isset($result['errcode'])){
+                        $output->error(sprintf('未处理 Channel_id:%d CustomQRCode_id:%d 获取微信二维码失败',$val['admin_id'],$val['id']));
+                        continue;
+                    }
+                    $wx_url = $officialAccount->qrcode->url($result['ticket']);
+                    $image_1 = imagecreatefrompng(ROOT_PATH . "public/assets/img/essay/qrcode_{$template_id}.png"); //底图
+                    $image_2 = $this->createThumbnail($wx_url,$this->imgSize[$template_id]['size'],$this->imgSize[$template_id]['size']);
+                    imagecopymerge($image_1, $image_2, $this->imgSize[$template_id]['x'], $this->imgSize[$template_id]['y'], 0, 0, imagesx($image_2),
+                        imagesy($image_2), 100);
+                    $imgPath = ROOT_PATH . 'public/uploads/qrcode/'; //图片保存路径
+                    if (!file_exists($imgPath)) { //创建文件夹
+                        mkdir($imgPath, 0700, true);
+                        clearstatcache();
+                    }
+                    $imgPath = $imgPath .$val['book_id'].'_'.$val['chapter_id'] .'_' . $template_id .'_'.$val['admin_id'].'_'.time().'.png';
+                    $data['url'] = cdnurl("/uploads/qrcode/" . $val['book_id'] .'_'.$val['chapter_id'] . '_' . $template_id .'_'.$val['admin_id'].'_'.time().'.png');
+                    if(!imagepng($image_1, $imgPath)){
+                        $output->error(sprintf('未处理 Channel_id:%d CustomQRCode_id:%d 拼接图片失败',$val['admin_id'],$val['id']));
+                        continue;
+                    }
+                    imagedestroy($image_1);
+                    imagedestroy($image_2);
+                    if(false !== model('CustomQrcode')->where(['id'=>$val['id']])->update($data)){
+                        $output->info(sprintf('已处理 Channel_id:%d CustomQRCode_id:%d ImagePath:%s',$val['admin_id'],$val['id'],$imgPath));
+                    }else{
+                        $output->info(sprintf('未处理 Channel_id:%d CustomQRCode_id:%d 更新数据失败 ImagePath:%s',$val['admin_id'],$val['id'],$imgPath));
+                    }
+                }
+            }
+        }catch (\Exception $e){
+            $output->error('处理自定义二维码出错,Error:'.$e->getMessage());
+        }
+
+    }
+
+    /**
+     * 生成保持原图纵横比的缩略图,支持.png .jpg .gif
+     * 缩略图类型统一为.png格式
+     * $srcFile     原图像文件名称
+     * $toW         缩略图宽
+     * $toH         缩略图高
+     * @return bool
+     */
+    private function createThumbnail($srcFile, $toW, $toH)
+    {
+        $toW += 20;
+        $toH += 20;
+        $info = "";
+        //返回含有4个单元的数组,0-宽,1-高,2-图像类型,3-宽高的文本描述。
+        //失败返回false并产生警告。
+        $data = getimagesize($srcFile, $info);
+        if (!$data)
+            return false;
+
+        //将文件载入到资源变量im中
+        switch ($data[2]) //1-GIF,2-JPG,3-PNG
+        {
+            case 1:
+                if(!function_exists("imagecreatefromgif"))
+                {
+                    echo "the GD can't support .gif, please use .jpeg or .png! <a href='javascript:history.back();'>back</a>";
+                    exit();
+                }
+                $im = imagecreatefromgif($srcFile);
+                break;
+
+            case 2:
+                if(!function_exists("imagecreatefromjpeg"))
+                {
+                    echo "the GD can't support .jpeg, please use other picture! <a href='javascript:history.back();'>back</a>";
+                    exit();
+                }
+                $im = imagecreatefromjpeg($srcFile);
+                break;
+
+            case 3:
+                $im = imagecreatefrompng($srcFile);
+                break;
+        }
+
+        //计算缩略图的宽高
+        $srcW = imagesx($im);
+        $srcH = imagesy($im);
+        $toWH = $toW / $toH;
+        $srcWH = $srcW / $srcH;
+        if ($toWH <= $srcWH)
+        {
+            $ftoW = $toW;
+            $ftoH = (int)($ftoW * ($srcH / $srcW));
+        }
+        else
+        {
+            $ftoH = $toH;
+            $ftoW = (int)($ftoH * ($srcW / $srcH));
+        }
+
+        if (function_exists("imagecreatetruecolor"))
+        {
+            $ni = imagecreatetruecolor($ftoW, $ftoH); //新建一个真彩色图像
+            if ($ni)
+            {
+                //重采样拷贝部分图像并调整大小 可保持较好的清晰度
+                imagecopyresampled($ni, $im, 0, 0, 0, 0, $ftoW, $ftoH, $srcW, $srcH);
+            }
+            else
+            {
+                //拷贝部分图像并调整大小
+                $ni = imagecreate($ftoW, $ftoH);
+                imagecopyresized($ni, $im, 0, 0, 0, 0, $ftoW, $ftoH, $srcW, $srcH);
+            }
+        }
+        else
+        {
+            $ni = imagecreate($ftoW, $ftoH);
+            imagecopyresized($ni, $im, 0, 0, 0, 0, $ftoW, $ftoH, $srcW, $srcH);
+        }
+        ImageDestroy($im);
+        return $ni;
+    }
+}

+ 98 - 0
application/admin/command/CheckChannelRefreshToken.php

@@ -0,0 +1,98 @@
+<?php
+/**
+ * Created by: PhpStorm
+ * User: lytian
+ * Date: 2019/11/28
+ * Time: 13:59
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\common\library\WeChatObject;
+use app\main\helper\ArrayHelper;
+use EasyWeChat\Kernel\Exceptions\HttpException;
+use EasyWeChat\Kernel\Exceptions\RuntimeException;
+use think\console\Command;
+use think\console\Input;
+use think\console\Output;
+use think\Log;
+use think\Request;
+
+class CheckChannelRefreshToken extends BaseCommand
+{
+    protected function configure()
+    {
+        $this->setName('CheckChannelRefreshToken')
+            ->setDescription('检测渠道的refresh_token');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin');
+        $platfromList = model("Platform")->getPlatformList();
+
+        foreach ($platfromList as $platform)
+        {
+            try {
+                $openPlatform = (new WeChatObject(null))->getPlatform($platform['id']);
+
+                $do = true;
+                $i = 0;
+                $limit = 20;
+                while ($do) {
+                    $offset = $limit * $i;
+                    $result = $openPlatform->getAuthorizers($offset, $limit);
+                    if (isset($result['list']) && $result['list']) {
+                        echo count($result['list']).PHP_EOL;
+                        $rows = ArrayHelper::index($result['list'], 'authorizer_appid');
+                        $appids = array_keys($rows);
+                        $appStr = implode(',', $appids);
+                        $adminInfos = model("AdminConfig")->field("admin_id, appid")->where('appid', 'in', $appStr)->select();
+
+                        if ($adminInfos) {
+                            foreach ($adminInfos as $adminInfo) {
+                                $maps = [
+                                    'platform_id' => $platform['id'],
+                                    'admin_id' => $adminInfo['admin_id'],
+                                ];
+                                $ptokenRow = model("Ptoken")->where($maps)->find();
+                                if (empty($ptokenRow)) {
+                                    continue;
+                                }
+
+                                if ($ptokenRow['refresh_token'] != $rows[$adminInfo['appid']]['refresh_token']) {
+                                    echo "需要刷新记录:admin_id: {$adminInfo['admin_id']} platform_id:{$adminInfo['admin_id']} 记录id:{$ptokenRow['id']}".PHP_EOL;
+
+                                    if (model("Ptoken")->update(['refresh_token' => $rows[$adminInfo['appid']]['refresh_token'], 'updatetime' => time()], ['id' => $ptokenRow['id']])) {
+                                        Redis::instance()->del('ANI:'.$adminInfo['admin_id']);
+                                        Log::info("refersh_token更新成功 platform_id:{$platform['id']} admin_id:{$adminInfo['admin_id']}");
+                                    } else {
+                                        Log::info("refersh_token更新失败 platform_id:{$platform['id']} admin_id:{$adminInfo['admin_id']}");
+                                    }
+                                }
+                                unset($ptokenRow);
+                            }
+                        }
+                        unset($adminInfos);
+                        unset($rows);
+                    } else {
+                        $do = false;
+                    }
+
+                    unset($result);
+                    $i++;
+                }
+
+                unset($openPlatform);
+            } catch (HttpException $e) {
+                Log::error("三方平台拉取授权公众号列表异常:id:{$platform['id']} msg:".$e->getMessage());
+                continue;
+            } catch (RuntimeException $e) {
+                Log::error("三方平台拉取授权公众号列表异常:id:{$platform['id']} msg:".$e->getMessage());
+                continue;
+            }
+        }
+    }
+}

+ 161 - 0
application/admin/command/CheckPlatform.php

@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * 检测refresh_token是否可用
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\common\library\WeChatObject;
+use app\common\model\Platform;
+use app\common\model\Ptoken;
+use EasyWeChat\Factory;
+use Symfony\Component\Cache\Simple\RedisCache;
+use think\Config;
+use think\console\Command;
+use think\console\input\Option;
+use think\console\Input;
+use think\console\Output;
+use app\common\model\AdminConfig;
+use app\common\model\Config as dbconfig;
+use think\Log;
+use think\Request;
+
+
+class CheckPlatform extends Command
+{
+    protected $message = '';
+
+    protected function configure()
+    {
+        $this->setName('CheckPlatform')
+            ->addOption("platform","p",Option::VALUE_REQUIRED,'三方平台id')
+            ->addOption("adminid","a",Option::VALUE_REQUIRED,'要单独检测的adminid 支持英文逗号分隔的多个')
+            ->addOption("delay","d",Option::VALUE_REQUIRED)
+            ->setDescription('Here is the remark ');
+    }
+
+    //获取公众号
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+        $pid = $input->getOption('platform');
+        if ($pid === null) {
+            $output->writeln("输入参数有误");
+            throw new \Exception('Please input correct platform type');
+        }
+        $pid = intval($pid);
+        $admin_id = $input->getOption('adminid');
+
+        $output->writeln("检测refresh_token---开始");
+        Log::info("检测refresh_token---开始");
+        $delay = $input->getOption('delay');
+
+        $admin_config = new AdminConfig();
+        if(!empty($admin_id)){
+            $adminlist = $admin_config
+                ->join('admin', 'admin.id=admin_config.admin_id')
+                ->join('ptoken', 'admin.id=ptoken.admin_id and ptoken.platform_id=' . $pid)
+                ->field('admin_config.*,admin.id,admin.username,admin.nickname,ptoken.platform_id as platid,ptoken.refresh_token as rtoken')
+                ->where(['admin.status' => 'normal'])
+                ->where('admin.id', 'in', $admin_id)
+                ->select();
+        }else {
+            $adminlist = $admin_config
+                ->join('admin', 'admin.id=admin_config.admin_id')
+                ->join('ptoken', 'admin.id=ptoken.admin_id and ptoken.platform_id=' . $pid)
+                ->field('admin_config.*,admin.id,admin.username,admin.nickname,ptoken.platform_id as platid,ptoken.refresh_token as rtoken')
+                ->where(['admin.status' => 'normal'])
+                ->select();
+        }
+        $count = 0; //服务号总数
+        $new = 0; //服务号新增异常数量
+        foreach ($adminlist as &$value) {
+            if(empty($value['wx_menu']) || empty($value['appid']) || empty($value['rtoken'])){
+                Log::info("admin_id=".$value['admin_id']."的用户未授权公众平台,跳过");
+                continue;
+            }
+            $count++;
+            $output->writeln("检测refresh_token---渠道ID:{$value['id']} 账号:{$value['username']} 昵称:{$value['nickname']} APPID:{$value['appid']}");
+            Log::info("检测refresh_token---渠道ID:{$value['id']} 账号:{$value['username']} 昵称:{$value['nickname']} APPID:{$value['appid']}");
+            $output->writeln("检测refresh_token---服务号授权状态:ok");
+            Log::info("检测refresh_token---服务号授权状态:ok");
+            $menulist = $this->emptyWechatMenu($value);
+            if ($menulist) {
+                if ($menulist === 1) {
+                    $errMsg = ' 错误日志:' . $this->message;
+                } else {
+                    $errMsg = '';
+                }
+                $output->writeln("检测refresh_token---服务号菜单状态:异常" . $errMsg);
+                Log::info("检测refresh_token---服务号菜单状态:异常" . $errMsg);
+                $new++;
+                $output->writeln("检测refresh_token---发送微信报警通知");
+                Log::info("检测refresh_token---发送微信报警通知");
+                $msg = "检测refresh_token--发现异常!" . date('Y-m-d H:i:s') . "\n公众号:" . ($value['json']['authorizer_info']['nick_name']??'') . "\n原始ID:" . ($value['json']['authorizer_info']['user_name']??'') . "\n渠道ID:" . $value['id'] . "\n账号:" . $value['username'] . "\n昵称:" . $value['nickname'];
+                if ($menulist === 1) {
+                    $msg .= "\n错误日志:" . $this->message;
+                }
+                $this->SendWorkChatMessage($msg);
+                if(empty($delay)){
+                    $delay = 1;
+                }
+                sleep($delay); //检测一个延时1秒
+            }else{
+                $output->writeln("检测refresh_token---服务号菜单状态:ok");
+                Log::info("检测refresh_token---服务号菜单状态:ok");
+            }
+        }
+        $msg = date('Y-m-d H:i:s') . " refresh_token异常数量:{$new} 服务号总数:{$count}";
+        $this->SendWorkChatMessage($msg);
+        $output->writeln("检测refresh_token---完毕!日志->" . $msg);
+        Log::info("检测refresh_token---完毕!日志->" . $msg);
+    }
+
+    //获取菜单
+    public function emptyWechatMenu($config)
+    {
+
+        if (empty($config)) {
+            return false;
+        }
+        try {
+            $adminInfo = model('AdminConfig')->getAdminInfoAll($config['admin_id']);
+            $wechat = new WeChatObject($adminInfo);
+            $officialAccount = $wechat->getOfficialAccountByPlatform($config['platid'],$config['appid'],$config['rtoken']);
+            $menu = $officialAccount->menu->list();//Log::write(1,'1111');
+            if (empty($menu)) {
+                return true;
+            }
+        } catch (\Exception $exception) {
+            Log::error('检测refresh_token---微信检查脚本触发异常!config:' . json_encode($config));
+            $this->message = $exception->getMessage();
+            return 1;
+        }
+        return false;
+
+    }
+
+    //发企业微信
+    public function SendWorkChatMessage($content)
+    {
+        if (empty($content)) {
+            return false;
+        }
+        $wechat = Config::get('wechat');
+        $wechat['http']['base_uri'] = $wechat['work']['base_uri'];
+        $wechat['http']['timeout'] = 20;
+        $wechat['corp_id'] = $wechat['work']['corp_id'];
+        $wechat['secret'] = $wechat['work']['secret'];
+        $app = Factory::work($wechat);
+        $app['cache'] = new RedisCache(Redis::instanceCache());
+        $res = $app->message
+            ->message($content)
+            ->ofAgent($wechat['work']['agent_id'])
+            ->toParty($wechat['work']['party_id'])
+            ->send();
+        return $res;
+    }
+}

+ 241 - 0
application/admin/command/CheckWechatForbidden.php

@@ -0,0 +1,241 @@
+<?php
+
+/*
+ * 检测公众号是否被封
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\common\model\Admin;
+use app\common\library\WeChatObject;
+use EasyWeChat\Factory;
+use Symfony\Component\Cache\Simple\RedisCache;
+use think\Config;
+use think\console\Command;
+use think\console\input\Option;
+use think\console\Input;
+use think\console\Output;
+use app\common\model\AdminConfig;
+use app\common\model\Config as dbconfig;
+use think\Log;
+use think\Request;
+
+
+class CheckWechatForbidden extends Command
+{
+    protected $message = '';
+
+    protected function configure()
+    {
+        $this->setName('CheckWechatForbidden')
+            ->addOption("delay","d",Option::VALUE_REQUIRED)
+            ->setDescription('Here is the remark ');
+    }
+
+    //获取公众号
+    protected function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin'); //cli模式下无法获取到当前的项目模块,手动指定一下
+
+        $output->writeln("检测微信服务号状态---开始");
+        Log::info("检测微信服务号状态---开始");
+        $delay = $input->getOption('delay');
+
+        $admin_config = new AdminConfig();
+        $adminlist = $admin_config
+            ->join('admin', 'admin.id=admin_config.admin_id','LEFT')
+            ->join('admin_extend','admin_extend.admin_id=admin_config.admin_id','LEFT')
+            ->join('auth_group_access ac','ac.uid=admin_config.admin_id','LEFT')
+            ->field('admin_config.*,admin.id,admin.username,admin.nickname,ac.group_id,admin_extend.create_by')
+            ->where('admin.status','=','normal')
+            ->where('admin_config.is_fouce','=','1')
+            ->select();
+        $redis = Redis::instance();
+        $count = 0; //服务号总数
+        $jmp = 0; //服务号跳过检测数量(半小时内有交互)
+        $err = 0; //服务号历史异常数量
+        $new = 0; //服务号新增异常数量
+        $re = 0; //服务号恢复数量
+        $configModel = $configdb = new dbconfig();
+        $siteconfig = $configModel->getConfigSiteArr();
+        $theme = $siteconfig['theme'];
+        switch ($theme) {
+            case 'qy':
+                $themeName = '袋鼠';
+                break;
+            case 'sf':
+                $themeName = '沙发';
+                break;
+            case 'ms':
+                $themeName = '美书';
+                break;
+            case 'px':
+                $themeName = '点看';
+                break;
+            case 'xd':
+                $themeName = '熊大';
+                break;
+            case 'yg':
+                $themeName = '阳光';
+                break;
+            case '':
+                $themeName = '西瓜';
+                break;
+            default:
+                $themeName = $theme;
+        }
+
+        //$output->writeln("当前平台是:".$siteconfig['theme']);
+        foreach ($adminlist as &$value) {
+            if(empty($value['wx_menu']) || empty($value['appid'])){
+                Log::info("admin_id=".$value['admin_id']."的用户未授权公众平台,跳过");
+                continue;
+            }
+            $adminInfo = $admin_config->getAdminInfoAll($value['admin_id']);
+            if(empty($adminInfo['refresh_token'])){  //判断refresh_tokan是否为空,为空跳过
+                Log::info("admin_id=".$value['admin_id']."的用户refresh_token为空,跳过");
+                continue;
+            }
+            $count++;
+            $value['refresh_token'] = $adminInfo['refresh_token'];
+            $output->writeln("检测微信服务号状态---渠道ID:{$value['id']} 账号:{$value['username']} 昵称:{$value['nickname']} APPID:{$value['appid']}");
+            Log::info("检测微信服务号状态---渠道ID:{$value['id']} 账号:{$value['username']} 昵称:{$value['nickname']} APPID:{$value['appid']}");
+            if (!empty($value['appid']) && !empty($value['refresh_token'])) {
+                $output->writeln("检测微信服务号状态---服务号授权状态:ok");
+                Log::info("检测微信服务号状态---服务号授权状态:ok");
+                $operate_time = $redis->hget('WEOP',$value['admin_id']);
+                if (!$operate_time || (time() - $operate_time) > 1800) { //无交互时间 或者 交互时间大约半小时
+                    if ($operate_time) {
+                        $redis->hdel('WEOP', $value['admin_id']);
+                    }
+                    $key2 = 'F:' . $value['admin_id'];
+                    $menulist = $this->emptyWechatMenu($value);
+                    if ($menulist) {
+                        if ($menulist === 1) {
+                            $errMsg = ' 错误日志:' . $this->message;
+                        } else {
+                            $errMsg = '';
+                        }
+                        $output->writeln("检测微信服务号状态---服务号菜单状态:异常" . $errMsg);
+                        Log::info("检测微信服务号状态---服务号菜单状态:异常" . $errMsg);
+                        if ($redis->exists($key2)) {
+                            $err++;
+                            $output->writeln("检测微信服务号状态---报警通知:30天内已发送,跳过");
+                            Log::info("检测微信服务号状态---报警通知:30天内已发送,跳过");
+                            continue;
+                        }
+                        $new++;
+                        $output->writeln("检测微信服务号状态---发送微信报警通知");
+                        Log::info("检测微信服务号状态---发送微信报警通知");
+
+                        $khr = "\n";
+                        if($value['group_id'] == 4){
+                            $khr.="开户渠道商ID:".$value['create_by'];
+                            $admin = new Admin();
+                            $adminInfos = $admin->join('admin_extend ae','admin.id=ae.admin_id')->field('admin.*,ae.create_by')->where('admin.id','=',$value['create_by'])->find();
+                            $khr.="\n开户渠道商账号:".$adminInfos->username;
+                            $khr.="\n开户渠道商昵称:".$adminInfos->nickname;
+                            $madminInfos = $admin->where('id','=',$adminInfos->create_by)->find();
+                            $khr.="\n开户管理员ID:".$adminInfos->create_by;
+                            $khr.="\n开户管理员账号:".$madminInfos->username;
+                            $khr.="\n开户管理员昵称:".$madminInfos->nickname;
+                        }elseif($value['group_id'] == 3){
+                            $khr.="开户管理员ID:".$value['create_by'];
+                            $admin = new Admin();
+                            $adminInfos = $admin->where('id','=',$value['create_by'])->find();
+                            $khr.="\n开户管理员账号:".$adminInfos->username;
+                            $khr.="\n开户管理员昵称:".$adminInfos->nickname;
+                        }
+
+                        $msg = "平台:{$themeName} 发现异常!" . date('Y-m-d H:i:s') . "\n公众号:" . ($value['json']['authorizer_info']['nick_name']??'') . "\n原始ID:" . ($value['json']['authorizer_info']['user_name']??'') ."\nAppid:".$value['appid']. "\n后台ID:" . $value['id'] . "\n账号:" . $value['username'] . "\n昵称:" . $value['nickname'].$khr;
+                        if ($menulist === 1) {
+                            if (stripos($this->message, 'Resolving timed')) {
+                                $msg .= "\n错误说明: API超时,商务请忽略";
+                            }
+                            if (stripos($this->message, 'user limited')) {
+                                $msg .= "\n错误说明: 公众号被封或菜单功能被封,请商务通知渠道";
+                            }
+                            if (stripos($this->message, 'component is not authorized')) {
+                                $msg .= "\n错误说明: 公众号授权三方权限有问题,请商务通知渠道";
+                            }
+                            $msg .= "\n错误日志:" . $this->message;
+                        }
+                        $this->SendWorkChatMessage($msg);
+                        $redis->setex($key2, 86400*30, 1);
+                    } else {
+                        $output->writeln("检测微信服务号状态---服务号菜单状态:ok");
+                        Log::info("检测微信服务号状态---服务号菜单状态:ok");
+                        if ($redis->exists($key2)) {
+                            $re++;
+                            $redis->del($key2);
+                        }
+                    }
+                    if(empty($delay)){
+                        $delay = 1;
+                    }
+                    sleep($delay); //检测一个延时1秒
+                } else { // 存在redis hash name,且交互时间小于半小时
+                    $jmp++;
+                    $output->writeln("检测微信服务号状态---半小时内存在交互,跳过检测");
+                    Log::info("检测微信服务号状态---半小时内存在交互,跳过检测");
+                }
+            }else{
+                $output->writeln("检测微信服务号状态---服务号授权状态:异常");
+                Log::info("检测微信服务号状态---服务号授权状态:异常");
+            }
+        }
+        $msg = "平台:{$themeName} ".date('Y-m-d H:i:s') . " 新增异常:{$new} 历史异常:{$err} 恢复正常:{$re} 跳过检测:{$jmp} 服务号总数:{$count}";
+        $this->SendWorkChatMessage($msg);
+        $output->writeln("检测微信服务号状态---完毕!日志->" . $msg);
+        Log::info("检测微信服务号状态---完毕!日志->" . $msg);
+
+    }
+
+    //获取菜单
+    public function emptyWechatMenu($config)
+    {
+
+        if (empty($config)) {
+            return false;
+        }
+        try {
+            $admin_config = new AdminConfig();
+            $info = $admin_config->getAdminInfoAll($config['admin_id']);
+            $wechat = new WeChatObject($info);
+            $officialAccount = $wechat->getOfficialAccount($config['appid'], $config['refresh_token']);
+            $menu = $officialAccount->menu->list();//Log::write(1,'1111');
+            if (empty($menu)) {
+                return true;
+            }
+        } catch (\Exception $exception) {
+            Log::error('检测微信服务号状态---微信检查脚本触发异常!config:' . json_encode($config));
+            $this->message = $exception->getMessage();
+            return 1;
+        }
+        return false;
+
+    }
+
+    //发企业微信
+    public function SendWorkChatMessage($content)
+    {
+        if (empty($content)) {
+            return false;
+        }
+        $wechat = Config::get('wechat');
+        $wechat['http']['base_uri'] = $wechat['work']['base_uri'];
+        $wechat['http']['timeout'] = 20;
+        $wechat['corp_id'] = $wechat['work']['corp_id'];
+        $wechat['secret'] = $wechat['work']['secret'];
+        $app = Factory::work($wechat);
+        $app['cache'] = new RedisCache(Redis::instanceCache());
+        $res = $app->message
+            ->message($content)
+            ->ofAgent($wechat['work']['agent_id'])
+            ->toParty($wechat['work']['party_id'])
+            ->send();
+        return $res;
+    }
+}

+ 51 - 0
application/admin/command/ClearCampaignRedis.php

@@ -0,0 +1,51 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Bear
+ * Date: 2019/1/7
+ * Time: 下午3:49
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Argument;
+use think\console\Output;
+use think\Request;
+
+class ClearCampaignRedis extends Command
+{
+    public function Configure()
+    {
+        $this->setName('ClearCampaignRedis')
+            ->addArgument('pre', Argument::OPTIONAL, 'redis前缀', null)
+            ->addArgument('key', Argument::OPTIONAL, '要清除得key', null)
+            ->setDescription('clear ClearCampaignRedis redis');
+    }
+
+    /**
+     * @param Input  $input
+     * @param Output $output
+     * @return int|null|void
+     * @throws \Exception
+     * @throws \think\Exception
+     */
+    public function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin');
+        $pre = $input->getArgument('pre');
+        $key = $input->getArgument('key');
+        $redisArr = [
+            'MR:','MRLATEST','MC:','UCR:','UMCS:','UMO:','CMK:','UMF:','MCL:','LK:','MCS:','MC_USERLIST','SAB:','SAU:','SAUL:',
+        ];
+        if (!in_array($pre, $redisArr) || strstr($pre,'*') !== false || strstr($key,'*') !== false ){
+            echo '不能清除此key';exit;
+        }
+        $res = Redis::instance()->del($key);
+        dump($res);
+
+    }
+}

+ 132 - 0
application/admin/command/ClearUser.php

@@ -0,0 +1,132 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Bear
+ * Date: 2019/1/7
+ * Time: 下午3:49
+ */
+
+namespace app\admin\command;
+
+
+use app\common\library\Redis;
+use app\main\constants\CacheConstants;
+use app\main\model\object\UserObject;
+use app\main\service\OfficialAccountsService;
+use app\main\service\UserService;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Request;
+
+class ClearUser extends Command
+{
+    public function Configure()
+    {
+        $this
+            ->setName('clearUser')
+            ->addOption('user_id', 'u', Option::VALUE_REQUIRED, 'user_id', null)
+            ->addOption('openid', 'o', Option::VALUE_REQUIRED, 'openid', null)
+            ->addOption('type', 't', Option::VALUE_REQUIRED, '类型', 'user.cache')
+            ->addOption('params', 'p', Option::VALUE_REQUIRED, '书币数量', '0,0')
+            ->setDescription('clear user in db and redis');
+    }
+
+    /**
+     * @param Input  $input
+     * @param Output $output
+     * @return int|null|void
+     * @throws \Exception
+     * @throws \think\Exception
+     */
+    public function execute(Input $input, Output $output)
+    {
+        Request::instance()->module('admin');
+        $user_id = $input->getOption('user_id');
+        $type = $input->getOption('type');
+        $open_id = $input->getOption('openid');
+        $params = explode(',', $input->getOption('params'));
+        switch ($type) {
+            case 'user':
+                $this->clearUser($user_id, $open_id);
+                break;
+            case 'user.cache':
+                $this->clearCache($user_id);
+                break;
+            default:
+                $this->clearCache($user_id);
+        }
+    }
+
+    /**
+     * 清除用户缓存
+     * @param $user_id
+     */
+    public function clearCache($user_id)
+    {
+        $key = 'UN:' . $user_id;
+        echo 'cache key: ' . $key . "\n";
+        if (Redis::instance()->exists($key)) {
+            if (Redis::instance()->delete($key)) {
+                echo "cache cleared";
+            } else {
+                echo "cache clear error";
+            }
+        } else {
+            echo 'cache not exists';
+        }
+        echo "\n";
+    }
+
+    /**
+     * 删除用户信息
+     * @param $user_id
+     * @param $open_id
+     * @throws \think\Exception
+     */
+    public function clearUser($user_id, $open_id)
+    {
+        //可不提供此参数
+        $user = UserService::instance()->getUserModel()->setConnect($user_id)->where('id', '=', $user_id)->find()->toArray();
+
+        $userObj = (new UserObject())->bind($user);
+        if (!$open_id) {
+            $open_id = $userObj->openid;
+        }
+        echo "\nuser_id:" . $user_id;
+        echo "\nopenid:" . $open_id;
+
+        if ($open_id == $user_id) {
+            echo "\nuser_id and open_id is same,exit\n";
+            return;
+        }
+
+        echo "\nchannel_id:" . $userObj->channel_id;
+        echo "\nuser库:" . UserService::instance()->getUserDBIndex($user_id);
+        echo "\nopen库:" . UserService::instance()->getOpenIdDBIndex($userObj->channel_id, $open_id);
+
+
+        echo "\nstart clear...";
+        echo "\nclear user";
+        if (UserService::instance()->getUserModel()->setConnect($user_id)->where('id', '=', $user_id)->find()) {
+            UserService::instance()->getUserModel()->setConnect($user_id)->where('id', '=', $user_id)->setField('openid', $user_id);
+        } else {
+            echo "\nuser not exists\n";
+        }
+        echo "\nclear open";
+        if (OfficialAccountsService::instance()->getOpenidModel()->setConnect($userObj->channel_id, $open_id)->where('user_id', '=', $user_id)->find()) {
+            OfficialAccountsService::instance()->getOpenidModel()->setConnect($userObj->channel_id, $open_id)->where('user_id', '=', $user_id)->setField('channel_openid', $user_id);
+        } else {
+            echo "\nopenid not exists";
+        }
+        $userKey = CacheConstants::getUserCacheKey($user_id);
+        if (Redis::instance()->exists($userKey)) {
+            echo "\nclear cache";
+            Redis::instance()->del($userKey);
+        } else {
+            echo "\ncache not exists";
+        }
+        echo "\nend clear...\n";
+    }
+}

+ 86 - 0
application/admin/command/ClearUserBlack.php

@@ -0,0 +1,86 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: Bear
+ * Date: 2020/6/29
+ * Time: 上午11:27
+ */
+
+namespace app\admin\command;
+
+
+use app\admin\service\LogService;
+use app\common\library\Redis;
+use app\main\constants\AdminConstants;
+use app\main\constants\CacheConstants;
+use app\main\constants\UserConstants;
+use app\main\service\OrderService;
+use app\main\service\UserService;
+use app\source\model\UserUpdate;
+use think\Config;
+use think\console\Input;
+use think\console\Output;
+use think\Request;
+
+class ClearUserBlack extends BaseCommand
+{
+    public function Configure()
+    {
+        $this
+            ->setName('clearUserBlack')
+            ->setDescription('clear black user for channel');
+    }
+
+    public function run(Input $input, Output $output)
+    {
+        Request::instance()->module('admin');
+
+        $channelList = model('admin_config')->where('JSON_EXTRACT(extend,"$.clear") = ' . AdminConstants::ADMIN_EXTEND_CLEAR_WAITING)->column('admin_id');
+
+        if ($channelList) {
+            $mUser = UserService::instance()->getUserModel();
+            $user_num = Config::get('db.user_num');
+            for ($i = 0; $i < $user_num; $i++) {
+                $blackIds = $mUser->setConnect($i + $user_num)
+                    ->whereIn('channel_id', $channelList)
+                    ->where('is_black', '<>', UserConstants::USER_BLACK_NO)
+                    ->column('id');
+                if ($blackIds) {
+                    LogService::info('黑名单用户数:' . count($blackIds) . ':' . implode(',', $blackIds));
+                    foreach ($blackIds as $blackId) {
+                        $userUpdate = new UserUpdate();
+                        $userUpdate->setId($blackId)->setIsBlack(0);
+                        \app\source\service\UserService::instance()->updateUser($userUpdate);
+                    }
+                } else {
+                    LogService::info('黑名单用户不存在,分库:' . $i);
+                }
+            }
+
+            $adminList = model('AdminConfig')->whereIn('admin_id', $channelList)->select();
+            foreach ($adminList as $admin) {
+                if ($admin->extend != 'null') {
+                    $extend = json_decode($admin->extend, true);
+                    $extend['clear'] = AdminConstants::ADMIN_EXTEND_CLEAR_YES;
+                } else {
+                    $extend = ['clear' => AdminConstants::ADMIN_EXTEND_CLEAR_YES];
+                }
+                $extend = json_encode($extend, JSON_UNESCAPED_UNICODE);
+                model('AdminConfig')->update(['extend' => $extend], ['admin_id' => $admin['admin_id']]);
+            }
+        }
+    }
+
+//    public function rollbackUser()
+//    {
+//        $data = file('/data/web/cps_shell/bb.txt');
+//        foreach ($data as $line) {
+//            $ids = explode(',', trim($line));
+//            foreach ($ids as $blackId) {
+//                $userUpdate = new UserUpdate();
+//                $userUpdate->setId($blackId)->setIsBlack(1);
+//                \app\source\service\UserService::instance()->updateUser($userUpdate);
+//            }
+//        }
+//    }
+}

+ 1291 - 0
application/admin/command/Crud.php

@@ -0,0 +1,1291 @@
+<?php
+
+namespace app\admin\command;
+
+use fast\Form;
+use think\Config;
+use think\console\Command;
+use think\console\Input;
+use think\console\input\Option;
+use think\console\Output;
+use think\Db;
+use think\Exception;
+use think\Lang;
+
+class Crud extends Command
+{
+
+    protected $stubList = [];
+
+    /**
+     * Selectpage搜索字段关联
+     */
+    protected $fieldSelectpageMap = [
+        'nickname' => ['user_id', 'user_ids', 'admin_id', 'admin_ids']
+    ];
+
+    /**
+     * Enum类型识别为单选框的结尾字符,默认会识别为单选下拉列表
+     */
+    protected $enumRadioSuffix = ['data', 'state', 'status'];
+
+    /**
+     * Set类型识别为复选框的结尾字符,默认会识别为多选下拉列表
+     */
+    protected $setCheckboxSuffix = ['data', 'state', 'status'];
+
+    /**
+     * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框
+     */
+    protected $intDateSuffix = ['time'];
+
+    /**
+     * 开关后缀
+     */
+    protected $switchSuffix = ['switch'];
+
+    /**
+     * 城市后缀
+     */
+    protected $citySuffix = ['city'];
+
+    /**
+     * Selectpage对应的后缀
+     */
+    protected $selectpageSuffix = ['_id', '_ids'];
+
+    /**
+     * Selectpage多选对应的后缀
+     */
+    protected $selectpagesSuffix = ['_ids'];
+
+    /**
+     * 以指定字符结尾的字段格式化函数
+     */
+    protected $fieldFormatterSuffix = [
+        'status' => ['type' => ['varchar'], 'name' => 'status'],
+        'icon'   => 'icon',
+        'flag'   => 'flag',
+        'url'    => 'url',
+        'image'  => 'image',
+        'images' => 'images',
+        'time'   => ['type' => ['int', 'timestamp'], 'name' => 'datetime']
+    ];
+
+    /**
+     * 识别为图片字段
+     */
+    protected $imageField = ['image', 'images', 'avatar', 'avatars'];
+
+    /**
+     * 识别为文件字段
+     */
+    protected $fileField = ['file', 'files'];
+
+    /**
+     * 保留字段
+     */
+    protected $reservedField = ['admin_id', 'createtime', 'updatetime'];
+
+    /**
+     * 排除字段
+     */
+    protected $ignoreFields = [];
+
+    /**
+     * 排序字段
+     */
+    protected $sortField = 'weigh';
+
+    /**
+     * 编辑器的Class
+     */
+    protected $editorClass = 'editor';
+
+    protected function configure()
+    {
+        $this
+                ->setName('crud')
+                ->addOption('table', 't', Option::VALUE_REQUIRED, 'table name without prefix', null)
+                ->addOption('controller', 'c', Option::VALUE_OPTIONAL, 'controller name', null)
+                ->addOption('model', 'm', Option::VALUE_OPTIONAL, 'model name', null)
+                ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override', null)
+                ->addOption('local', 'l', Option::VALUE_OPTIONAL, 'local model', 1)
+                ->addOption('relation', 'r', Option::VALUE_OPTIONAL, 'relation table name without prefix', null)
+                ->addOption('relationmodel', 'e', Option::VALUE_OPTIONAL, 'relation model name', null)
+                ->addOption('relationforeignkey', 'k', Option::VALUE_OPTIONAL, 'relation foreign key', null)
+                ->addOption('relationprimarykey', 'p', Option::VALUE_OPTIONAL, 'relation primary key', null)
+                ->addOption('mode', 'o', Option::VALUE_OPTIONAL, 'relation table mode,hasone or belongsto', 'belongsto')
+                ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete all files generated by CRUD', null)
+                ->addOption('menu', 'u', Option::VALUE_OPTIONAL, 'create menu when CRUD completed', null)
+                ->addOption('setcheckboxsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate checkbox component with suffix', null)
+                ->addOption('enumradiosuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate radio component with suffix', null)
+                ->addOption('imagefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate image component with suffix', null)
+                ->addOption('filefield', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate file component with suffix', null)
+                ->addOption('intdatesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate date component with suffix', null)
+                ->addOption('switchsuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate switch component with suffix', null)
+                ->addOption('citysuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate citypicker component with suffix', null)
+                ->addOption('selectpagesuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate selectpage component with suffix', null)
+                ->addOption('selectpagessuffix', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'automatically generate multiple selectpage component with suffix', null)
+                ->addOption('ignorefields', null, Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'ignore fields', null)
+                ->addOption('sortfield', null, Option::VALUE_OPTIONAL, 'sort field', null)
+                ->addOption('editorclass', null, Option::VALUE_OPTIONAL, 'automatically generate editor class', null)
+                ->setDescription('Build CRUD controller and model from table');
+    }
+
+    protected function execute(Input $input, Output $output)
+    {
+        $adminPath = dirname(__DIR__) . DS;
+        //表名
+        $table = $input->getOption('table') ?: '';
+        //自定义控制器
+        $controller = $input->getOption('controller');
+        //自定义模型
+        $model = $input->getOption('model');
+        //强制覆盖
+        $force = $input->getOption('force');
+        //是否为本地model,为0时表示为全局model将会把model放在app/common/model中
+        $local = $input->getOption('local');
+        if (!$table)
+        {
+            throw new Exception('table name can\'t empty');
+        }
+        //是否生成菜单
+        $menu = $input->getOption("menu");
+        //关联表
+        $relation = $input->getOption('relation');
+        //自定义关联表模型
+        $relationModel = $input->getOption('relationmodel');
+        //模式
+        $mode = $input->getOption('mode');
+        //外键
+        $relationForeignKey = $input->getOption('relationforeignkey');
+        //主键
+        $relationPrimaryKey = $input->getOption('relationprimarykey');
+        //复选框后缀
+        $setcheckboxsuffix = $input->getOption('setcheckboxsuffix');
+        //单选框后缀
+        $enumradiosuffix = $input->getOption('enumradiosuffix');
+        //图片后缀
+        $imagefield = $input->getOption('imagefield');
+        //文件后缀
+        $filefield = $input->getOption('filefield');
+        //日期后缀
+        $intdatesuffix = $input->getOption('intdatesuffix');
+        //开关后缀
+        $switchsuffix = $input->getOption('switchsuffix');
+        //城市后缀
+        $citysuffix = $input->getOption('citysuffix');
+        //selectpage后缀
+        $selectpagesuffix = $input->getOption('selectpagesuffix');
+        //selectpage多选后缀
+        $selectpagessuffix = $input->getOption('selectpagessuffix');
+        //排除字段
+        $ignoreFields = $input->getOption('ignorefields');
+        //排序字段
+        $sortfield = $input->getOption('sortfield');
+        //编辑器Class
+        $editorclass = $input->getOption('editorclass');
+        if ($setcheckboxsuffix)
+            $this->setCheckboxSuffix = $setcheckboxsuffix;
+        if ($enumradiosuffix)
+            $this->enumRadioSuffix = $enumradiosuffix;
+        if ($imagefield)
+            $this->imageField = $imagefield;
+        if ($filefield)
+            $this->fileField = $filefield;
+        if ($intdatesuffix)
+            $this->intDateSuffix = $intdatesuffix;
+        if ($switchsuffix)
+            $this->switchSuffix = $switchsuffix;
+        if ($citysuffix)
+            $this->citySuffix = $citysuffix;
+        if ($selectpagesuffix)
+            $this->selectpageSuffix = $selectpagesuffix;
+        if ($selectpagessuffix)
+            $this->selectpagesSuffix = $selectpagessuffix;
+        if ($ignoreFields)
+            $this->ignoreFields = $ignoreFields;
+        if ($editorclass)
+            $this->editorClass = $editorclass;
+        if ($sortfield)
+            $this->sortField = $sortfield;
+
+        //如果有启用关联模式
+        if ($relation && !in_array($mode, ['hasone', 'belongsto']))
+        {
+            throw new Exception("relation table only work in hasone or belongsto mode");
+        }
+
+        $dbname = Config::get('database.database');
+        $prefix = Config::get('database.prefix');
+
+        //检查主表
+        $table = stripos($table, $prefix) === 0 ? substr($table, strlen($prefix)) : $table;
+        $modelTableName = $tableName = $table;
+        $modelTableType = 'table';
+        $tableInfo = Db::query("SHOW TABLE STATUS LIKE '{$tableName}'", [], TRUE);
+        if (!$tableInfo)
+        {
+            $tableName = $prefix . $table;
+            $modelTableType = 'name';
+            $tableInfo = Db::query("SHOW TABLE STATUS LIKE '{$tableName}'", [], TRUE);
+            if (!$tableInfo)
+            {
+                throw new Exception("table not found");
+            }
+        }
+        $tableInfo = $tableInfo[0];
+
+        $relationModelTableName = $relationTableName = $relation;
+        $relationModelTableType = 'table';
+        //检查关联表
+        if ($relation)
+        {
+            $relation = stripos($relation, $prefix) === 0 ? substr($relation, strlen($prefix)) : $relation;
+            $relationModelTableName = $relationTableName = $relation;
+            $relationTableInfo = Db::query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], TRUE);
+            if (!$relationTableInfo)
+            {
+                $relationTableName = $prefix . $relation;
+                $relationModelTableType = 'name';
+                $relationTableInfo = Db::query("SHOW TABLE STATUS LIKE '{$relationTableName}'", [], TRUE);
+                if (!$relationTableInfo)
+                {
+                    throw new Exception("relation table not found");
+                }
+            }
+        }
+
+        //根据表名匹配对应的Fontawesome图标
+        $iconPath = ROOT_PATH . str_replace('/', DS, '/public/assets/libs/font-awesome/less/variables.less');
+        $iconName = is_file($iconPath) && stripos(file_get_contents($iconPath), '@fa-var-' . $table . ':') ? 'fa fa-' . $table : 'fa fa-circle-o';
+
+        //控制器默认以表名进行处理,以下划线进行分隔,如果需要自定义则需要传入controller,格式为目录层级
+        $controller = str_replace('_', '', $controller);
+        $controllerArr = !$controller ? explode('_', strtolower($table)) : explode('/', strtolower($controller));
+        $controllerUrl = implode('/', $controllerArr);
+        $controllerName = ucfirst(array_pop($controllerArr));
+        $controllerDir = implode(DS, $controllerArr);
+        $controllerFile = ($controllerDir ? $controllerDir . DS : '') . $controllerName . '.php';
+        $viewDir = $adminPath . 'view' . DS . $controllerUrl . DS;
+
+        //最终将生成的文件路径
+        $controllerFile = $adminPath . 'controller' . DS . $controllerFile;
+        $javascriptFile = ROOT_PATH . 'public' . DS . 'assets' . DS . 'js' . DS . 'backend' . DS . $controllerUrl . '.js';
+        $addFile = $viewDir . 'add.html';
+        $editFile = $viewDir . 'edit.html';
+        $indexFile = $viewDir . 'index.html';
+        $langFile = $adminPath . 'lang' . DS . Lang::detect() . DS . $controllerUrl . '.php';
+
+        //模型默认以表名进行处理,以下划线进行分隔,如果需要自定义则需要传入model,不支持目录层级
+        $modelName = $this->getModelName($model, $table);
+        $modelFile = ($local ? $adminPath : APP_PATH . 'common' . DS) . 'model' . DS . $modelName . '.php';
+
+        $validateFile = $adminPath . 'validate' . DS . $modelName . '.php';
+
+        //关联模型默认以表名进行处理,以下划线进行分隔,如果需要自定义则需要传入relationmodel,不支持目录层级
+        $relationModelName = $this->getModelName($relationModel, $relation);
+        $relationModelFile = ($local ? $adminPath : APP_PATH . 'common' . DS) . 'model' . DS . $relationModelName . '.php';
+
+        //是否为删除模式
+        $delete = $input->getOption('delete');
+        if ($delete)
+        {
+            $readyFiles = [$controllerFile, $modelFile, $validateFile, $addFile, $editFile, $indexFile, $langFile, $javascriptFile];
+            foreach ($readyFiles as $k => $v)
+            {
+                $output->warning($v);
+            }
+            $output->info("Are you sure you want to delete all those files?  Type 'yes' to continue: ");
+            $line = fgets(STDIN);
+            if (trim($line) != 'yes')
+            {
+                throw new Exception("Operation is aborted!");
+            }
+            foreach ($readyFiles as $k => $v)
+            {
+                if (file_exists($v))
+                    unlink($v);
+            }
+
+            $output->info("Delete Successed");
+            return;
+        }
+
+        //非覆盖模式时如果存在控制器文件则报错
+        if (is_file($controllerFile) && !$force)
+        {
+            throw new Exception("controller already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+
+        //非覆盖模式时如果存在模型文件则报错
+        if (is_file($modelFile) && !$force)
+        {
+            throw new Exception("model already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+
+        //非覆盖模式时如果存在验证文件则报错
+        if (is_file($validateFile) && !$force)
+        {
+            throw new Exception("validate already exists!\nIf you need to rebuild again, use the parameter --force=true ");
+        }
+
+        require $adminPath . 'common.php';
+
+        //从数据库中获取表字段信息
+        $sql = "SELECT * FROM `information_schema`.`columns` "
+                . "WHERE TABLE_SCHEMA = ? AND table_name = ? "
+                . "ORDER BY ORDINAL_POSITION";
+        $columnList = Db::query($sql, [$dbname, $tableName]);
+        $relationColumnList = [];
+        if ($relation)
+        {
+            $relationColumnList = Db::query($sql, [$dbname, $relationTableName]);
+        }
+
+        $fieldArr = [];
+        foreach ($columnList as $k => $v)
+        {
+            $fieldArr[] = $v['COLUMN_NAME'];
+        }
+
+        $relationFieldArr = [];
+        foreach ($relationColumnList as $k => $v)
+        {
+            $relationFieldArr[] = $v['COLUMN_NAME'];
+        }
+
+        $addList = [];
+        $editList = [];
+        $javascriptList = [];
+        $langList = [];
+        $field = 'id';
+        $order = 'id';
+        $priDefined = FALSE;
+        $priKey = '';
+        $relationPriKey = '';
+        foreach ($columnList as $k => $v)
+        {
+            if ($v['COLUMN_KEY'] == 'PRI')
+            {
+                $priKey = $v['COLUMN_NAME'];
+                break;
+            }
+        }
+        if (!$priKey)
+        {
+            throw new Exception('Primary key not found!');
+        }
+        if ($relation)
+        {
+            foreach ($relationColumnList as $k => $v)
+            {
+                if ($v['COLUMN_KEY'] == 'PRI')
+                {
+                    $relationPriKey = $v['COLUMN_NAME'];
+                    break;
+                }
+            }
+            if (!$relationPriKey)
+            {
+                throw new Exception('Relation Primary key not found!');
+            }
+        }
+        $order = $priKey;
+
+
+        //如果是关联模型
+        if ($relation)
+        {
+            if ($mode == 'hasone')
+            {
+                $relationForeignKey = $relationForeignKey ? $relationForeignKey : $table . "_id";
+                $relationPrimaryKey = $relationPrimaryKey ? $relationPrimaryKey : $priKey;
+                if (!in_array($relationForeignKey, $relationFieldArr))
+                {
+                    throw new Exception('relation table must be contain field:' . $relationForeignKey);
+                }
+                if (!in_array($relationPrimaryKey, $fieldArr))
+                {
+                    throw new Exception('table must be contain field:' . $relationPrimaryKey);
+                }
+            }
+            else
+            {
+                $relationForeignKey = $relationForeignKey ? $relationForeignKey : $relation . "_id";
+                $relationPrimaryKey = $relationPrimaryKey ? $relationPrimaryKey : $relationPriKey;
+                if (!in_array($relationForeignKey, $fieldArr))
+                {
+                    throw new Exception('table must be contain field:' . $relationForeignKey);
+                }
+                if (!in_array($relationPrimaryKey, $relationFieldArr))
+                {
+                    throw new Exception('relation table must be contain field:' . $relationPrimaryKey);
+                }
+            }
+        }
+
+        try
+        {
+            Form::setEscapeHtml(false);
+            $setAttrArr = [];
+            $getAttrArr = [];
+            $getEnumArr = [];
+            $appendAttrList = [];
+            $controllerAssignList = [];
+
+            //循环所有字段,开始构造视图的HTML和JS信息
+            foreach ($columnList as $k => $v)
+            {
+                $field = $v['COLUMN_NAME'];
+                $itemArr = [];
+                // 这里构建Enum和Set类型的列表数据
+                if (in_array($v['DATA_TYPE'], ['enum', 'set']))
+                {
+                    $itemArr = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1);
+                    $itemArr = explode(',', str_replace("'", '', $itemArr));
+                    $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']);
+                }
+                // 语言列表
+                if ($v['COLUMN_COMMENT'] != '')
+                {
+                    $langList[] = $this->getLangItem($field, $v['COLUMN_COMMENT']);
+                }
+                $inputType = '';
+                //createtime和updatetime是保留字段不能修改和添加
+                if ($v['COLUMN_KEY'] != 'PRI' && !in_array($field, $this->reservedField) && !in_array($field, $this->ignoreFields))
+                {
+                    $inputType = $this->getFieldType($v);
+
+                    // 如果是number类型时增加一个步长
+                    $step = $inputType == 'number' && $v['NUMERIC_SCALE'] > 0 ? "0." . str_repeat(0, $v['NUMERIC_SCALE'] - 1) . "1" : 0;
+
+                    $attrArr = ['id' => "c-{$field}"];
+                    $cssClassArr = ['form-control'];
+                    $fieldName = "row[{$field}]";
+                    $defaultValue = $v['COLUMN_DEFAULT'];
+                    $editValue = "{\$row.{$field}}";
+                    // 如果默认值非null,则是一个必选项
+                    if ($v['IS_NULLABLE'] == 'NO')
+                    {
+                        $attrArr['data-rule'] = 'required';
+                    }
+
+                    if ($inputType == 'select')
+                    {
+                        $cssClassArr[] = 'selectpicker';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        if ($v['DATA_TYPE'] == 'set')
+                        {
+                            $attrArr['multiple'] = '';
+                            $fieldName .= "[]";
+                        }
+                        $attrArr['name'] = $fieldName;
+
+                        $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select');
+
+                        $itemArr = $this->getLangArray($itemArr, FALSE);
+                        //添加一个获取器
+                        $this->getAttr($getAttrArr, $field, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select');
+                        if ($v['DATA_TYPE'] == 'set')
+                        {
+                            $this->setAttr($setAttrArr, $field, $inputType);
+                        }
+                        $this->appendAttr($appendAttrList, $field);
+                        $formAddElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]);
+                        $formEditElement = $this->getReplacedStub('html/select', ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]);
+                    }
+                    else if ($inputType == 'datetime')
+                    {
+                        $cssClassArr[] = 'datetimepicker';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $format = "YYYY-MM-DD HH:mm:ss";
+                        $phpFormat = "Y-m-d H:i:s";
+                        $fieldFunc = '';
+                        switch ($v['DATA_TYPE'])
+                        {
+                            case 'year';
+                                $format = "YYYY";
+                                $phpFormat = 'Y';
+                                break;
+                            case 'date';
+                                $format = "YYYY-MM-DD";
+                                $phpFormat = 'Y-m-d';
+                                break;
+                            case 'time';
+                                $format = "HH:mm:ss";
+                                $phpFormat = 'H:i:s';
+                                break;
+                            case 'timestamp';
+                                $fieldFunc = 'datetime';
+                            case 'datetime';
+                                $format = "YYYY-MM-DD HH:mm:ss";
+                                $phpFormat = 'Y-m-d H:i:s';
+                                break;
+                            default:
+                                $fieldFunc = 'datetime';
+                                $this->getAttr($getAttrArr, $field, $inputType);
+                                $this->setAttr($setAttrArr, $field, $inputType);
+                                $this->appendAttr($appendAttrList, $field);
+                                break;
+                        }
+                        $defaultDateTime = "{:date('{$phpFormat}')}";
+                        $attrArr['data-date-format'] = $format;
+                        $attrArr['data-use-current'] = "true";
+                        $fieldFunc = $fieldFunc ? "|{$fieldFunc}" : "";
+                        $formAddElement = Form::text($fieldName, $defaultDateTime, $attrArr);
+                        $formEditElement = Form::text($fieldName, "{\$row.{$field}{$fieldFunc}}", $attrArr);
+                    }
+                    else if ($inputType == 'checkbox' || $inputType == 'radio')
+                    {
+                        unset($attrArr['data-rule']);
+                        $fieldName = $inputType == 'checkbox' ? $fieldName .= "[]" : $fieldName;
+                        $attrArr['name'] = "row[{$fieldName}]";
+
+                        $this->getEnum($getEnumArr, $controllerAssignList, $field, $itemArr, $inputType);
+                        $itemArr = $this->getLangArray($itemArr, FALSE);
+                        //添加一个获取器
+                        $this->getAttr($getAttrArr, $field, $inputType);
+                        if ($inputType == 'checkbox')
+                        {
+                            $this->setAttr($setAttrArr, $field, $inputType);
+                        }
+                        $this->appendAttr($appendAttrList, $field);
+                        $defaultValue = $inputType == 'radio' && !$defaultValue ? key($itemArr) : $defaultValue;
+
+                        $formAddElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => $defaultValue]);
+                        $formEditElement = $this->getReplacedStub('html/' . $inputType, ['field' => $field, 'fieldName' => $fieldName, 'fieldList' => $this->getFieldListName($field), 'attrStr' => Form::attributes($attrArr), 'selectedValue' => "\$row.{$field}"]);
+                    }
+                    else if ($inputType == 'textarea')
+                    {
+                        $cssClassArr[] = substr($field, -7) == 'content' ? $this->editorClass : '';
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $attrArr['rows'] = 5;
+                        $formAddElement = Form::textarea($fieldName, $defaultValue, $attrArr);
+                        $formEditElement = Form::textarea($fieldName, $editValue, $attrArr);
+                    }
+                    else if ($inputType == 'switch')
+                    {
+                        unset($attrArr['data-rule']);
+                        if ($defaultValue === '1' || $defaultValue === 'Y')
+                        {
+                            $yes = $defaultValue;
+                            $no = $defaultValue === '1' ? '0' : 'N';
+                        }
+                        else
+                        {
+                            $no = $defaultValue;
+                            $yes = $defaultValue === '0' ? '1' : 'Y';
+                        }
+                        $formAddElement = $formEditElement = Form::hidden($fieldName, $no, array_merge(['checked' => ''], $attrArr));
+                        $attrArr['id'] = $fieldName . "-switch";
+                        $formAddElement .= sprintf(Form::label("{$attrArr['id']}", "%s abcdefg"), Form::checkbox($fieldName, $yes, $defaultValue === $yes, $attrArr));
+                        $formEditElement .= sprintf(Form::label("{$attrArr['id']}", "%s abcdefg"), Form::checkbox($fieldName, $yes, 0, $attrArr));
+                        $formEditElement = str_replace('type="checkbox"', 'type="checkbox" {in name="' . "\$row.{$field}" . '" value="' . $yes . '"}checked{/in}', $formEditElement);
+                    }
+                    else if ($inputType == 'citypicker')
+                    {
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $attrArr['data-toggle'] = "city-picker";
+                        $formAddElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $defaultValue, $attrArr));
+                        $formEditElement = sprintf("<div class='control-relative'>%s</div>", Form::input('text', $fieldName, $editValue, $attrArr));
+                    }
+                    else
+                    {
+                        $search = $replace = '';
+                        //特殊字段为关联搜索
+                        if ($this->isMatchSuffix($field, $this->selectpageSuffix))
+                        {
+                            $inputType = 'text';
+                            $defaultValue = '';
+                            $attrArr['data-rule'] = 'required';
+                            $cssClassArr[] = 'selectpage';
+                            $selectpageController = str_replace('_', '/', substr($field, 0, strripos($field, '_')));
+                            $attrArr['data-source'] = $selectpageController . "/index";
+                            //如果是类型表需要特殊处理下
+                            if ($selectpageController == 'category')
+                            {
+                                $attrArr['data-source'] = 'category/selectpage';
+                                $attrArr['data-params'] = '##replacetext##';
+                                $search = '"##replacetext##"';
+                                $replace = '\'{"custom[type]":"' . $table . '"}\'';
+                            }
+                            if ($this->isMatchSuffix($field, $this->selectpagesSuffix))
+                            {
+                                $attrArr['data-multiple'] = 'true';
+                            }
+                            foreach ($this->fieldSelectpageMap as $m => $n)
+                            {
+                                if (in_array($field, $n))
+                                {
+                                    $attrArr['data-field'] = $m;
+                                    break;
+                                }
+                            }
+                        }
+                        //因为有自动完成可输入其它内容
+                        $step = array_intersect($cssClassArr, ['selectpage']) ? 0 : $step;
+                        $attrArr['class'] = implode(' ', $cssClassArr);
+                        $isUpload = false;
+                        if ($this->isMatchSuffix($field, array_merge($this->imageField, $this->fileField)))
+                        {
+                            $isUpload = true;
+                        }
+                        //如果是步长则加上步长
+                        if ($step)
+                        {
+                            $attrArr['step'] = $step;
+                        }
+                        //如果是图片加上个size
+                        if ($isUpload)
+                        {
+                            $attrArr['size'] = 50;
+                        }
+
+                        $formAddElement = Form::input($inputType, $fieldName, $defaultValue, $attrArr);
+                        $formEditElement = Form::input($inputType, $fieldName, $editValue, $attrArr);
+                        if ($search && $replace)
+                        {
+                            $formAddElement = str_replace($search, $replace, $formAddElement);
+                            $formEditElement = str_replace($search, $replace, $formEditElement);
+                        }
+                        //如果是图片或文件
+                        if ($isUpload)
+                        {
+                            $formAddElement = $this->getImageUpload($field, $formAddElement);
+                            $formEditElement = $this->getImageUpload($field, $formEditElement);
+                        }
+                    }
+                    //构造添加和编辑HTML信息
+                    $addList[] = $this->getFormGroup($field, $formAddElement);
+                    $editList[] = $this->getFormGroup($field, $formEditElement);
+                }
+
+                //过滤text类型字段
+                if ($v['DATA_TYPE'] != 'text')
+                {
+                    //主键
+                    if ($v['COLUMN_KEY'] == 'PRI' && !$priDefined)
+                    {
+                        $priDefined = TRUE;
+                        $javascriptList[] = "{checkbox: true}";
+                    }
+                    //构造JS列信息
+                    $javascriptList[] = $this->getJsColumn($field, $v['DATA_TYPE'], $inputType && in_array($inputType, ['select', 'checkbox', 'radio']) ? '_text' : '', $itemArr);
+
+                    //排序方式,如果有指定排序字段,否则按主键排序
+                    $order = $field == $this->sortField ? $this->sortField : $order;
+                }
+            }
+
+            $relationPriKey = 'id';
+            $relationFieldArr = [];
+            foreach ($relationColumnList as $k => $v)
+            {
+                $relationField = $v['COLUMN_NAME'];
+                $relationFieldArr[] = $field;
+
+                $relationField = strtolower($relationModelName) . "." . $relationField;
+                // 语言列表
+                if ($v['COLUMN_COMMENT'] != '')
+                {
+                    $langList[] = $this->getLangItem($relationField, $v['COLUMN_COMMENT']);
+                }
+
+                //过滤text类型字段
+                if ($v['DATA_TYPE'] != 'text')
+                {
+                    //构造JS列信息
+                    $javascriptList[] = $this->getJsColumn($relationField, $v['DATA_TYPE']);
+                }
+            }
+
+            //JS最后一列加上操作列
+            $javascriptList[] = str_repeat(" ", 24) . "{field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate}";
+            $addList = implode("\n", array_filter($addList));
+            $editList = implode("\n", array_filter($editList));
+            $javascriptList = implode(",\n", array_filter($javascriptList));
+            $langList = implode(",\n", array_filter($langList));
+
+            //表注释
+            $tableComment = $tableInfo['Comment'];
+            $tableComment = mb_substr($tableComment, -1) == '表' ? mb_substr($tableComment, 0, -1) . '管理' : $tableComment;
+
+            $appNamespace = Config::get('app_namespace');
+            $moduleName = 'admin';
+            $controllerNamespace = "{$appNamespace}\\{$moduleName}\\controller" . ($controllerDir ? "\\" : "") . str_replace('/', "\\", $controllerDir);
+            $modelNamespace = "{$appNamespace}\\" . ($local ? $moduleName : "common") . "\\model";
+            $validateNamespace = "{$appNamespace}\\" . $moduleName . "\\validate";
+            $validateName = $modelName;
+
+            $modelInit = '';
+            if ($priKey != $order)
+            {
+                $modelInit = $this->getReplacedStub('mixins' . DS . 'modelinit', ['order' => $order]);
+            }
+
+            $data = [
+                'controllerNamespace'     => $controllerNamespace,
+                'modelNamespace'          => $modelNamespace,
+                'validateNamespace'       => $validateNamespace,
+                'controllerUrl'           => $controllerUrl,
+                'controllerDir'           => $controllerDir,
+                'controllerName'          => $controllerName,
+                'controllerAssignList'    => implode("\n", $controllerAssignList),
+                'modelName'               => $modelName,
+                'validateName'            => $validateName,
+                'tableComment'            => $tableComment,
+                'iconName'                => $iconName,
+                'pk'                      => $priKey,
+                'order'                   => $order,
+                'table'                   => $table,
+                'tableName'               => $tableName,
+                'addList'                 => $addList,
+                'editList'                => $editList,
+                'javascriptList'          => $javascriptList,
+                'langList'                => $langList,
+                'modelAutoWriteTimestamp' => in_array('createtime', $fieldArr) || in_array('updatetime', $fieldArr) ? "'int'" : 'false',
+                'createTime'              => in_array('createtime', $fieldArr) ? "'createtime'" : 'false',
+                'updateTime'              => in_array('updatetime', $fieldArr) ? "'updatetime'" : 'false',
+                'modelTableName'          => $modelTableName,
+                'modelTableType'          => $modelTableType,
+                'relationModelTableName'  => $relationModelTableName,
+                'relationModelTableType'  => $relationModelTableType,
+                'relationModelName'       => $relationModelName,
+                'relationWith'            => '',
+                'relationMethod'          => '',
+                'relationModel'           => '',
+                'relationForeignKey'      => '',
+                'relationPrimaryKey'      => '',
+                'relationSearch'          => $relation ? 'true' : 'false',
+                'controllerIndex'         => '',
+                'appendAttrList'          => implode(",\n", $appendAttrList),
+                'getEnumList'             => implode("\n\n", $getEnumArr),
+                'getAttrList'             => implode("\n\n", $getAttrArr),
+                'setAttrList'             => implode("\n\n", $setAttrArr),
+                'modelInit'               => $modelInit,
+                'modelRelationMethod'     => '',
+            ];
+
+            //如果使用关联模型
+            if ($relation)
+            {
+                //需要构造关联的方法
+                $data['relationMethod'] = strtolower($relationModelName);
+                //预载入的方法
+                $data['relationWith'] = "->with('{$data['relationMethod']}')";
+                //需要重写index方法
+                $data['controllerIndex'] = $this->getReplacedStub('controllerindex', $data);
+                //关联的模式
+                $data['relationMode'] = $mode == 'hasone' ? 'hasOne' : 'belongsTo';
+                //关联字段
+                $data['relationForeignKey'] = $relationForeignKey;
+                $data['relationPrimaryKey'] = $relationPrimaryKey ? $relationPrimaryKey : $priKey;
+                //构造关联模型的方法
+                $data['modelRelationMethod'] = $this->getReplacedStub('mixins' . DS . 'modelrelationmethod', $data);
+            }
+
+            // 生成控制器文件
+            $result = $this->writeToFile('controller', $data, $controllerFile);
+            // 生成模型文件
+            $result = $this->writeToFile('model', $data, $modelFile);
+            if ($relation && !is_file($relationModelFile))
+            {
+                // 生成关联模型文件
+                $result = $this->writeToFile('relationmodel', $data, $relationModelFile);
+            }
+            // 生成验证文件
+            $result = $this->writeToFile('validate', $data, $validateFile);
+            // 生成视图文件
+            $result = $this->writeToFile('add', $data, $addFile);
+            $result = $this->writeToFile('edit', $data, $editFile);
+            $result = $this->writeToFile('index', $data, $indexFile);
+            // 生成JS文件
+            $result = $this->writeToFile('javascript', $data, $javascriptFile);
+            // 生成语言文件
+            if ($langList)
+            {
+                $result = $this->writeToFile('lang', $data, $langFile);
+            }
+        }
+        catch (\think\exception\ErrorException $e)
+        {
+            throw new Exception("Code: " . $e->getCode() . "\nLine: " . $e->getLine() . "\nMessage: " . $e->getMessage() . "\nFile: " . $e->getFile());
+        }
+
+        //继续生成菜单
+        if ($menu)
+        {
+            exec("php think menu -c {$controllerUrl}");
+        }
+
+        $output->info("Build Successed");
+    }
+
+    protected function getEnum(&$getEnum, &$controllerAssignList, $field, $itemArr = '', $inputType = '')
+    {
+        if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio']))
+            return;
+        $fieldList = $this->getFieldListName($field);
+        $methodName = 'get' . ucfirst($fieldList);
+        foreach ($itemArr as $k => &$v)
+        {
+            $v = "__('" . mb_ucfirst($v) . "')";
+        }
+        unset($v);
+        $itemString = $this->getArrayString($itemArr);
+        $getEnum[] = <<<EOD
+    public function {$methodName}()
+    {
+        return [{$itemString}];
+    }     
+EOD;
+        $controllerAssignList[] = <<<EOD
+        \$this->view->assign("{$fieldList}", \$this->model->{$methodName}());
+EOD;
+    }
+
+    protected function getAttr(&$getAttr, $field, $inputType = '')
+    {
+        if (!in_array($inputType, ['datetime', 'select', 'multiple', 'checkbox', 'radio']))
+            return;
+        $attrField = ucfirst($this->getCamelizeName($field));
+        $getAttr[] = $this->getReplacedStub("mixins" . DS . $inputType, ['field' => $field, 'methodName' => "get{$attrField}TextAttr", 'listMethodName' => "get{$attrField}List"]);
+    }
+
+    protected function setAttr(&$setAttr, $field, $inputType = '')
+    {
+        if (!in_array($inputType, ['datetime', 'checkbox', 'select']))
+            return;
+        $attrField = ucfirst($this->getCamelizeName($field));
+        if ($inputType == 'datetime')
+        {
+            $return = <<<EOD
+return \$value && !is_numeric(\$value) ? strtotime(\$value) : \$value;
+EOD;
+        }
+        else if (in_array($inputType, ['checkbox', 'select']))
+        {
+            $return = <<<EOD
+return is_array(\$value) ? implode(',', \$value) : \$value;
+EOD;
+        }
+        $setAttr[] = <<<EOD
+    protected function set{$attrField}Attr(\$value)
+    {
+        $return
+    }
+EOD;
+    }
+
+    protected function appendAttr(&$appendAttrList, $field)
+    {
+        $appendAttrList[] = <<<EOD
+        '{$field}_text'
+EOD;
+    }
+
+    protected function getModelName($model, $table)
+    {
+        if (!$model)
+        {
+            $modelarr = explode('_', strtolower($table));
+            foreach ($modelarr as $k => &$v)
+                $v = ucfirst($v);
+            unset($v);
+            $modelName = implode('', $modelarr);
+        }
+        else
+        {
+            $modelName = ucfirst($model);
+        }
+        return $modelName;
+    }
+
+    /**
+     * 写入到文件
+     * @param string $name
+     * @param array $data
+     * @param string $pathname
+     * @return mixed
+     */
+    protected function writeToFile($name, $data, $pathname)
+    {
+        $content = $this->getReplacedStub($name, $data);
+
+        if (!is_dir(dirname($pathname)))
+        {
+            mkdir(strtolower(dirname($pathname)), 0755, true);
+        }
+        return file_put_contents($pathname, $content);
+    }
+
+    /**
+     * 获取替换后的数据
+     * @param string $name
+     * @param array $data
+     * @return string
+     */
+    protected function getReplacedStub($name, $data)
+    {
+        $search = $replace = [];
+        foreach ($data as $k => $v)
+        {
+            $search[] = "{%{$k}%}";
+            $replace[] = $v;
+        }
+        $stubname = $this->getStub($name);
+        if (isset($this->stubList[$stubname]))
+        {
+            $stub = $this->stubList[$stubname];
+        }
+        else
+        {
+            $this->stubList[$stubname] = $stub = file_get_contents($stubname);
+        }
+        $content = str_replace($search, $replace, $stub);
+        return $content;
+    }
+
+    /**
+     * 获取基础模板
+     * @param string $name
+     * @return string
+     */
+    protected function getStub($name)
+    {
+        return __DIR__ . DS . 'Crud' . DS . 'stubs' . DS . $name . '.stub';
+    }
+
+    protected function getLangItem($field, $content)
+    {
+        if ($content || !Lang::has($field))
+        {
+            $itemArr = [];
+            if (stripos($content, ':') !== false && stripos($content, ',') && stripos($content, '=') !== false)
+            {
+                list($fieldLang, $item) = explode(':', $content);
+                $itemArr = [$field => $fieldLang];
+                foreach (explode(',', $item) as $k => $v)
+                {
+                    $valArr = explode('=', $v);
+                    if (count($valArr) == 2)
+                    {
+                        list($key, $value) = $valArr;
+                        $itemArr[$field . ' ' . $key] = $value;
+                    }
+                }
+            }
+            else
+            {
+                $itemArr = [$field => $content];
+            }
+            $resultArr = [];
+            foreach ($itemArr as $k => $v)
+            {
+                $resultArr[] = "    '" . mb_ucfirst($k) . "'  =>  '{$v}'";
+            }
+            return implode(",\n", $resultArr);
+        }
+        else
+        {
+            return '';
+        }
+    }
+
+    /**
+     * 读取数据和语言数组列表
+     * @param array $arr
+     * @return array
+     */
+    protected function getLangArray($arr, $withTpl = TRUE)
+    {
+        $langArr = [];
+        foreach ($arr as $k => $v)
+        {
+            $langArr[(is_numeric($k) ? $v : $k)] = is_numeric($k) ? ($withTpl ? "{:" : "") . "__('" . mb_ucfirst($v) . "')" . ($withTpl ? "}" : "") : $v;
+        }
+        return $langArr;
+    }
+
+    /**
+     * 将数据转换成带字符串
+     * @param array $arr
+     * @return string
+     */
+    protected function getArrayString($arr)
+    {
+        if (!is_array($arr))
+            return $arr;
+        $stringArr = [];
+        foreach ($arr as $k => $v)
+        {
+            $is_var = in_array(substr($v, 0, 1), ['$', '_']);
+            if (!$is_var)
+            {
+                $v = str_replace("'", "\'", $v);
+                $k = str_replace("'", "\'", $k);
+            }
+            $stringArr[] = "'" . $k . "' => " . ($is_var ? $v : "'{$v}'");
+        }
+        return implode(",", $stringArr);
+    }
+
+    protected function getItemArray($item, $field, $comment)
+    {
+        $itemArr = [];
+        if (stripos($comment, ':') !== false && stripos($comment, ',') && stripos($comment, '=') !== false)
+        {
+            list($fieldLang, $item) = explode(':', $comment);
+            $itemArr = [];
+            foreach (explode(',', $item) as $k => $v)
+            {
+                $valArr = explode('=', $v);
+                if (count($valArr) == 2)
+                {
+                    list($key, $value) = $valArr;
+                    $itemArr[$key] = $field . ' ' . $key;
+                }
+            }
+        }
+        else
+        {
+            foreach ($item as $k => $v)
+            {
+                $itemArr[$v] = is_numeric($v) ? $field . ' ' . $v : $v;
+            }
+        }
+        return $itemArr;
+    }
+
+    protected function getFieldType(& $v)
+    {
+        $inputType = 'text';
+        switch ($v['DATA_TYPE'])
+        {
+            case 'bigint':
+            case 'int':
+            case 'mediumint':
+            case 'smallint':
+            case 'tinyint':
+                $inputType = 'number';
+                break;
+            case 'enum':
+            case 'set':
+                $inputType = 'select';
+                break;
+            case 'decimal':
+            case 'double':
+            case 'float':
+                $inputType = 'number';
+                break;
+            case 'longtext':
+            case 'text':
+            case 'mediumtext':
+            case 'smalltext':
+            case 'tinytext':
+                $inputType = 'textarea';
+                break;
+            case 'year';
+            case 'date';
+            case 'time';
+            case 'datetime';
+            case 'timestamp';
+                $inputType = 'datetime';
+                break;
+            default:
+                break;
+        }
+        $fieldsName = $v['COLUMN_NAME'];
+        // 指定后缀说明也是个时间字段
+        if ($this->isMatchSuffix($fieldsName, $this->intDateSuffix))
+        {
+            $inputType = 'datetime';
+        }
+        // 指定后缀结尾且类型为enum,说明是个单选框
+        if ($this->isMatchSuffix($fieldsName, $this->enumRadioSuffix) && $v['DATA_TYPE'] == 'enum')
+        {
+            $inputType = "radio";
+        }
+        // 指定后缀结尾且类型为set,说明是个复选框
+        if ($this->isMatchSuffix($fieldsName, $this->setCheckboxSuffix) && $v['DATA_TYPE'] == 'set')
+        {
+            $inputType = "checkbox";
+        }
+        // 指定后缀结尾且类型为char或tinyint且长度为1,说明是个Switch复选框
+        if ($this->isMatchSuffix($fieldsName, $this->switchSuffix) && ($v['COLUMN_TYPE'] == 'tinyint(1)' || $v['COLUMN_TYPE'] == 'char(1)') && $v['COLUMN_DEFAULT'] !== '' && $v['COLUMN_DEFAULT'] !== null)
+        {
+            $inputType = "switch";
+        }
+        // 指定后缀结尾城市选择框
+        if ($this->isMatchSuffix($fieldsName, $this->citySuffix) && ($v['DATA_TYPE'] == 'varchar' || $v['DATA_TYPE'] == 'char'))
+        {
+            $inputType = "citypicker";
+        }
+        return $inputType;
+    }
+
+    /**
+     * 判断是否符合指定后缀
+     * @param string $field 字段名称
+     * @param mixed $suffixArr 后缀
+     * @return boolean
+     */
+    protected function isMatchSuffix($field, $suffixArr)
+    {
+        $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr);
+        foreach ($suffixArr as $k => $v)
+        {
+            if (preg_match("/{$v}$/i", $field))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 获取表单分组数据
+     * @param string $field
+     * @param string $content
+     * @return string
+     */
+    protected function getFormGroup($field, $content)
+    {
+        $langField = mb_ucfirst($field);
+        return<<<EOD
+    <div class="form-group">
+        <label for="c-{$field}" class="control-label col-xs-12 col-sm-2">{:__('{$langField}')}:</label>
+        <div class="col-xs-12 col-sm-8">
+            {$content}
+        </div>
+    </div>
+EOD;
+    }
+
+    /**
+     * 获取图片模板数据
+     * @param string $field
+     * @param string $content
+     * @return array
+     */
+    protected function getImageUpload($field, $content)
+    {
+        $uploadfilter = $selectfilter = '';
+        if ($this->isMatchSuffix($field, $this->imageField))
+        {
+            $uploadfilter = ' data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp"';
+            $selectfilter = ' data-mimetype="image/*"';
+        }
+        $multiple = substr($field, -1) == 's' ? ' data-multiple="true"' : ' data-multiple="false"';
+        $preview = $uploadfilter ? ' data-preview-id="p-' . $field . '"' : '';
+        $previewcontainer = $preview ? '<ul class="row list-inline plupload-preview" id="p-' . $field . '"></ul>' : '';
+        return <<<EOD
+<div class="input-group">
+                {$content}
+                <div class="input-group-addon no-border no-padding">
+                    <span><button type="button" id="plupload-{$field}" class="btn btn-danger plupload" data-input-id="c-{$field}"{$uploadfilter}{$multiple}{$preview}><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                    <span><button type="button" id="fachoose-{$field}" class="btn btn-primary fachoose" data-input-id="c-{$field}"{$selectfilter}{$multiple}><i class="fa fa-list"></i> {:__('Choose')}</button></span>
+                </div>
+                <span class="msg-box n-right" for="c-{$field}"></span>
+            </div>
+            {$previewcontainer}
+EOD;
+    }
+
+    /**
+     * 获取JS列数据
+     * @param string $field
+     * @param string $datatype
+     * @param string $extend
+     * @param array $itemArr
+     * @return string
+     */
+    protected function getJsColumn($field, $datatype = '', $extend = '', $itemArr = [])
+    {
+        $lang = mb_ucfirst($field);
+        $formatter = '';
+        foreach ($this->fieldFormatterSuffix as $k => $v)
+        {
+            if (preg_match("/{$k}$/i", $field))
+            {
+                if (is_array($v))
+                {
+                    if (in_array($datatype, $v['type']))
+                    {
+                        $formatter = $v['name'];
+                        break;
+                    }
+                }
+                else
+                {
+                    $formatter = $v;
+                    break;
+                }
+            }
+        }
+        if ($formatter)
+        {
+            $extend = '';
+        }
+        $html = str_repeat(" ", 24) . "{field: '{$field}{$extend}', title: __('{$lang}')";
+        //$formatter = $extend ? '' : $formatter;
+        if ($extend)
+        {
+            $html .= ", operate:false";
+            if ($datatype == 'set')
+            {
+                $formatter = 'label';
+            }
+        }
+        foreach ($itemArr as $k => &$v)
+        {
+            if (substr($v, 0, 3) !== '__(')
+                $v = "__('" . $v . "')";
+        }
+        unset($v);
+        $searchList = json_encode($itemArr);
+        $searchList = str_replace(['":"', '"}', ')","'], ['":', '}', '),"'], $searchList);
+        if ($itemArr && !$extend)
+        {
+            $html .= ", searchList: " . $searchList;
+        }
+        echo $datatype, "\n";
+        if (in_array($datatype, ['date', 'datetime']) || $formatter === 'datetime')
+        {
+            $html .= ", operate:'RANGE', addclass:'datetimerange'";
+        }
+        else if (in_array($datatype,['float', 'double', 'decimal']))
+        {
+            $html .= ", operate:'BETWEEN'";
+        }
+        if ($formatter)
+            $html .= ", formatter: Table.api.formatter." . $formatter . "}";
+        else
+            $html .= "}";
+        if ($extend)
+        {
+            $origin = str_repeat(" ", 24) . "{field: '{$field}', title: __('{$lang}'), visible:false";
+            if ($searchList)
+            {
+                $origin .= ", searchList: " . $searchList;
+            }
+            $origin .= "}";
+            $html = $origin . ",\n" . $html;
+        }
+        return $html;
+    }
+
+    protected function getCamelizeName($uncamelized_words, $separator = '_')
+    {
+        $uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words));
+        return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator);
+    }
+
+    protected function getFieldListName($field)
+    {
+        return $this->getCamelizeName($field) . 'List';
+    }
+
+}

+ 11 - 0
application/admin/command/Crud/stubs/add.stub

@@ -0,0 +1,11 @@
+<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+{%addList%}
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 37 - 0
application/admin/command/Crud/stubs/controller.stub

@@ -0,0 +1,37 @@
+<?php
+
+namespace {%controllerNamespace%};
+
+use app\common\controller\Backend;
+
+use think\Controller;
+use think\Request;
+
+/**
+ * {%tableComment%}
+ *
+ * @icon {%iconName%}
+ */
+class {%controllerName%} extends Backend
+{
+    
+    /**
+     * {%modelName%}模型对象
+     */
+    protected $model = null;
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = model('{%modelName%}');
+{%controllerAssignList%}
+    }
+    
+    /**
+     * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个方法
+     * 因此在当前控制器中可不用编写增删改查的代码,如果需要自己控制这部分逻辑
+     * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改
+     */
+    
+{%controllerIndex%}
+}

+ 31 - 0
application/admin/command/Crud/stubs/controllerindex.stub

@@ -0,0 +1,31 @@
+
+    /**
+     * 查看
+     */
+    public function index()
+    {
+        //当前是否为关联查询
+        $this->relationSearch = {%relationSearch%};
+        //设置过滤方法
+        $this->request->filter(['strip_tags', 'htmlspecialchars']);
+        if ($this->request->isAjax())
+        {
+            list($where, $sort, $order, $offset, $limit) = $this->buildparams();
+            $total = $this->model
+                    {%relationWith%}
+                    ->where($where)
+                    ->order($sort, $order)
+                    ->count();
+
+            $list = $this->model
+                    {%relationWith%}
+                    ->where($where)
+                    ->order($sort, $order)
+                    ->limit($offset, $limit)
+                    ->select();
+            $result = array("total" => $total, "rows" => $list);
+
+            return json($result);
+        }
+        return $this->view->fetch();
+    }

+ 11 - 0
application/admin/command/Crud/stubs/edit.stub

@@ -0,0 +1,11 @@
+<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
+
+{%editList%}
+    <div class="form-group layer-footer">
+        <label class="control-label col-xs-12 col-sm-2"></label>
+        <div class="col-xs-12 col-sm-8">
+            <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button>
+            <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+        </div>
+    </div>
+</form>

+ 6 - 0
application/admin/command/Crud/stubs/html/checkbox.stub

@@ -0,0 +1,6 @@
+
+            <div class="checkbox">
+            {foreach name="{%fieldList%}" item="vo"}
+            <label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="checkbox" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label> 
+            {/foreach}
+            </div>

+ 6 - 0
application/admin/command/Crud/stubs/html/radio.stub

@@ -0,0 +1,6 @@
+
+            <div class="radio">
+            {foreach name="{%fieldList%}" item="vo"}
+            <label for="{%fieldName%}-{$key}"><input id="{%fieldName%}-{$key}" name="{%fieldName%}" type="radio" value="{$key}" {in name="key" value="{%selectedValue%}"}checked{/in} /> {$vo}</label> 
+            {/foreach}
+            </div>

+ 6 - 0
application/admin/command/Crud/stubs/html/select.stub

@@ -0,0 +1,6 @@
+            
+            <select {%attrStr%}>
+                {foreach name="{%fieldList%}" item="vo"}
+                    <option value="{$key}" {in name="key" value="{%selectedValue%}"}selected{/in}>{$vo}</option>
+                {/foreach}
+            </select>

+ 33 - 0
application/admin/command/Crud/stubs/index.stub

@@ -0,0 +1,33 @@
+<div class="panel panel-default panel-intro">
+    {:build_heading()}
+
+    <div class="panel-body">
+        <div id="myTabContent" class="tab-content">
+            <div class="tab-pane fade active in" id="one">
+                <div class="widget-body no-padding">
+                    <div id="toolbar" class="toolbar">
+                        <a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
+                        <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('{%controllerUrl%}/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
+                        <a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('{%controllerUrl%}/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('{%controllerUrl%}/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
+                        <a href="javascript:;" class="btn btn-danger btn-import {:$auth->check('{%controllerUrl%}/import')?'':'hide'}" title="{:__('Import')}" id="btn-import-file" data-url="ajax/upload" data-mimetype="csv,xls,xlsx" data-multiple="false"><i class="fa fa-upload"></i> {:__('Import')}</a>
+
+                        <div class="dropdown btn-group {:$auth->check('{%controllerUrl%}/multi')?'':'hide'}">
+                            <a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
+                            <ul class="dropdown-menu text-left" role="menu">
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=normal"><i class="fa fa-eye"></i> {:__('Set to normal')}</a></li>
+                                <li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:;" data-params="status=hidden"><i class="fa fa-eye-slash"></i> {:__('Set to hidden')}</a></li>
+                            </ul>
+                        </div>
+                    </div>
+                    <table id="table" class="table table-striped table-bordered table-hover" 
+                           data-operate-edit="{:$auth->check('{%controllerUrl%}/edit')}" 
+                           data-operate-del="{:$auth->check('{%controllerUrl%}/del')}" 
+                           width="100%">
+                    </table>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>

+ 47 - 0
application/admin/command/Crud/stubs/javascript.stub

@@ -0,0 +1,47 @@
+define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+
+    var Controller = {
+        index: function () {
+            // 初始化表格参数配置
+            Table.api.init({
+                extend: {
+                    index_url: '{%controllerUrl%}/index',
+                    add_url: '{%controllerUrl%}/add',
+                    edit_url: '{%controllerUrl%}/edit',
+                    del_url: '{%controllerUrl%}/del',
+                    multi_url: '{%controllerUrl%}/multi',
+                    table: '{%table%}',
+                }
+            });
+
+            var table = $("#table");
+
+            // 初始化表格
+            table.bootstrapTable({
+                url: $.fn.bootstrapTable.defaults.extend.index_url,
+                pk: '{%pk%}',
+                sortName: '{%order%}',
+                columns: [
+                    [
+                        {%javascriptList%}
+                    ]
+                ]
+            });
+
+            // 为表格绑定事件
+            Table.api.bindevent(table);
+        },
+        add: function () {
+            Controller.api.bindevent();
+        },
+        edit: function () {
+            Controller.api.bindevent();
+        },
+        api: {
+            bindevent: function () {
+                Form.api.bindevent($("form[role=form]"));
+            }
+        }
+    };
+    return Controller;
+});

+ 5 - 0
application/admin/command/Crud/stubs/lang.stub

@@ -0,0 +1,5 @@
+<?php
+
+return [
+{%langList%}
+];

+ 8 - 0
application/admin/command/Crud/stubs/mixins/checkbox.stub

@@ -0,0 +1,8 @@
+
+    public function {%methodName%}($value, $data)
+    {
+        $value = $value ? $value : $data['{%field%}'];
+        $valueArr = explode(',', $value);
+        $list = $this->{%listMethodName%}();
+        return implode(',', array_intersect_key($list, array_flip($valueArr)));
+    }

Some files were not shown because too many files changed in this diff