├── .gitignore
├── .idea
├── Ttel-Server-Koa.iml
├── dbnavigator.xml
├── misc.xml
├── modules.xml
├── vcs.xml
├── watcherTasks.xml
└── workspace.xml
├── README.md
├── app.js
├── app
├── controllers
│ └── appInfo_controller.js
├── db
│ └── dbHelper.js
├── error
│ ├── api_error.js
│ ├── appInfo_error.js
│ └── index.js
├── middleWares
│ ├── log_util.js
│ └── response_formatter.js
├── model
│ └── appInfo_model.js
├── routes
│ ├── appInfo_router.js
│ ├── index.js
│ └── plist_router.js
├── service
│ ├── appInfo_service.js
│ ├── email_service.js
│ └── pushNoti_service.js
└── supportingFiles
│ ├── pngdefry-linux
│ ├── pngdefry-osx
│ └── template.plist
├── bin
├── init
│ ├── cer
│ │ └── generate-certificate.sh
│ ├── prepareCerNDir.js
│ ├── setupDatabase.js
│ └── sql
│ │ └── app_info.sql
└── www
├── config
├── envi_config.js
└── log_config.js
├── package.json
├── pm2.json
├── public
├── css
│ └── Index.css
├── img
│ ├── index_bg.jpg
│ └── qr_code.png
├── index.html
├── js
│ ├── index.js
│ └── jquery.js
└── stylesheets
│ └── style.css
└── views
├── error.pug
├── index.pug
└── layout.pug
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData
8 | node_modules/
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata
19 |
20 | ## Other
21 | *.xccheckout
22 | *.moved-aside
23 | *.xcuserstate
24 | *.xcscmblueprint
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 |
30 | ## Playgrounds
31 | timeline.xctimeline
32 | playground.xcworkspace
33 |
34 | # Swift Package Manager
35 | #
36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
37 | # Packages/
38 | .build/
39 |
40 | # CocoaPods
41 | #
42 | # We recommend against adding the Pods directory to your .gitignore. However
43 | # you should judge for yourself, the pros and cons are mentioned at:
44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
45 | #
46 | # Pods/
47 |
48 | # Carthage
49 | #
50 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
51 | # Carthage/Checkouts
52 |
53 | Carthage/Build
54 |
55 | # fastlane
56 | #
57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
58 | # screenshots whenever they are needed.
59 | # For more information about the recommended setup visit:
60 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
61 |
62 | fastlane/report.xml
63 | fastlane/screenshots
64 |
65 | Packages/
66 | PerfectTemplate.xcodeproj
67 | Tests/
68 | webroot/
69 |
--------------------------------------------------------------------------------
/.idea/Ttel-Server-Koa.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/dbnavigator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 |
524 |
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/watcherTasks.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
134 |
135 |
136 |
137 | ApiError
138 | rorHa----------
139 | nhandled Rejection at: Pr
140 | dev.app_info' doesn't exist
141 | sdfhljklj
142 | global
143 | "
144 | format
145 | onerror
146 | console
147 | walkFile
148 | sql脚本执行结束
149 | sqlContentMap
150 | getSqlContentMap
151 | underscore
152 | serverD->
153 | path
154 | formatReqLog
155 | envi_config
156 | log_config
157 | sqlite3
158 | mysql
159 | pug
160 | fs-extra
161 | mustache
162 | dbushell-grunt-mustatic
163 | shelljs
164 | strftime
165 | exec_mode
166 | 10
167 |
168 |
169 |
170 | const
171 | '
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 | true
240 |
241 | false
242 | true
243 |
244 |
245 | true
246 | DEFINITION_ORDER
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 |
474 |
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 |
484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 |
494 |
495 |
496 |
497 |
498 |
499 |
500 | project
501 |
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 |
514 |
515 |
516 | project
517 |
518 |
519 | true
520 |
521 |
522 |
523 | DIRECTORY
524 |
525 | false
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 |
544 |
545 |
546 |
547 |
548 |
549 |
550 |
551 | 1493964758531
552 |
553 |
554 | 1493964758531
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 |
574 |
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 |
584 |
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 |
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 |
614 |
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 |
624 |
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 |
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 |
734 |
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 |
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 |
834 |
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 |
844 |
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 |
864 |
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 |
874 |
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 |
894 |
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 |
904 |
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ttel-Server
2 |
3 | ## 接口字段说明:
4 |
5 |
6 | | 字段名 | 说明 |示例 |
7 | |----------|--------|----|
8 | |**changeLog**| 更新日志| 增加邮件发送功能|
9 | |**itemId**| 包的ID, 每次上传的包都会生成一个唯一的ID,即使传的是同一个ipa或者apk文件 | 4af47af5-92ca-4f6c-a4d0-d0f3cdd56471
10 | |**prodType**| 产品类型码,不同产品,对应的产品ID | 1031
11 | |**envType**| 包环境类型, 1:SIT 2:UAT 3:PRO|
12 | |**platform**| 平台 1:iOS 2:Android |
13 | |**buildVersion**| 编译版本号 | 2017060503
14 | |**displayName**| 产品名称 | Ttel
15 | | **version**| 产品版本| 1.2
16 | | **appIdentifier**| iOS为bundleID, Android为包名| com.tencent.wechat
17 | | **iconUrl**| 图片下载地址 | https://172.16.88.230:8874/icon/4af47af5-92ca-4f6c-a4d0-d0f3cdd56471.png
18 | |**downloadUrl**| 包的下载地址| Android: https://172.16.88.123:8383/apk/75a8c69c43134.apk, iOS:itms-services://?action=download-manifest&url=https://172.16.88.230:8874/plist/13dea4ae-69f0-42e2-beab-11e576d061fc
19 | |**updatedDate**| 更新时间|2017-06-12T03:45:37.000Z
20 | | **createdDate**|包上传时间 |2017-06-12T03:45:57.000Z,
21 | |**fileSize**| 安装包大小| 33345197
22 | |**pageSize**|分页大小,默认为10| 20
23 | |**pageNo**|页码,从1开始的正整数| 1
24 |
25 |
26 | ## 上传应用
27 | **POST**: apiv1/app/upload
28 |
29 | **Request:**
30 |
31 | | 请求 | 必填 | 参数 | [类型]/限制 | 示例 |
32 | |-----|----|---|--------|----|
33 | 1 | 必填 | package | [int] | 无 |
34 | 1 | 必填 | envType | [int]| 无 |
35 | 1 | 必填 | prodType | [int]| 无 |
36 | 1 | 选填 | changeLog | [text]| 无 |
37 |
38 | **Response:**
39 |
40 | ```json
41 | {
42 | "msg":"SUCCESS",
43 | "code":1,
44 | "data":{
45 | "changeLog":"changeLogchangeLogchangeLogchangeLog",
46 | "itemId":"9a344aeb-2057-4f78-9134-3decb69d411c",
47 | "prodType":1001,
48 | "envType":1,
49 | "updatedDate":"2017-04-05T03:36:10.462Z",
50 | "createdDate":"2017-04-05T03:36:10.462Z",
51 | "fileSize":33030328,
52 | "platform":1,
53 | "buildVersion":"2017032102",
54 | "displayName":"Ttel",
55 | "version":"4.1.4",
56 | "appIdentifier":"cn.papa.xfbeta",
57 | "iconUrl":"https://172.16.88.126:8181/icon/9a344aeb-2057-4f78-9134-3decb69d411c.png",
58 | "downloadUrl":"itms-services://?action=download-manifest&url=https://172.16.88.126:8181/plist/9a344aeb-2057-4f78-9134-3decb69d411c"
59 | }
60 | }
61 | ```
62 |
63 | ## 查询所有产品最新的版本
64 | **GET:** apiv1/app/listAllProds
65 |
66 | **Request:**
67 |
68 | | 请求 | 必填 | 参数 | [类型]/限制 | 示例 |
69 | |-----|----|---|--------|----|
70 | 1 | 必填 | platform | [int] | 无 |
71 | 1 | 必填 | pageNo | [int] | 无 |
72 | 1 | 选填 | pageSize | [int] | 无 |
73 |
74 | ```
75 | {"pageNo":"1","pageSize":"15","platform":"1"}
76 | ```
77 |
78 | **Response:**
79 |
80 | ```json
81 | {
82 | "code": 1,
83 | "msg": "success",
84 | "data": [
85 | {
86 | "itemId": "297a22b6-2caa-4c1f-acce-bd35e95aff91",
87 | "prodType": 1031,
88 | "envType": 3,
89 | "platform": 1,
90 | "fileSize": "16499950",
91 | "buildVersion": "201700612.01",
92 | "displayName": "Ttel",
93 | "version": "1.2",
94 | "updatedDate": "2017-06-12T03:45:57.000Z",
95 | "createdDate": "2017-06-12T03:45:57.000Z",
96 | "appIdentifier": "cn.papa.ttel",
97 | "changeLog": "",
98 | "iconUrl": "https://172.16.88.230:8874/icon/297a22b6-2caa-4c1f-acce-bd35e95aff91.png",
99 | "downloadUrl": "itms-services://?action=download-manifest&url=https://172.16.88.230:8874/plist/297a22b6-2caa-4c1f-acce-bd35e95aff91"
100 | },
101 | {
102 | "itemId": "1c2f477f-c266-46c9-8752-ab9f87a5cddc",
103 | "prodType": 1001,
104 | "envType": 1,
105 | "platform": 1,
106 | "fileSize": "30231810",
107 | "buildVersion": "20170610.02",
108 | "displayName": "通 ",
109 | "version": "5.1.0",
110 | "updatedDate": "2017-06-10T09:59:03.000Z",
111 | "createdDate": "2017-06-10T09:59:03.000Z",
112 | "appIdentifier": "com.ppjs.ddtbeta",
113 | "changeLog": "",
114 | "iconUrl": "https://172.16.88.230:8874/icon/1c2f477f-c266-46c9-8752-ab9f87a5cddc.png",
115 | "downloadUrl": "itms-services://?action=download-manifest&url=https://172.16.88.230:8874/plist/1c2f477f-c266-46c9-8752-ab9f87a5cddc"
116 | }
117 | ]
118 | }
119 | ```
120 |
121 | ## 查询指定产品
122 | **GET:** apiv1/app/listSpecificProd
123 |
124 | **Request:**
125 |
126 | | 请求 | 必填 | 参数 | [类型]/限制 | 示例 |
127 | |-----|----|---|--------|----|
128 | 1 | 必填 | prodType | [int]| 无 |
129 | 2 | 必填 | pageNo | [int] | 无 |
130 | 3 | 非必填 | pageSize | [int] | 无
131 | 4 | 必填 | envType | [int]| 无
132 | 5 | 必填 |platform |[int] |无
133 |
134 | ```
135 | {"envType":"3","pageNo":"1","pageSize":"100","platform":"1","prodType":"1031"}
136 | ```
137 |
138 | **Response:**
139 |
140 | ```json
141 | {
142 | "code": 1,
143 | "msg": "success",
144 | "data": [
145 | {
146 | "itemId": "297a22b6-2caa-4c1f-acce-bd35e95aff91",
147 | "prodType": 1031,
148 | "envType": 3,
149 | "platform": 1,
150 | "fileSize": "16499950",
151 | "buildVersion": "201700612.01",
152 | "displayName": "Ttel",
153 | "version": "1.2",
154 | "updatedDate": "2017-06-12T03:45:57.000Z",
155 | "createdDate": "2017-06-12T03:45:57.000Z",
156 | "appIdentifier": "cn.papa.ttel",
157 | "changeLog": "",
158 | "iconUrl": "https://172.16.88.230:8874/icon/297a22b6-2caa-4c1f-acce-bd35e95aff91.png",
159 | "downloadUrl": "itms-services://?action=download-manifest&url=https://172.16.88.230:8874/plist/297a22b6-2caa-4c1f-acce-bd35e95aff91"
160 | }
161 | ]
162 | }
163 | ```
164 |
165 | ## 发送app到QA邮箱
166 | **POST:** apiv1/app/emailqa
167 |
168 | | 请求 | 必填 | 参数 | [类型]/限制 | 示例
169 | |-----|----|---|--------|----|
170 | 1| 必填 | itemIds: 要发送的app ids| [array]| 无
171 | 2 | 必填 | receivers 接收的Email地址 | [array]| 无
172 | 3 | 非必填| from 谁发送的 | [text]| 无
173 | 4 | 非必填| subject| 邮件主题| [text]| 无
174 | 5 | 非必填| remark 备注信息| [text]| 无
175 |
176 | **Request:**
177 |
178 | ```
179 | {
180 | "itemIds":["297a22b6-2caa-4c1f-acce-bd35e95aff91"],
181 | "receivers":["whailong2010@gmail.com"]
182 | }
183 | ```
184 |
185 | Response:
186 |
187 | ```json
188 | {
189 | "code": 1,
190 | "msg": "success",
191 | "data": {
192 | "note": "发送成功"
193 | }
194 | }
195 | ```
196 | ## 删除指定的APP条目
197 |
198 | **DELETE:** apiv1/app/delete
199 |
200 | | 请求 | 必填 | 参数 | [类型]/限制 | 示例
201 | |-----|----|---|--------|----|
202 | 1| 必填 | itemId: | [text]| 无
203 |
204 | Response:
205 |
206 | ```json
207 | {
208 | "msg":"SUCCESS",
209 | "code":1,
210 | "data":null
211 | }
212 | ```
213 | ## 取证书
214 | **GET:** /cer/pubCer/selfSigned_pubCA.cer
215 |
216 | iOS系统需要安装证书才能正常安装APP, 使用Safari浏览器打开即可安装
217 |
218 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const app = new Koa();
3 | const views = require('koa-views');
4 | const json = require('koa-json');
5 | const onerror = require('koa-onerror');
6 | const koaBody = require('koa-body');
7 | const logger = require('koa-logger');
8 | const serve = require('koa-static');
9 |
10 | const logUtil = require('./app/middleWares/log_util');
11 | const resFormatter = require('./app/middleWares/response_formatter');
12 | const enviConfig = require('./config/envi_config');
13 |
14 | //routes
15 | const routerIndex = require('./app/routes');
16 |
17 | // error handler
18 | onerror(app);
19 |
20 | // koaBody
21 | app.use(koaBody({
22 | multipart: true,
23 | formidable: {
24 | keepExtensions: true
25 | }
26 | }));
27 |
28 | // app.use(logger());
29 | app.use(serve(__dirname + '/public'));
30 | app.use(serve(enviConfig.serverDir));
31 |
32 | app.use(views(__dirname + '/views', {
33 | extension: 'pug'
34 | }));
35 |
36 | // logger
37 | app.use(logUtil());
38 |
39 | //添加格式化处理响应结果的中间件,在添加路由之前调用
40 | //仅对/api开头的url进行格式化处理
41 | app.use(resFormatter('^/api'));
42 |
43 | // routes
44 | app.use(routerIndex.routes(), routerIndex.allowedMethods());
45 |
46 | module.exports = app;
47 |
--------------------------------------------------------------------------------
/app/controllers/appInfo_controller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 05/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const AppInfoError = require('./../error/appInfo_error');
7 |
8 | const appInfoService = require('./../service/appInfo_service');
9 |
10 | const mailService = require('./../service/email_service');
11 |
12 | let listAllProds = async (ctx, next) => {
13 |
14 | const pageNo = parseInt(ctx.query.pageNo);
15 | if (!pageNo) {
16 |
17 | throw AppInfoError.ParamError('pageNo 参数错误');
18 | }
19 |
20 | var pageSize = parseInt(ctx.query.pageSize);
21 | if (!pageSize || pageSize === 0) {
22 |
23 | pageSize = 10;
24 | }
25 |
26 | const platform = parseInt(ctx.query.platform);
27 | if (!platform) {
28 |
29 | throw AppInfoError.ParamError('platform 参数错误');
30 | }
31 |
32 | ctx.body = await appInfoService.listAllProds(platform, pageNo, pageSize);
33 | };
34 |
35 | let listSpecificProd = async (ctx, next) => {
36 | //pageNo pageSize platform
37 |
38 | const platform = parseInt(ctx.query.platform);
39 | if (!platform) {
40 |
41 | throw AppInfoError.ParamError('platform 参数错误');
42 |
43 | }
44 |
45 | const prodType = parseInt(ctx.query.prodType);
46 | if (!prodType) {
47 |
48 | throw AppInfoError.ParamError('prodType 参数错误');
49 | }
50 |
51 | const pageNo = parseInt(ctx.query.pageNo);
52 | if (!pageNo) {
53 |
54 | throw AppInfoError.ParamError('pageNo 参数错误');
55 | }
56 |
57 | var pageSize = parseInt(ctx.query.pageSize);
58 | if (!pageSize || pageSize === 0) {
59 |
60 | pageSize = 10;
61 | }
62 |
63 | const envType = parseInt(ctx.query.envType);
64 | if (!envType) {
65 |
66 | throw AppInfoError.ParamError('envType 参数错误');
67 | }
68 |
69 |
70 | ctx.body = await appInfoService.listSpecificProd(prodType, envType, platform, pageNo, pageSize);
71 |
72 |
73 | };
74 |
75 | let deleteApp = async (ctx, next) => {
76 |
77 | const itemId = ctx.query.itemId;
78 | if (!itemId) {
79 |
80 | throw AppInfoError.ParamError('itemId 参数错误');
81 | }
82 |
83 | await appInfoService.deleteApp(itemId);
84 |
85 | ctx.body = {};
86 |
87 | };
88 |
89 | let uploadApp = async (ctx, next) => {
90 |
91 | const fields = ctx.request.body.fields;
92 |
93 | const envType = parseInt(fields.envType);
94 | if (isNaN(envType) || envType > 5) {
95 |
96 | throw AppInfoError.ParamError('envType 参数错误');
97 | }
98 |
99 | const prodType = parseInt(fields.prodType);
100 | if (isNaN(prodType)) {
101 |
102 | throw AppInfoError.ParamError('prodType 参数错误');
103 | }
104 |
105 | const changeLog = fields.changeLog;
106 |
107 | const package = ctx.request.body.files.package;
108 |
109 | if (!package) {
110 |
111 | throw AppInfoError.ParamError('package 参数错误');
112 | }
113 |
114 | var platform;
115 |
116 | if (package.path.split('.').pop() === "ipa") {
117 |
118 | platform = 1;
119 | } else if (package.path.split('.').pop() === "apk") {
120 |
121 | platform = 0;
122 | } else {
123 |
124 | throw AppInfoError.ParamError('package 文件类型错误');
125 | }
126 |
127 | ctx.body = await appInfoService.uploadApp(platform, prodType, envType, package.path, changeLog);
128 | };
129 |
130 | let mailToQA = async (ctx, next) => {
131 |
132 |
133 | var fields = ctx.request.body.fields;
134 | if (!fields){
135 |
136 | fields = ctx.request.body;
137 | }
138 |
139 | const itemIds = fields.itemIds;
140 |
141 | if (!(itemIds instanceof Array) || itemIds.count === 0) {
142 |
143 | throw AppInfoError.ParamError('itemIds 参数错误');
144 | }
145 |
146 | const receivers = fields.receivers;
147 |
148 | if (!(receivers instanceof Array) || receivers.count === 0) {
149 |
150 | throw AppInfoError.ParamError('receivers 参数错误');
151 | }
152 |
153 | ctx.body = await mailService.mailToQA(itemIds, receivers, fields.from, fields.subject, fields.remark);
154 | };
155 |
156 | let generatePlist = async (ctx, next) => {
157 |
158 | const itemId = ctx.params.itemId;
159 |
160 | if (!itemId) {
161 |
162 | throw AppInfoError.ParamError('itemId 参数错误');
163 | }
164 |
165 | ctx.body = await appInfoService.generatePlist(itemId);
166 |
167 | };
168 |
169 | module.exports = {
170 |
171 | listAllProds,
172 | listSpecificProd,
173 | deleteApp,
174 | uploadApp,
175 | mailToQA,
176 | generatePlist
177 | };
--------------------------------------------------------------------------------
/app/db/dbHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 05/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const dbConfig = require('./../../config/envi_config').mysql;
7 | const mysql = require("mysql");
8 |
9 | const pool = mysql.createPool(dbConfig);
10 |
11 | let query = function( sql, values ) {
12 |
13 | return new Promise(( resolve, reject ) => {
14 |
15 | pool.getConnection(function(err, connection) {
16 |
17 | if (err) {
18 |
19 | reject( err );
20 | } else {
21 |
22 | connection.query(sql, values, ( err, rows) => {
23 |
24 | if ( err ) {
25 |
26 | reject( err );
27 | } else {
28 |
29 | resolve( rows );
30 | }
31 | connection.release();
32 | })
33 | }
34 | })
35 | })
36 |
37 | };
38 |
39 | let createTable = function( sql ) {
40 | return query( sql, [] )
41 | };
42 |
43 |
44 | let findDataById = function( table, id ) {
45 | let _sql = "SELECT * FROM ?? WHERE id = ? "
46 | return query( _sql, [ table, id, start, end ] )
47 | }
48 |
49 |
50 | let findDataByPage = function( table, keys, start, end ) {
51 | let _sql = "SELECT ?? FROM ?? LIMIT ? , ?"
52 | return query( _sql, [keys, table, start, end ] )
53 | }
54 |
55 |
56 | let insertData = function( table, values ) {
57 | let _sql = "INSERT INTO ?? SET ?"
58 | return query( _sql, [ table, values ] )
59 | }
60 |
61 |
62 | let updateData = function( table, values, id ) {
63 | let _sql = "UPDATE ?? SET ? WHERE id = ?"
64 | return query( _sql, [ table, values, id ] )
65 | }
66 |
67 |
68 | let deleteDataById = function( table, id ) {
69 | let _sql = "DELETE FROM ?? WHERE id = ?"
70 | return query( _sql, [ table, id ] )
71 | }
72 |
73 |
74 | let select = function( table, keys ) {
75 | let _sql = "SELECT ?? FROM ?? "
76 | return query( _sql, [ keys, table ] )
77 | }
78 |
79 | let count = function( table ) {
80 | let _sql = "SELECT COUNT(*) AS total_count FROM ?? "
81 | return query( _sql, [ table ] )
82 | }
83 |
84 | module.exports = {
85 | query,
86 | createTable,
87 | findDataById,
88 | findDataByPage,
89 | deleteDataById,
90 | insertData,
91 | updateData,
92 | select,
93 | count,
94 | }
95 |
--------------------------------------------------------------------------------
/app/error/api_error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 05/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 |
7 | /**
8 | * 自定义Api异常
9 | */
10 | class ApiError extends Error {
11 |
12 | //构造方法
13 | constructor(name,code, msg) {
14 | super();
15 |
16 | this.name = name;
17 | this.code = code;
18 | this.message = msg || '';
19 | }
20 | }
21 |
22 | module.exports = ApiError;
23 |
--------------------------------------------------------------------------------
/app/error/appInfo_error.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 07/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const ApiError = require('./api_error');
7 |
8 | const AppInfoError = {}
9 |
10 | AppInfoError.ParamError = (msg) => {
11 |
12 | return new ApiError('PARAM_ERROR', -101, msg);
13 | };
14 |
15 | AppInfoError.AppNotFoundError = (msg) => {
16 |
17 | return new ApiError('APP_NOT_FOUND_ERROR', -111, msg);
18 | };
19 |
20 | module.exports = AppInfoError;
21 |
22 |
--------------------------------------------------------------------------------
/app/error/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 07/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 |
7 | const ApiError = require('./api_error');
8 |
9 | const AppInfoError = require('./appInfo_error');
10 |
11 |
12 | module.exports = {
13 |
14 | ApiError,
15 | AppInfoError
16 | };
17 |
--------------------------------------------------------------------------------
/app/middleWares/log_util.js:
--------------------------------------------------------------------------------
1 | const log4js = require('log4js');
2 |
3 | const log_config = require('../../config/log_config');
4 | const ApiError = require('./../error/api_error');
5 | //加载配置文件
6 | log4js.configure(log_config);
7 |
8 | const logUtil = {};
9 |
10 | const errorLogger = log4js.getLogger('errorLogger');
11 | const resLogger = log4js.getLogger('resLogger');
12 |
13 | //封装错误日志
14 | logUtil.logError = function (ctx, error, resTime) {
15 | if (ctx && error) {
16 | errorLogger.error(formatError(ctx, error, resTime));
17 | }
18 | };
19 |
20 | //封装响应日志
21 | logUtil.logResponse = function (ctx, resTime) {
22 | if (ctx) {
23 | resLogger.info(formatRes(ctx, resTime));
24 | }
25 | };
26 |
27 | //格式化响应日志
28 | const formatRes = function (ctx, resTime) {
29 | var logText = '';
30 |
31 | //响应日志开始
32 | logText += "\n" + "*************** response log start ***************" + "\n";
33 |
34 | //添加请求日志
35 | logText += formatReqLog(ctx.request, resTime);
36 |
37 | //响应状态码
38 | logText += "response status: " + ctx.status + "\n";
39 |
40 | //响应内容
41 | logText += "response body: " + "\n" + JSON.stringify(ctx.body) + "\n";
42 |
43 | //响应日志结束
44 | logText += "*************** response log end ***************" + "\n";
45 |
46 | console.log(logText);
47 |
48 | return logText;
49 |
50 | }
51 |
52 | //格式化错误日志
53 | const formatError = function (ctx, err, resTime) {
54 | var logText = '';
55 |
56 | //错误信息开始
57 | logText += "\n" + "*************** error log start ***************" + "\n";
58 |
59 | //添加请求日志
60 | logText += formatReqLog(ctx.request, resTime);
61 |
62 | //错误名称
63 | logText += "err name: " + err.name + "\n";
64 | //错误信息
65 | logText += "err message: " + err.message + "\n";
66 | //错误详情
67 | logText += "err stack: " + err.stack + "\n";
68 |
69 | //错误信息结束
70 | logText += "*************** error log end ***************" + "\n";
71 |
72 | console.log(logText);
73 |
74 | return logText;
75 | };
76 |
77 | //格式化请求日志
78 | const formatReqLog = function (req, resTime) {
79 |
80 | var logText = '';
81 |
82 | const method = req.method;
83 | //访问方法
84 | logText += "request method: " + method + "\n";
85 |
86 | //请求原始地址
87 | logText += "request originalUrl: " + req.originalUrl + "\n";
88 |
89 | //客户端ip
90 | logText += "request client ip: " + req.ip + "\n";
91 |
92 | //开始时间
93 | var startTime;
94 |
95 | //请求参数
96 | if (method === 'GET') {
97 | logText += "request query: " + JSON.stringify(req.query) + "\n";
98 | } else {
99 |
100 | logText += "request body: " + "\n" + JSON.stringify(req.body) + "\n";
101 | }
102 |
103 | logText += 'request header: ' + JSON.stringify(req.header) + "\n";
104 |
105 | //服务器响应时间
106 | logText += "response time: " + resTime + "\n";
107 |
108 | return logText;
109 | };
110 |
111 |
112 | module.exports = () => {
113 | return async (ctx, next) => {
114 | //响应开始时间
115 | const start = new Date();
116 | //响应间隔时间
117 | var ms;
118 | try {
119 | //开始进入到下一个中间件
120 | await next();
121 |
122 | ms = new Date() - start;
123 | //记录响应日志
124 | logUtil.logResponse(ctx, ms);
125 |
126 | } catch (error) {
127 |
128 | ms = new Date() - start;
129 | //记录异常日志
130 | logUtil.logError(ctx, error, ms);
131 |
132 | console.log(error);
133 | }
134 |
135 | }
136 | };
137 |
--------------------------------------------------------------------------------
/app/middleWares/response_formatter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 在app.use(router)之前调用
3 | */
4 |
5 | var {ApiError, AppInfoError} = require('../error/index');
6 |
7 | /**
8 | * 在app.use(router)之前调用
9 | */
10 | var response_formatter = (ctx) => {
11 |
12 | //如果有返回数据,将返回数据添加到data中
13 | if (ctx.body) {
14 | ctx.body = {
15 | code: 1,
16 | msg: 'success',
17 | data: ctx.body
18 | }
19 | } else {
20 | ctx.body = {
21 | code: 1,
22 | msg: 'success'
23 | }
24 | }
25 | }
26 |
27 | var url_filter = (pattern) => {
28 |
29 | return async (ctx, next) => {
30 |
31 | const reg = new RegExp(pattern);
32 |
33 | try {
34 | //先去执行路由
35 | await next();
36 | } catch (error) {
37 |
38 | //如果异常类型是API异常并且通过正则验证的url,将错误信息添加到响应体中返回。
39 | if (error instanceof ApiError) {
40 |
41 | ctx.status = 200;
42 | ctx.body = {
43 |
44 | code: error.code,
45 | msg: error.message
46 | };
47 |
48 | } else {
49 |
50 | ctx.status = 200;
51 | ctx.body = {
52 |
53 | code: -1000,
54 | msg: '服务器内部错误'
55 | };
56 | }
57 |
58 |
59 | //继续抛,让外层中间件处理日志
60 | throw error;
61 | }
62 |
63 | //通过正则的url进行格式化处理
64 | if (reg.test(ctx.originalUrl) && ctx.status === 200) {
65 |
66 | response_formatter(ctx);
67 | }
68 | }
69 | }
70 |
71 | // module.exports = response_formatter;
72 | module.exports = url_filter;
--------------------------------------------------------------------------------
/app/model/appInfo_model.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 07/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 |
7 | const dbHelper = require('./../db/dbHelper');
8 |
9 |
10 | module.exports = {
11 |
12 | async listAllProds(platform, pageNo, pageSize){
13 |
14 | return await dbHelper.query(
15 | 'SELECT * FROM app_info WHERE updatedDate in (SELECT MAX(updatedDate) FROM app_info i WHERE i.platform = ? group by prodType) group by prodType order by updatedDate desc limit ?, ?',
16 | [platform, (pageNo - 1) * pageSize, pageSize]);
17 | },
18 |
19 | async listSpecificProd(prodType, envType, platform, pageNo, pageSize){
20 |
21 | return await dbHelper.query(
22 | "select * from app_info where prodType = ? and envType = ? and platform = ? order by updatedDate desc limit ?, ?",
23 | [prodType, envType, platform, (pageNo - 1) * pageSize, pageSize]);
24 |
25 | },
26 |
27 | async listProdArchivePackage(prodType, platform, pageNo, pageSize){
28 |
29 | return await dbHelper.query(
30 | "SELECT * FROM app_info WHERE updatedDate in (SELECT MAX(updatedDate) " +
31 | "FROM app_info i WHERE i.platform = ? and i.prodType = ? group by i.version, i.envType)" +
32 | " ORDER BY version DESC, envType ASC limit ?, ?",
33 | [ platform, prodType, (pageNo - 1) * pageSize, pageSize]
34 | );
35 |
36 | },
37 |
38 | async insertAppInfoToDB(itemInfo) {
39 |
40 | return await dbHelper.query(
41 | "INSERT INTO app_info " +
42 | "(itemId, prodType, envType, fileSize, platform, buildVersion, displayName, version, appIdentifier, changeLog, updatedDate, createdDate) VALUES" +
43 | " (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
44 | [itemInfo.itemId, itemInfo.prodType, itemInfo.envType, itemInfo.fileSize, itemInfo.platform,
45 | itemInfo.buildVersion, itemInfo.displayName, itemInfo.version, itemInfo.appIdentifier, itemInfo.changeLog,
46 | itemInfo.updatedDate, itemInfo.createdDate]
47 | );
48 | },
49 |
50 | async deleteApp(itemId){
51 |
52 | return await dbHelper.query("delete from app_info where itemId = ? ", [itemId]);
53 | },
54 |
55 | async retrieveApps(itemId){
56 |
57 | var itemIds;
58 |
59 | if (itemId instanceof Array) {
60 |
61 | itemIds = itemId;
62 | } else {
63 |
64 | itemIds = [itemId];
65 | }
66 |
67 | var sql = "select * from app_info where itemId in (";
68 |
69 | for (var i = 0; i < itemIds.length; i++) {
70 |
71 | if (i !== 0) {
72 |
73 | sql = sql + ",";
74 | }
75 |
76 | sql = sql + "?";
77 | }
78 |
79 | sql = sql + ");";
80 |
81 | return await dbHelper.query(sql, itemIds);
82 |
83 | }
84 |
85 | };
86 |
--------------------------------------------------------------------------------
/app/routes/appInfo_router.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 07/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const router = require('koa-router')();
7 |
8 | const appInfoController = require('../controllers/appInfo_controller');
9 |
10 | router.get('/listAllProds', appInfoController.listAllProds);
11 |
12 | router.get('/listSpecificProd', appInfoController.listSpecificProd);
13 |
14 | router.delete('/delete', appInfoController.deleteApp);
15 |
16 | router.post('/upload', appInfoController.uploadApp);
17 |
18 | router.post('/emailqa', appInfoController.mailToQA);
19 |
20 | module.exports = router;
21 |
--------------------------------------------------------------------------------
/app/routes/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 整合所有子路由
3 | */
4 | const router = require('koa-router')();
5 | const appInfo = require('./appInfo_router');
6 | const plistRouter = require('./plist_router');
7 |
8 | router.use('/apiv1/app', appInfo.routes(), appInfo.allowedMethods());
9 | router.use('/plist', plistRouter.routes(), plistRouter.allowedMethods());
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/app/routes/plist_router.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 09/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 |
7 | const router = require('koa-router')();
8 |
9 | const appInfoController = require('../controllers/appInfo_controller');
10 |
11 | router.get('/:itemId', appInfoController.generatePlist);
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/app/service/appInfo_service.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 07/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const fs = require('fs-extra');
7 | const mustache = require('mustache');
8 |
9 | const extractIPA = require('ipa-extract-info');//获取ipa信息
10 | const path = require('path');
11 | const AdmZip = require('adm-zip');
12 | const uuidV4 = require('uuid/v4');
13 |
14 | const apkParser3 = require("apk-parser3");
15 |
16 | require('shelljs/global');
17 |
18 | const enviConfig = require('./../../config/envi_config');
19 |
20 | const pushNotiService = require('./pushNoti_service');
21 |
22 | const appInfoModel = require('./../model/appInfo_model')
23 |
24 | const AppInfoError = require('./../error/appInfo_error');
25 |
26 |
27 | String.prototype.format = function () {
28 | var args = arguments;
29 | return this.replace(/\{(\d+)\}/g, function (s, i) {
30 | return args[i];
31 | });
32 | };
33 |
34 |
35 | let listAllProds = async (platform, pageNo, pageSize) => {
36 |
37 | const result = await appInfoModel.listAllProds(platform, pageNo, pageSize);
38 | return mapIconAndUrl(result);
39 | };
40 |
41 | let listSpecificProd = async (prodType, envType, platform, pageNo, pageSize) => {
42 |
43 | var result;
44 | if (envType === 4) {
45 | result = await appInfoModel.listProdArchivePackage(prodType, platform, pageNo, pageSize);
46 | } else {
47 | result = await appInfoModel.listSpecificProd(prodType, envType, platform, pageNo, pageSize);
48 | }
49 |
50 | return mapIconAndUrl(result);
51 | };
52 |
53 | let deleteApp = async (itemId) => {
54 |
55 | // 1. 删除文件 2. 删除数据库记录
56 |
57 | const appItems = await appInfoModel.retrieveApps(itemId);
58 |
59 | const appItem = appItems[0];
60 |
61 | if (!appItem) {
62 |
63 | throw AppInfoError.AppNotFoundError('itemId app不存在');
64 | }
65 |
66 | var appFileName = '';
67 | if (appItem.platform === 1) {
68 |
69 | appFileName = enviConfig.serverDir + '/ipa/' + appItem.itemId + '.ipa';
70 | } else {
71 |
72 | appFileName = enviConfig.serverDir + '/apk/' + appItem.itemId + '.apk';
73 | }
74 |
75 | const iconName = enviConfig.serverDir + '/icon/' + appItem.itemId + '.png';
76 |
77 | // remove file
78 | await new Promise((resolve, reject) => {
79 |
80 | fs.remove(appFileName, err => {
81 |
82 | if (err) {
83 |
84 | reject(err);
85 |
86 | } else {
87 |
88 | resolve();
89 | }
90 |
91 |
92 | })
93 |
94 | }).catch(function (err) {
95 |
96 | throw err;
97 | });
98 |
99 | await new Promise((resolve, reject) => {
100 |
101 | fs.remove(iconName, err => {
102 |
103 | if (err) {
104 |
105 | reject(err);
106 |
107 | } else {
108 |
109 | resolve();
110 | }
111 |
112 |
113 | })
114 |
115 | }).catch(function (err) {
116 | throw err;
117 | });
118 |
119 | await appInfoModel.deleteApp(itemId);
120 | };
121 |
122 | let uploadApp = async (platform, prodType, envType, packagePath, changeLog) => {
123 |
124 | var itemInfo = {};
125 | itemInfo.changeLog = changeLog;
126 | itemInfo.itemId = uuidV4();
127 | itemInfo.prodType = prodType;
128 | itemInfo.envType = envType;
129 | itemInfo.createdDate = new Date();
130 | itemInfo.updatedDate = new Date();
131 |
132 | var parsePackage, extractIcon;
133 |
134 | if (platform === 1) {
135 |
136 | parsePackage = parseIpa;
137 | extractIcon = extractIpaIcon;
138 | } else {
139 |
140 | parsePackage = parseApk;
141 | extractIcon = extractApkIcon;
142 | }
143 | /*
144 |
145 | 1. parse Package
146 | 2. parse icon
147 | 3. store package
148 | 4. save to db
149 | 5. post notification
150 |
151 | */
152 |
153 | //1.
154 | itemInfo = await parsePackage(packagePath, itemInfo);
155 |
156 | //2.
157 | await extractIcon(packagePath, itemInfo);
158 |
159 | //3.
160 | await storeApp(packagePath, itemInfo);
161 |
162 | //4.
163 | await appInfoModel.insertAppInfoToDB(itemInfo);
164 |
165 | //5. notification 异步调用,不管推送是否成功
166 | pushNotiService.pushAppUploadSuccess(itemInfo);
167 |
168 | return mapIconAndUrl(itemInfo);
169 |
170 | };
171 |
172 |
173 | let generatePlist = async (itemId) => {
174 |
175 | const items = await appInfoModel.retrieveApps(itemId);
176 |
177 | if (items.length === 0) {
178 | throw AppInfoError.AppNotFoundError(itemId + '未找到');
179 | }
180 |
181 | const item = items[0];
182 |
183 |
184 | return await new Promise((resolve, reject) => {
185 |
186 | fs.readFile(path.join(__dirname, '../supportingFiles') + '/template.plist', function (err, data) {
187 | if (err) reject(err);
188 |
189 | var template = data.toString();
190 | const rendered = mustache.render(template, {
191 | itemId: itemId,
192 | name: item.displayName,
193 | bundleID: item.appIdentifier,
194 | basePath: enviConfig.hostURL,
195 | });
196 |
197 | resolve(rendered);
198 | })
199 | });
200 |
201 | };
202 |
203 |
204 | function storeApp(fileName, itemInfo) {
205 |
206 | return new Promise((resolve, reject) => {
207 |
208 | var new_path;
209 |
210 | const serverDir = enviConfig.serverDir;
211 | const ipasDir = serverDir + "/ipa";
212 | const apksDir = serverDir + "/apk";
213 | if (itemInfo.platform === 1) {
214 |
215 | new_path = path.join(ipasDir, itemInfo.itemId + ".ipa");
216 | } else {
217 |
218 | new_path = path.join(apksDir, itemInfo.itemId + ".apk");
219 | }
220 |
221 |
222 | itemInfo.fileSize = fs.statSync(fileName).size;
223 | fs.move(fileName, new_path, error => {
224 |
225 | if (error) {
226 |
227 | reject(error);
228 | } else {
229 |
230 | resolve(itemInfo);
231 | }
232 | });
233 | });
234 | }
235 |
236 | function parseIpa(filename, itemInfo) {
237 | return new Promise(function (resolve, reject) {
238 |
239 | const fd = fs.openSync(filename, 'r');
240 | extractIPA(fd, function (err, info, raw) {
241 |
242 | if (err) {
243 |
244 | printLog('extracIPA error' + err);
245 |
246 | reject(err);
247 | }
248 |
249 | var data = info[0];
250 | itemInfo.platform = 1;
251 |
252 | itemInfo.buildVersion = data.CFBundleVersion;
253 | itemInfo.displayName = data.CFBundleDisplayName;
254 | itemInfo.version = data.CFBundleShortVersionString;
255 | itemInfo.appIdentifier = data.CFBundleIdentifier;
256 |
257 | resolve(itemInfo)
258 | });
259 | });
260 | }
261 |
262 | function parseApk(filename, itemInfo) {
263 | return new Promise(function (resolve, reject) {
264 |
265 | apkParser3(filename, function (err, data) {
266 | var package = parseText(data.package);
267 |
268 | itemInfo.platform = 2;
269 |
270 | itemInfo.buildVersion = package.versionCode;
271 | itemInfo.displayName = data["application-label"].replace(/'/g, "");
272 | itemInfo.version = package.versionName;
273 | itemInfo.appIdentifier = package.name;
274 |
275 | resolve(itemInfo)
276 | });
277 | });
278 | }
279 |
280 | function parseText(text) {
281 | var regx = /(\w+)='([\w\.\d]+)'/g
282 | var match = null, result = {}
283 | while (match = regx.exec(text)) {
284 | result[match[1]] = match[2]
285 | }
286 | return result
287 | }
288 |
289 | function extractApkIcon(filename, itemInfo) {
290 |
291 | var itemId = itemInfo.itemId;
292 |
293 | return new Promise(function (resolve, reject) {
294 | apkParser3(filename, function (err, data) {
295 | var iconPath = false;
296 | [640, 320, 240, 160].every(i => {
297 | if (typeof data["application-icon-" + i] !== 'undefined') {
298 | iconPath = data["application-icon-" + i];
299 | return false;
300 | }
301 | return true;
302 | });
303 | if (!iconPath) {
304 | reject("can not find icon ");
305 | }
306 |
307 | iconPath = iconPath.replace(/'/g, "")
308 | var tmpOut = enviConfig.serverDir + '/icon' + "/{0}.png".format(itemId)
309 | var zip = new AdmZip(filename);
310 | var ipaEntries = zip.getEntries();
311 | var found = false
312 | ipaEntries.forEach(function (ipaEntry) {
313 | if (ipaEntry.entryName.indexOf(iconPath) != -1) {
314 | var buffer = new Buffer(ipaEntry.getData());
315 | if (buffer.length) {
316 | found = true
317 | fs.writeFile(tmpOut, buffer, function (err) {
318 | if (err) {
319 | reject(err)
320 | }
321 | resolve(itemInfo)
322 | })
323 | }
324 | }
325 | })
326 | if (!found) {
327 | reject("can not find icon ")
328 | }
329 | });
330 | })
331 | }
332 |
333 | function extractIpaIcon(filename, itemInfo) {
334 | var itemId = itemInfo.itemId;
335 | return new Promise(function (resolve, reject) {
336 |
337 | var tmpOut = enviConfig.serverDir + '/icon' + "/{0}.png".format(itemId)
338 | var zip = new AdmZip(filename);
339 | var ipaEntries = zip.getEntries();
340 | var exeName = '';
341 | if (process.platform == 'darwin') {
342 | exeName = '../supportingFiles/pngdefry-osx';
343 | } else {
344 | exeName = '../supportingFiles/pngdefry-linux';
345 | }
346 |
347 | var found = false;
348 | ipaEntries.forEach(function (ipaEntry) {
349 | if (ipaEntry.entryName.indexOf('AppIcon60x60@2x.png') != -1) {
350 | found = true;
351 | var buffer = new Buffer(ipaEntry.getData());
352 | if (buffer.length) {
353 | fs.writeFile(tmpOut, buffer, function (err) {
354 | if (err) {
355 | reject(err)
356 | } else {
357 | var execResult = exec(path.join(__dirname, exeName + ' -s _tmp ') + ' ' + tmpOut)
358 | if (execResult.stdout.indexOf('not an -iphone crushed PNG file') != -1) {
359 | resolve(itemInfo)
360 | } else {
361 | fs.remove(tmpOut, function (err) {
362 | if (err) {
363 | reject(err)
364 | } else {
365 | var tmp_path = enviConfig.serverDir + '/icon' + "/{0}_tmp.png".format(itemId)
366 | fs.rename(tmp_path, tmpOut, function (err) {
367 | if (err) {
368 | reject(err)
369 | } else {
370 | resolve(itemInfo)
371 | }
372 | })
373 | }
374 | })
375 | }
376 | }
377 | })
378 | }
379 | }
380 | })
381 | if (!found) {
382 | reject("can not find icon ")
383 | }
384 | })
385 | }
386 |
387 | function mapIconAndUrl(result) {
388 |
389 | var dataList;
390 |
391 | if (result instanceof Array) {
392 |
393 | dataList = result;
394 | } else {
395 |
396 | dataList = [result];
397 | }
398 |
399 | dataList.map(function (item) {
400 |
401 | item.iconUrl = "{0}/icon/{1}.png".format(enviConfig.hostURL, item.itemId);
402 |
403 | if (item.platform === 1) {
404 |
405 | item.downloadUrl = "itms-services://?action=download-manifest&url={0}/plist/{1}".format(enviConfig.hostURL, item.itemId);
406 | } else if (item.platform === 2) {
407 |
408 | item.downloadUrl = "{0}/apk/{1}.apk".format(enviConfig.hostURL, item.itemId);
409 | }
410 |
411 | return item;
412 | });
413 |
414 | return result;
415 | }
416 |
417 |
418 | module.exports = {
419 |
420 | listAllProds,
421 | listSpecificProd,
422 | deleteApp,
423 | uploadApp,
424 | generatePlist
425 | };
--------------------------------------------------------------------------------
/app/service/email_service.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 09/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const nodemailer = require('nodemailer');
7 | const enviConfig = require('./../../config/envi_config');
8 | const appInfoModel = require('./../model/appInfo_model');
9 | const AppInfoError = require('./../error/appInfo_error');
10 |
11 | String.prototype.format = function () {
12 | var args = arguments;
13 | return this.replace(/\{(\d+)\}/g, function (s, i) {
14 | return args[i];
15 | });
16 | };
17 |
18 | let mailToQA = async (itemIds, receivers, from, subject, remark) => {
19 |
20 | const mailTransport = nodemailer.createTransport(enviConfig.email);
21 |
22 | const items = await appInfoModel.retrieveApps(itemIds);
23 |
24 | if (items.length === 0) {
25 |
26 | throw AppInfoError.AppNotFoundError('App 未找到');
27 | }
28 | const item = items[0];
29 |
30 | const platform = item.platform === 1 ? 'iOS' : 'Android';
31 | const appName = item.displayName;
32 | const appVersion = item.version;
33 |
34 | const subjectStr = "[" + appName + "] (" + platform + ") " + appVersion + "版本 归档包下载";
35 |
36 | var row = '';
37 |
38 | for (var i in items) {
39 |
40 | var itemInfo = items[i];
41 |
42 |
43 | var envType = "";
44 | switch (itemInfo.envType) {
45 | case 1:
46 | envType = "SIT";
47 | break;
48 | case 2:
49 | envType = "UAT";
50 | break;
51 | case 3:
52 | envType = "PRO";
53 | break;
54 | }
55 |
56 | const packDir = itemInfo.platform === 1 ? "/ipa/" : "/apk/";
57 | const fileExtName = itemInfo.platform === 1 ? ".ipa" : ".apk";
58 |
59 | const appFileName = enviConfig.hostURL + packDir + itemInfo.itemId + fileExtName;
60 |
61 |
62 | row +=
63 | '
' +
64 | '' + platform + ' | ' +
65 | '' + itemInfo.displayName + ' | ' +
66 | '' + itemInfo.version + ' | ' +
67 | '' + itemInfo.buildVersion + ' | ' +
68 | '' + envType + ' | ' +
69 | '点我下载 | ' +
70 | '
';
71 | }
72 |
73 | //TODO: need optimize, should provide html template.
74 | const html = '' +
75 | ' 您要归档包已备好:
平台 | 名称 | 版本 | 编译版本号 | 环境 | 下载 |
' +
76 | row + '
Have a nice day :)
';
77 |
78 |
79 | const options = {
80 | from: "Ttel-移动端测试分发平台<" + enviConfig.email.auth.user + ">",
81 | to: receivers.join(','),
82 | subject: subjectStr,
83 | html: html
84 | };
85 | return await new Promise((resolve, reject) => {
86 |
87 | mailTransport.sendMail(options, function (err, msg) {
88 |
89 | if (err) {
90 |
91 | reject({note: '发送失败'});
92 | }
93 | else {
94 | resolve({note: '发送成功'});
95 |
96 | }
97 | });
98 | });
99 | };
100 |
101 | module.exports = {
102 |
103 | mailToQA
104 | };
--------------------------------------------------------------------------------
/app/service/pushNoti_service.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 09/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const enviConfig = require('./../../config/envi_config');
7 |
8 | const JPush = require("jpush-sdk")
9 | const client = JPush.buildClient(enviConfig.JPush.appKey, enviConfig.JPush.masterSecret);
10 |
11 |
12 | let pushAppUploadSuccess = async (itemInfo) => {
13 |
14 | var envType = "";
15 | switch (itemInfo.envType) {
16 | case 1:
17 | envType = "SIT";
18 | break;
19 | case 2:
20 | envType = "UAT";
21 | break;
22 | case 3:
23 | envType = "PRO";
24 | break;
25 | }
26 |
27 | const pushClient = client.push().setAudience(JPush.tag(enviConfig.env === 'dev' ? 'sit' : 'pro'));
28 |
29 | if (itemInfo.platform === 1) {//如果是iOS平台
30 |
31 | pushClient.setPlatform("ios");
32 | if (itemInfo.aldSendProNoti == undefined) {
33 | /**
34 | setOptions 设置 options,本方法接收 5 个参数,sendno(int), time_to_live(int), override_msg_id(int), apns_production(boolean), big_push_duration(int)。
35 | */
36 | console.log("apns_production: true")
37 | pushClient.setOptions(null, null, null, true)
38 | } else {
39 | console.log("apns_production: false")
40 | pushClient.setOptions(null, null, null, false)
41 |
42 | }
43 |
44 | if (itemInfo.aldSendProNoti == undefined) {
45 |
46 | itemInfo.aldSendProNoti = false;
47 | //这里触发一次递归调用
48 | await pushAppUploadSuccess(itemInfo);//发送Dev通知
49 | }
50 |
51 | } else {//如果是Android平台
52 |
53 | pushClient.setPlatform("android");
54 | }
55 |
56 | const msg = itemInfo.displayName + " " + envType + "包已更新\n版本: v" + itemInfo.version + "(" + itemInfo.buildVersion + ")";
57 |
58 | pushClient.setNotification('Ttel', JPush.ios(msg, "mid123", "+1"), JPush.android(msg, null, 1))
59 |
60 | .send(function (err, res) {
61 | if (err) {
62 | if (err instanceof JPush.APIConnectionError) {
63 | console.log(err.message)
64 | // Response Timeout means your request to the server may have already received,
65 | // please check whether or not to push
66 | console.log(err.isResponseTimeout)
67 | } else if (err instanceof JPush.APIRequestError) {
68 | console.log(err.message)
69 | }
70 | } else {
71 | console.log('push successfully~ Msg_id:' + res.msg_id);
72 | }
73 | })
74 | }
75 |
76 | module.exports = {pushAppUploadSuccess};
--------------------------------------------------------------------------------
/app/supportingFiles/pngdefry-linux:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ttel/Ttel-Server/29bc4217c1801cd21f84e17fa3b353a38eac4351/app/supportingFiles/pngdefry-linux
--------------------------------------------------------------------------------
/app/supportingFiles/pngdefry-osx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ttel/Ttel-Server/29bc4217c1801cd21f84e17fa3b353a38eac4351/app/supportingFiles/pngdefry-osx
--------------------------------------------------------------------------------
/app/supportingFiles/template.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | items
6 |
7 |
8 | assets
9 |
10 |
11 | kind
12 | software-package
13 | url
14 | {{{basePath}}}/ipa/{{itemId}}.ipa
15 |
16 |
17 | metadata
18 |
19 | bundle-identifier
20 | {{bundleID}}
21 | bundle-version
22 | 1.0
23 | kind
24 | software
25 | title
26 | {{name}}
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/bin/init/cer/generate-certificate.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/bash
2 |
3 | ip=$1
4 | cerDir=$2/
5 | echo IP: $ip
6 | echo Cer dir: $cerDir
7 |
8 | # get rid of output
9 | blackhole="/dev/null"
10 |
11 | # generate Private Key
12 | openssl genrsa -out "$cerDir"rsa_pri.key 2048 2> $blackhole
13 |
14 | openssl req -x509 -nodes -sha256 -new -key "$cerDir"rsa_pri.key -out "$cerDir"selfSigned_pubCA.cer -days 730 -subj /CN="ttel-server "$ip" Custom CA" 2> $blackhole
15 |
16 | # certification authority
17 | openssl genrsa -out "$cerDir"server.key 2048 2> $blackhole
18 |
19 | # new certificate signing request
20 | openssl req -new -out "$cerDir"server.req -key "$cerDir"server.key -subj /CN=$ip 2> $blackhole
21 |
22 | openssl x509 -req -in "$cerDir"server.req -out "$cerDir"server.cer -CAkey "$cerDir"rsa_pri.key -CA "$cerDir"selfSigned_pubCA.cer -days 365 -CAcreateserial -CAserial "$cerDir"serial 2> $blackhole
23 |
24 | #make an new dir named pubCer
25 | mkdir "$cerDir"pubCer
26 |
27 | #move .cer
28 | mv "$cerDir"selfSigned_pubCA.cer "$cerDir"pubCer/selfSigned_pubCA.cer
29 |
--------------------------------------------------------------------------------
/bin/init/prepareCerNDir.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 09/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 |
7 | require('shelljs/global');
8 | const fs = require('fs-extra');
9 | const os = require('os');
10 | const underscore = require('underscore');
11 | const path = require('path');
12 | const enviConfig = require('../../config/envi_config');
13 | const logConfig = require('../../config/log_config');
14 |
15 | function createDirIfNeeded(path) {
16 | if (!fs.existsSync(path)) {
17 | fs.mkdirSync(path, function (err) {
18 | if (err) {
19 | console.log(err);
20 | }
21 | });
22 | }
23 | }
24 |
25 | function getHostIPAddress() {
26 |
27 | return underscore.chain(os.networkInterfaces())
28 | .values().flatten().find(function (iface) {
29 | return iface.family === 'IPv4' && iface.internal === false;
30 | }).value().address;
31 | }
32 |
33 | /**
34 | * 初始化log相关目录
35 | */
36 | function initLogPath() {
37 | //创建log的根目录'logs'
38 | if (logConfig.baseLogPath) {
39 | createDirIfNeeded(logConfig.baseLogPath)
40 | //根据不同的logType创建不同的文件目录
41 | for (var i = 0, len = logConfig.appenders.length; i < len; i++) {
42 | if (logConfig.appenders[i].path) {
43 | createDirIfNeeded(logConfig.baseLogPath + logConfig.appenders[i].path);
44 | }
45 | }
46 | }
47 | }
48 |
49 |
50 | const serverDir = enviConfig.serverDir;
51 | const cerDir = serverDir + '/cer';
52 | const ipaDir = serverDir + '/ipa';
53 | const apkDir = serverDir + '/apk';
54 | const iconDir = serverDir + '/icon';
55 |
56 | createDirIfNeeded(serverDir);
57 | createDirIfNeeded(cerDir);
58 | createDirIfNeeded(ipaDir);
59 | createDirIfNeeded(apkDir);
60 | createDirIfNeeded(iconDir);
61 |
62 | initLogPath()
63 |
64 | exec('sh ' + path.join(__dirname, './cer/generate-certificate.sh') +
65 | ' ' + getHostIPAddress() + ' ' + enviConfig.serverDir + '/cer').output;
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/bin/init/setupDatabase.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 05/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const fs = require('fs-extra');
7 | const dbHelper = require('./../../app/db/dbHelper');
8 |
9 | /**
10 | * 获取sql目录下的文件目录数据
11 | * @return {object}
12 | */
13 | function getSqlMap() {
14 |
15 | let basePath = __dirname
16 |
17 | basePath = basePath.replace(/\\/g, '\/')
18 |
19 | let pathArr = basePath.split('\/')
20 | pathArr = pathArr.splice(0, pathArr.length - 1)
21 | basePath = pathArr.join('/') + '/init/sql/'
22 |
23 |
24 | let fileList = walkFile(basePath, 'sql')
25 |
26 |
27 | return fileList
28 | }
29 |
30 | /**
31 | * 遍历目录下的文件目录
32 | * @param {string} pathResolve 需进行遍历的目录路径
33 | * @param {string} mime 遍历文件的后缀名
34 | * @return {object} 返回遍历后的目录结果
35 | */
36 | function walkFile(pathResolve, mime) {
37 |
38 | let files = fs.readdirSync(pathResolve)
39 |
40 | let fileList = {}
41 |
42 | for (let [i, item] of files.entries()) {
43 | let itemArr = item.split('\.')
44 |
45 | let itemMime = ( itemArr.length > 1 ) ? itemArr[itemArr.length - 1] : 'undefined'
46 | let keyName = item + ''
47 | if (mime === itemMime) {
48 | fileList[item] = pathResolve + item
49 | }
50 | }
51 |
52 | return fileList
53 | }
54 |
55 | /**
56 | * 读取sql文件内容
57 | * @param {string} fileName 文件名称
58 | * @param {string} path 文件所在的路径
59 | * @return {string} 脚本文件内容
60 | */
61 | function getSqlContent(fileName, path, sqlContentMap) {
62 | let content = fs.readFileSync(path, 'binary')
63 | sqlContentMap[fileName] = content
64 | }
65 |
66 | /**
67 | * 封装所有sql文件脚本内容
68 | * @return {object}
69 | */
70 | function getSqlContentMap() {
71 |
72 | var sqlContentMap = {};
73 |
74 | let sqlMap = getSqlMap()
75 | for (let key in sqlMap) {
76 | getSqlContent(key, sqlMap[key], sqlContentMap)
77 | }
78 |
79 | return sqlContentMap;
80 | }
81 |
82 | // 打印脚本执行日志
83 | const eventLog = function (err, sqlFile, index) {
84 | if (err) {
85 | console.log(`[ERROR] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行失败 o(╯□╰)o !`)
86 | } else {
87 | console.log(`[SUCCESS] sql脚本文件: ${sqlFile} 第${index + 1}条脚本 执行成功 O(∩_∩)O !`)
88 | }
89 | }
90 |
91 | // 执行建表sql脚本
92 | const createAllTables = async () => {
93 |
94 | var map = getSqlContentMap();
95 | for (let key in getSqlContentMap()) {
96 |
97 | let sqlShell = map[key]
98 | let sqlShellList = sqlShell.split(';')
99 |
100 | for (let [i, shell] of sqlShellList.entries()) {
101 | if (shell.trim()) {
102 | let result = await dbHelper.query(shell)
103 | if (result.serverStatus * 1 === 2) {
104 | eventLog(null, key, i)
105 | } else {
106 | eventLog(true, key, i)
107 | }
108 | }
109 | }
110 | }
111 | console.log('sql脚本执行结束!')
112 | console.log('请按 ctrl + c 键退出!')
113 |
114 | }
115 |
116 | createAllTables();
--------------------------------------------------------------------------------
/bin/init/sql/app_info.sql:
--------------------------------------------------------------------------------
1 | --DROP TABLE IF EXISTS `app_info`;
2 | --CREATE TABLE IF NOt`app_info` (
3 | -- `itemId` varchar(256) NOT NULL COMMENT '包的ID, 每次上传的包都会生成一个唯一的ID,即使传的是同一个ipa或者apk文件',
4 | -- `prodType` int NOT NULL COMMENT '产品类型码 ',
5 | -- `envType` int NOT NULL COMMENT '包环境类型 1:SIT 2:UAT 3:PRO',
6 | -- `platform` int NOT NULL COMMENT '平台 1:iOS 2:Android',
7 | -- `fileSize` varchar(256) NOT NULL COMMENT '主键',
8 | -- `buildVersion` varchar(256) NOT NULL COMMENT '编译版本号 2017040503',
9 | -- `displayName` varchar(256) NOT NULL COMMENT '产品名称',
10 | -- `version` varchar(256) NOT NULL COMMENT '产品版本',
11 | -- `updatedDate` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
12 | -- `createdDate` datetime NOT NULL COMMENT '包上传时间',
13 | -- `appIdentifier` varchar(256) NOT NULL COMMENT 'iOS为bundleID, Android为包名',
14 | -- `changeLog` text COMMENT '更新日志'
15 | --);
16 |
17 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 | const app = require('../app');
7 | const debug = require('debug')('ttel-server:server');
8 | const https = require('https');
9 | const http = require('http');
10 | const os = require('os');
11 | const underscore = require('underscore');
12 | const fs = require('fs-extra');
13 |
14 | //引入配置文件
15 | const enviConfig = require('../config/envi_config');
16 |
17 | const ipAddr = getHostIPAddress();
18 | enviConfig.hostURL = 'https://' + ipAddr + ':' + enviConfig.port;
19 |
20 | console.log('serverDir: ' + enviConfig.serverDir);
21 | console.log('hostURL: ' + enviConfig.hostURL);
22 |
23 | /**
24 | * Create HTTPS server.
25 | */
26 | const server = https.createServer({
27 |
28 | key: fs.readFileSync(enviConfig.serverDir + '/cer/server.key', 'utf8'),
29 | cert: fs.readFileSync(enviConfig.serverDir + '/cer/server.cer', 'utf8')
30 | }, app.callback());
31 |
32 | const httpServer = http.createServer( app.callback());
33 |
34 | /**
35 | * Listen on provided port, on all network interfaces.
36 | */
37 |
38 | server.listen(normalizePort(enviConfig.port));
39 | server.on('error', onError);
40 | server.on('listening', onListening);
41 |
42 | httpServer.listen(normalizePort(enviConfig.httpPort));
43 | httpServer.on('error', onError);
44 | httpServer.on('listening', onListening);
45 | /**
46 | * Normalize a port into a number, string, or false.
47 | */
48 |
49 | function normalizePort(val) {
50 | const port = parseInt(val, 10);
51 |
52 | if (isNaN(port)) {
53 | // named pipe
54 | return val;
55 | }
56 |
57 | if (port >= 0) {
58 | // port number
59 | return port;
60 | }
61 |
62 | return false;
63 | }
64 |
65 | /**
66 | * Event listener for HTTP server "error" event.
67 | */
68 |
69 | function onError(error) {
70 | if (error.syscall !== 'listen') {
71 | throw error;
72 | }
73 |
74 | const bind = typeof port === 'string'
75 | ? 'Pipe ' + port
76 | : 'Port ' + port;
77 |
78 | // handle specific listen errors with friendly messages
79 | switch (error.code) {
80 | case 'EACCES':
81 | console.error(bind + ' requires elevated privileges');
82 | process.exit(1);
83 | break;
84 | case 'EADDRINUSE':
85 | console.error(bind + ' is already in use');
86 | process.exit(1);
87 | break;
88 | default:
89 | throw error;
90 | }
91 | }
92 |
93 | /**
94 | * Event listener for HTTP server "listening" event.
95 | */
96 |
97 | function onListening() {
98 | var addr = httpServer.address();
99 | var bind = typeof addr === 'string'
100 | ? 'pipe ' + addr
101 | : 'port ' + addr.port;
102 | debug('Listening on ' + bind);
103 | }
104 |
105 |
106 | function getHostIPAddress() {
107 |
108 | return underscore.chain(os.networkInterfaces())
109 | .values().flatten().find(function (iface) {
110 | return iface.family === 'IPv4' && iface.internal === false;
111 | }).value().address;
112 | }
113 |
114 |
--------------------------------------------------------------------------------
/config/envi_config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 05/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 |
6 | const osHomeDir = require('os-homedir')();
7 | // 开发环境的配置内容
8 | const devConfig = {
9 |
10 | env: 'dev', //环境名称
11 | port: 8873, //服务端口号
12 | httpPort: 8863, //HTTP服务端口号
13 | serverDir: osHomeDir + '/.ttel-dev-server', //数据文件地址,不要以/结尾
14 | mysql: {
15 |
16 | host: '172.16.88.230', //mysql主机地址
17 | user: 'ttel', //登录用户
18 | password: 'rjs123', //登录密码
19 | database: 'ttel-dev' //数据库
20 | },
21 |
22 | JPush: { //极光推送
23 | appKey: '',
24 | masterSecret: ''
25 | },
26 |
27 | email: {
28 | host: 'smtp.163.com', // SMTP服务器
29 | secureConnection: true, // 使用SSL方式(安全方式,防止被窃取信息)
30 | port:465,
31 | auth: {
32 | user: '',
33 | pass: ''
34 | }
35 | },
36 |
37 | hostURL: '' //** 主机URL, 用于下载地址拼接。请保持空值
38 | };
39 |
40 | // 生产环境的配置内容
41 | const proConfig = {
42 |
43 | env: 'pro', //环境名称
44 | port: 8874, //服务端口号
45 | httpPort: 8864, //HTTP服务端口号
46 |
47 | serverDir: osHomeDir + '/.ttel-pro-server', //数据文件地址,不要以/结尾
48 |
49 | mysql: {
50 |
51 | host: '172.16.88.230', //mysql主机地址
52 | user: 'ttel', //登录用户
53 | password: 'rjs123', //登录密码
54 | database: 'ttel-pro' //数据库
55 | },
56 |
57 | JPush: {
58 | appKey: '',
59 | masterSecret: ''
60 | },
61 |
62 | email: {
63 | host: 'smtp.163.com', // smtp服务器
64 | secureConnection: true, // 使用SSL方式(安全方式,防止被窃取信息)
65 | port:465,
66 | auth: {
67 | user: '',
68 | pass: ''
69 | }
70 | },
71 |
72 | hostURL: ''
73 | };
74 |
75 | //根据不同的NODE_ENV,输出不同的配置对象,默认输出dev的配置对象
76 | module.exports = {
77 |
78 | dev: devConfig,
79 | pro: proConfig
80 | }[process.env.NODE_ENV || 'dev'];
--------------------------------------------------------------------------------
/config/log_config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by PandaApe on 05/05/2017.
3 | * Email: whailong2010@gmail.com
4 | */
5 | /**
6 | * log4js 配置文件
7 | *
8 | * 日志等级由低到高
9 | * ALL TRACE DEBUG INFO WARN ERROR FATAL OFF.
10 | *
11 | * 关于log4js的appenders的配置说明
12 | * https://github.com/nomiddlename/log4js-node/wiki/Appenders
13 | */
14 |
15 | const path = require('path');
16 |
17 | const enviConfig = require('./envi_config');
18 |
19 | //日志根目录
20 | const baseLogPath = path.resolve(enviConfig.serverDir, './logs')
21 |
22 | //错误日志目录
23 | const errorPath = "/error";
24 | //错误日志文件名
25 | const errorFileName = "error";
26 | //错误日志输出完整路径
27 | const errorLogPath = baseLogPath + errorPath + "/" + errorFileName;
28 |
29 | //响应日志目录
30 | const responsePath = "/response";
31 | //响应日志文件名
32 | const responseFileName = "response";
33 | //响应日志输出完整路径
34 | const responseLogPath = baseLogPath + responsePath + "/" + responseFileName;
35 |
36 | module.exports = {
37 | "appenders":
38 | [
39 | //错误日志
40 | {
41 | "category":"errorLogger", //logger名称
42 | "type": "dateFile", //日志类型
43 | "filename": errorLogPath, //日志输出位置
44 | "alwaysIncludePattern":true, //是否总是有后缀名
45 | "pattern": "-yyyy-MM-dd.log", //后缀,每天创建一个新的日志文件
46 | "path": errorPath //自定义属性,错误日志的根目录
47 | },
48 | //响应日志
49 | {
50 | "category":"resLogger",
51 | "type": "dateFile",
52 | "filename": responseLogPath,
53 | "alwaysIncludePattern":true,
54 | "pattern": "-yyyy-MM-dd.log",
55 | "path": responsePath
56 | }
57 | ],
58 | "levels": //设置logger名称对应的的日志等级
59 | {
60 | "errorLogger":"ERROR",
61 | "resLogger":"ALL"
62 | },
63 | "baseLogPath": baseLogPath //logs根目录
64 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ttel-server",
3 | "version": "1.2.2",
4 | "private": true,
5 | "scripts": {
6 | "setupDev": "NODE_ENV=dev node bin/init/prepareCerNDir & NODE_ENV=dev node bin/init/setupDatabase ",
7 | "setupPro": "NODE_ENV=pro node bin/init/prepareCerNDir & NODE_ENV=pro node bin/init/setupDatabase ",
8 | "dev": "NODE_ENV=dev node bin/www",
9 | "prd": "NODE_ENV=pro node bin/www"
10 | },
11 | "dependencies": {
12 | "co": "^4.6.0",
13 | "debug": "^2.6.3",
14 | "koa": "^2.2.0",
15 | "koa-convert": "^1.2.0",
16 | "koa-json": "^2.0.2",
17 | "koa-logger": "^2.0.1",
18 | "koa-onerror": "^1.2.1",
19 | "koa-router": "^7.0.1",
20 | "koa-static": "^3.0.0",
21 | "koa-views": "^5.2.1",
22 | "pug": "^2.0.0-rc.1",
23 | "log4js": "^0.6.38",
24 | "mysql":"latest",
25 | "os-homedir": "^1.0.2",
26 | "apk-parser3": "^0.1.6",
27 | "commander": "^2.9.0",
28 | "fs-extra": "^0.30.0",
29 | "ipa-extract-info": "^1.2.2",
30 | "multiparty": "^4.1.2",
31 | "mustache": "^2.1.2",
32 | "shelljs": "^0.7.0",
33 | "underscore": "^1.8.3",
34 | "uuid": "^3.0.1",
35 | "request":"^2.8.0",
36 | "adm-zip": "^0.4.7",
37 | "koa-body":"^2.0.1",
38 | "nodemailer":"^4.0.1",
39 | "jpush-sdk": "*"
40 | },
41 | "devDependencies": {
42 | "nodemon": "^1.8.1"
43 | }
44 | }
--------------------------------------------------------------------------------
/pm2.json:
--------------------------------------------------------------------------------
1 | {
2 | "apps": [
3 | {
4 | "name": "ttel-pro",
5 | "script": "bin/www",
6 | "log_date_format": "YYYY-MM-DD HH:mm Z",
7 | "error_file": "logs/pro/error-error.log",
8 | "out_file": "logs/pro/node-app.stdout.log",
9 | "pid_file": "pids/pro/node-geo-api.pid",
10 | "instances": 1,
11 | "min_uptime": "200s",
12 | "max_restarts": 10,
13 | "max_memory_restart": "550M",
14 | "cron_restart": "1 0 * * *",
15 | "watch": false,
16 | "merge_logs": true,
17 | "exec_mode":"fork",
18 | "env": {
19 | "NODE_ENV": "pro"
20 | },
21 | "vizion": false
22 | },
23 | {
24 | "name": "ttel-dev",
25 | "script": "bin/www",
26 | "log_date_format": "YYYY-MM-DD HH:mm Z",
27 | "error_file": "logs/dev/error-error.log",
28 | "out_file": "logs/dev/node-app.stdout.log",
29 | "pid_file": "pids/dev/node-geo-api.pid",
30 | "instances": 1,
31 | "min_uptime": "200s",
32 | "max_restarts": 10,
33 | "max_memory_restart": "550M",
34 | "cron_restart": "1 0 * * *",
35 | "watch": false,
36 | "merge_logs": true,
37 | "exec_mode":"fork",
38 | "env": {
39 | "NODE_ENV": "dev"
40 | },
41 | "vizion": false
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/public/css/Index.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | text-align: center;
4 | background: rgba(237,244,252,0.8);
5 | }
6 |
7 | .top_title{
8 | color: rgba(40,135,240,0.9);
9 | font-size: 22px;
10 | font-weight: bold;
11 | }
12 |
13 | .qr_code_img{
14 | width: 140Px;
15 | height: 140px;
16 | }
17 |
18 | .download_btn{
19 | width: 120px;
20 | height: 30px;
21 |
22 | background: rgba(35,150,255,0.6);
23 | border-radius: 5px;
24 | border-style: none;
25 |
26 | color: white;
27 | }
28 |
--------------------------------------------------------------------------------
/public/img/index_bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ttel/Ttel-Server/29bc4217c1801cd21f84e17fa3b353a38eac4351/public/img/index_bg.jpg
--------------------------------------------------------------------------------
/public/img/qr_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ttel/Ttel-Server/29bc4217c1801cd21f84e17fa3b353a38eac4351/public/img/qr_code.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Ttel
7 |
8 |
9 |
10 |
11 |
12 | ༺ Ttel ༻
13 | APP内测分发平台
14 |
15 |
16 |
17 | 扫描二维码,并使用浏览器打开
18 |
19 |
20 |
21 |
22 |
23 |
24 | 暂未查询到相应app
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/public/js/index.js:
--------------------------------------------------------------------------------
1 | // create by weiwei
2 |
3 | var G_URL_SERVER = getHttpHead(window.location.href) + "apiv1/";
4 |
5 | var u = navigator.userAgent,
6 | app = navigator.appVersion;
7 | var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
8 | var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
9 | var downloadUrl = "";
10 |
11 | $(function() {
12 | // init
13 | // change btn style to disable
14 | document.getElementById("downloadBtn").disabled = true;
15 | document.getElementById("downloadBtn").style.backgroundColor = "gray";
16 |
17 | $('.qr_code_img').attr("src","http://qr.liantu.com/api.php?&bg=F0F6FC&text="+window.location.href);
18 |
19 | var paramStr;
20 |
21 | if(isiOS) {
22 | paramStr = "pageNo=1&pageSize=100&platform=1";
23 | $("#cerBtn").show();
24 |
25 | } else {
26 | paramStr = "pageNo=1&pageSize=100&platform=2";
27 | $("#cerBtn").hide();
28 | }
29 |
30 | // 查询所有产品最新版本 app/listAllProds
31 | startHttpRequest("get", "app/listAllProds", paramStr, function(status, message, jsonData) {
32 | console.log("success");
33 |
34 | var arrOfData = jsonData;
35 |
36 | var dictOfItem;
37 | for(var i = 0; i < arrOfData.length; i++) {
38 |
39 | if(arrOfData[i].prodType == "1031") {
40 | dictOfItem = arrOfData[i];
41 | }
42 | }
43 |
44 | console.log(dictOfItem);
45 |
46 | if(dictOfItem) {
47 |
48 | downloadUrl = dictOfItem.downloadUrl;
49 |
50 | // change btn style to enable
51 | document.getElementById("downloadBtn").disabled = false;
52 | document.getElementById("downloadBtn").style.backgroundColor = "rgba(35,150,255,0.6)"
53 | } else {
54 |
55 | document.getElementById("tip").hidden = false;
56 | }
57 | });
58 | })
59 |
60 | function download_btn_click() {
61 |
62 | window.location.href = downloadUrl;
63 | }
64 |
65 | function cerBtn_btn_click() {
66 |
67 | window.location.href = getHttpHead(window.location.href) + "cer/pubCer/selfSigned_pubCA.cer";
68 | }
69 |
70 | //网络请求入口
71 | function startHttpRequest(method, url, postData, callback) {
72 | var queryUrl = G_URL_SERVER + url;
73 |
74 | console.log(queryUrl);
75 | console.log(postData);
76 | $.ajax({
77 | contentType: "application/json; charset=utf-8",
78 | dataType: "json",
79 | url: queryUrl,
80 | type: method,
81 | data: postData,
82 | cache: false,
83 | async: true,
84 | success: function(result) {
85 | if(1 == result.code) {
86 | console.log(result);
87 | callback(result.status, result.message, result.data);
88 | } else {
89 | console.log("请求失败")
90 | }
91 | },
92 | error: function(result) {
93 | console.log(JSON.stringify(result));
94 | }
95 | });
96 | }
97 |
98 | function getHttpHead(s) {
99 | var r = new RegExp("(http.*\/\/[^\/]+\/)");
100 | var a = r.exec(s);
101 | if(a) {
102 | return a[1];
103 | }
104 | }
--------------------------------------------------------------------------------
/public/stylesheets/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 50px;
3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
4 | }
5 |
6 | a {
7 | color: #00B7FF;
8 | }
9 |
--------------------------------------------------------------------------------
/views/error.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= message
5 | h2= error.status
6 | pre #{error.stack}
7 |
--------------------------------------------------------------------------------
/views/index.pug:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= title
5 | p Welcome to #{title}
6 |
--------------------------------------------------------------------------------
/views/layout.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 | link(rel='stylesheet', href='/stylesheets/style.css')
6 | body
7 | block content
8 |
--------------------------------------------------------------------------------