├── .gitignore ├── 1571041968875-PostRefactoring.ts ├── API document └── API.md ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── public └── uploads │ ├── image │ ├── IMG1574774534983.png │ ├── IMG1574774541633.png │ ├── IMG1574774561941.png │ ├── IMG1574774815528.png │ ├── IMG1574774837702.png │ ├── IMG1574775069740.jpeg │ ├── IMG1574775080376.jpeg │ ├── IMG1574775227050.png │ ├── IMG1574775231781.png │ ├── IMG1574775238151.png │ ├── IMG1574775254251.png │ ├── IMG1574775256162.png │ ├── IMG1574775257796.png │ ├── IMG1574775375773.jpeg │ ├── IMG1574775401425.jpeg │ ├── IMG1574775510327.gif │ ├── IMG1574775535619.jpeg │ ├── IMG1574775558535.jpeg │ ├── IMG1574775629434.gif │ ├── IMG1574776142245.jpeg │ ├── IMG1574776180697.jpeg │ ├── IMG1574776400848.jpeg │ ├── IMG1574776407835.jpeg │ ├── IMG1574776425240.jpeg │ ├── IMG1574776440926.jpeg │ ├── IMG1574776442400.jpeg │ ├── IMG1574776443812.jpeg │ ├── IMG1574776893961.jpeg │ ├── IMG1574776903789.jpeg │ ├── IMG1574776923264.jpeg │ ├── IMG1574777267029.png │ ├── IMG1574778073476.jpeg │ ├── IMG1574778103632.jpeg │ ├── IMG1574778154464.jpeg │ ├── IMG1585366114265.jpeg │ ├── IMG1585366218773.jpeg │ ├── IMG1585366276083.jpeg │ ├── IMG1585366772829.jpeg │ ├── IMG1585366789206.jpeg │ ├── IMG1585367338663.jpeg │ ├── IMG1585367372860.jpeg │ ├── IMG1585532993213.jpeg │ ├── IMG1585533240658.jpeg │ ├── IMG1585535507988.jpeg │ └── default_avatar.jpg │ └── media │ └── MEDIA1574777220928.mp4 ├── src ├── api │ ├── category │ │ ├── controllers │ │ │ └── category.ts │ │ └── entity │ │ │ └── category.ts │ ├── post │ │ ├── controllers │ │ │ └── post.ts │ │ └── entity │ │ │ ├── comment.ts │ │ │ └── post.ts │ ├── upload │ │ ├── controllers │ │ │ └── upload.ts │ │ └── entity │ │ │ └── upload.ts │ └── user │ │ ├── controllers │ │ ├── authorization.ts │ │ ├── role.ts │ │ └── user.ts │ │ └── entity │ │ ├── role.ts │ │ └── user.ts ├── app.ts ├── config │ ├── constant.ts │ └── database │ │ └── connection.ts ├── middleware │ ├── authorize.ts │ └── request.ts ├── server.ts └── utils │ └── response.ts ├── temp ├── sqlite.db ├── sqlite.db.bak └── sqlite.db.sql ├── test └── user.test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # API keys and secrets 3 | .env 4 | 5 | # Dependency directory 6 | node_modules 7 | 8 | # Editors 9 | .idea 10 | *.iml 11 | 12 | # OS metadata 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Ignore built ts files 17 | dist/**/* 18 | 19 | # ignore yarn.lock 20 | yarn.lock 21 | yarn-error.lock 22 | 23 | # typeorm 24 | ormlogs.log 25 | -------------------------------------------------------------------------------- /1571041968875-PostRefactoring.ts: -------------------------------------------------------------------------------- 1 | // 在这里写迁移sql脚本,要手动写,简直开玩笑 2 | 3 | import {MigrationInterface, QueryRunner} from "typeorm"; 4 | 5 | export class PostRefactoring1571041968875 implements MigrationInterface { 6 | 7 | public async up(queryRunner: QueryRunner): Promise { 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /API document/API.md: -------------------------------------------------------------------------------- 1 | # 新闻 API 2 | 3 | ## 统一说明 4 | 5 | 以下说明在本套接口中通用。 6 | 7 | 8 | 9 | ### 分页 10 | 11 | > URL参数 12 | 13 | | 参数 | 默认值 | 说明 | 14 | | --------- | ------ | -------- | 15 | | pageIndex | 1 | 当前页数 | 16 | | pageSize | 10 | 数据条数 | 17 | 18 | 19 | 20 | > 例子 21 | 22 | ``` 23 | http://localhost:3000/list?pageIndex=1&pageSize=10 24 | ``` 25 | 26 | 27 | 28 | ### 授权认证 29 | 30 | > headers头信息参数 31 | 32 | | 参数 | 默认值 | 说明 | 33 | | ------------- | ------ | ------------------- | 34 | | Authorization | 空 | 用户登录返回的token | 35 | 36 | 37 | 38 | ### 状态码 39 | 40 | | 状态码 | 说明 | 41 | | ------ | -------- | 42 | | 200 | 成功 | 43 | | 400 | 请求错误 | 44 | | 401 | 未授权 | 45 | | 403 | 没有权限 | 46 | 47 | 48 | 49 | ## 用户 50 | 51 | ### 登录 52 | 53 | > 接口类型:【POST】 54 | > 55 | > 接口地址: 56 | 57 | ``` 58 | /login 59 | ``` 60 | 61 | 62 | 63 | > 参数 64 | 65 | | 参数 | 默认值 | 说明 | 必传 | 66 | | -------- | ------ | ------------- | ---- | 67 | | username | 空 | 用户名 / 手机 | 是 | 68 | | password | 空 | 密码 | 是 | 69 | 70 | 71 | 72 | ### 注册 73 | 74 | > 接口类型:【POST】 75 | > 76 | > 接口地址: 77 | 78 | ``` 79 | /register 80 | ``` 81 | 82 | 83 | 84 | > 参数 85 | 86 | | 参数 | 默认值 | 说明 | 必传 | 87 | | -------- | ------ | ------------- | ---- | 88 | | username | 空 | 用户名 / 手机 | 是 | 89 | | password | 空 | 密码 | 是 | 90 | | nickname | 空 | 昵称 | 是 | 91 | 92 | 93 | 94 | ### 用户详情 95 | 96 | > 接口类型:【GET】 97 | > 98 | > 需要验证:【Authorization 】 99 | > 100 | > 接口地址: 101 | 102 | ``` 103 | /user/:id 104 | ``` 105 | 106 | 107 | 108 | > URL动态参数 109 | 110 | | 参数 | 默认值 | 说明 | 必传 | 111 | | ---- | ------ | ------ | ---- | 112 | | :id | 空 | 用户id | 是 | 113 | 114 | 115 | 116 | > 返回 117 | 118 | 用户详情对象 119 | 120 | ```js 121 | { 122 | "message": "获取成功", 123 | "data": { 124 | "id": 1, // 唯一id 125 | "username": "10086", 126 | "password": "123", 127 | "nickname": "官方认证火星网友", 128 | "head_img": "", // 头像 129 | "gender": 1, // 性别,男1,女0 130 | "post_comments": 0, // 发布评论条数 131 | "post_star": 0, // 收藏文章条数 132 | } 133 | } 134 | ``` 135 | 136 | 137 | 138 | ### 编辑用户信息 139 | 140 | > 接口类型:【POST】 141 | > 142 | > 需要验证:【Authorization 】 143 | > 144 | > 接口地址: 145 | 146 | ``` 147 | /user_update/:id 148 | ``` 149 | 150 | 151 | 152 | > 参数 153 | 154 | | 参数 | 默认值 | 必传 | 说明 | 155 | | -------- | ------ | ---- | ------ | 156 | | :id | 空 | 是 | 用户id | 157 | | username | 空 | 否 | 用户名 | 158 | | password | 空 | 否 | 密码 | 159 | | nickname | 空 | 否 | 昵称 | 160 | | head_img | 空 | 否 | 头像 | 161 | | gender | 空 | 否 | 性别 | 162 | 163 | 164 | 165 | > 返回 166 | 167 | ```js 168 | { 169 | "message": "修改成功", 170 | "data": {} 171 | } 172 | ``` 173 | 174 | 175 | 176 | ### 关注用户 177 | 178 | > 接口类型:【GET】 179 | > 180 | > 需要验证:【Authorization 】 181 | > 182 | > 接口地址: 183 | 184 | ``` 185 | /user_follows/:id 186 | ``` 187 | 188 | 189 | 190 | > 参数 191 | 192 | | 参数 | 默认值 | 必传 | 说明 | 193 | | ---- | ------ | ---- | ------------ | 194 | | :id | 空 | 是 | 关注的用户id | 195 | 196 | 197 | 198 | > 返回 199 | 200 | ```js 201 | { 202 | "message": "关注成功" 203 | } 204 | ``` 205 | 206 | 207 | 208 | ### 取消关注用户 209 | 210 | > 接口类型:【GET】 211 | > 212 | > 需要验证:【Authorization 】 213 | > 214 | > 接口地址: 215 | 216 | ``` 217 | /user_unfollow/:id 218 | ``` 219 | 220 | 221 | 222 | > 参数 223 | 224 | | 参数 | 默认值 | 必传 | 说明 | 225 | | ---- | ------ | ---- | ------------ | 226 | | :id | 空 | 是 | 关注的用户id | 227 | 228 | 229 | 230 | > 返回 231 | 232 | ```js 233 | { 234 | "message": "取消关注成功" 235 | } 236 | ``` 237 | 238 | 239 | 240 | ### 用户关注列表 241 | 242 | > 接口类型:【GET】 243 | > 244 | > 需要验证:【Authorization 】 245 | > 246 | > 接口地址: 247 | 248 | ``` 249 | /user_follows 250 | ``` 251 | 252 | 253 | 254 | > 返回 255 | 256 | ```js 257 | { 258 | "data": [ 259 | { 260 | "id": 3, 261 | "username": "100862", 262 | "password": "123", 263 | "nickname": "娱乐在线", 264 | "head_img": "/uploads/image/IMG1568705287936.jpeg" 265 | } 266 | ] 267 | } 268 | ``` 269 | 270 | 271 | 272 | ### 用户评论列表 273 | 274 | > 接口类型:【GET】 275 | > 276 | > 需要验证:【Authorization 】 277 | > 278 | > 接口地址: 279 | 280 | ``` 281 | /user_comments 282 | ``` 283 | 284 | 285 | 286 | > 返回 287 | 288 | ```js 289 | { 290 | "message": "", 291 | "data": [ 292 | "id": 1, 293 | "content": "啊信是张信哲吗?张信哲是不是的张学友弟弟?", 294 | "post": {} 295 | ] 296 | } 297 | ``` 298 | 299 | 300 | 301 | ### 收藏文章列表 302 | 303 | > 接口类型:【GET】 304 | > 305 | > 需要验证:【Authorization 】 306 | > 307 | > 接口地址: 308 | 309 | ``` 310 | /user_star 311 | ``` 312 | 313 | 314 | 315 | > 返回 316 | 317 | ```js 318 | { 319 | "message": "", 320 | "data": [ 321 | { 322 | "id": 1, 323 | "title": "阿信分享《说好不哭》幕后故事:只听一次就配唱", 324 | "content": "", 325 | "type": 1, // 文章类型 326 | "cover": [], // 封面 327 | "user": {}, // 文章作者 328 | "comments": [] // 文章评论 329 | } 330 | ] 331 | } 332 | ``` 333 | 334 | 335 | 336 | ## 新闻文章 337 | 338 | ### 文章列表 339 | 340 | > 接口类型:【GET】 341 | > 342 | > 需要验证:【Authorization 】(该接口不强制要求登录,但如果访问的是关注栏目,就必须要在头信息加上token) 343 | > 344 | > 接口地址: 345 | 346 | ``` 347 | /post 348 | ``` 349 | 350 | 351 | 352 | > 参数 353 | 354 | | 参数 | 默认值 | 必传 | 说明 | 355 | | -------- | ------ | ---- | ------ | 356 | | category | 空 | 否 | 栏目id | 357 | 358 | 359 | 360 | > 例子 361 | 362 | ``` 363 | localhost:3000/post?pageIndex=1&pageSize=2&category=8 364 | ``` 365 | 366 | 367 | 368 | > 返回 369 | 370 | ```js 371 | { 372 | "data": [] 373 | } 374 | ``` 375 | 376 | 377 | 378 | ### 搜索文章 379 | 380 | > 接口类型:【GET】 381 | > 382 | > 接口地址: 383 | 384 | ``` 385 | /post_search 386 | ``` 387 | 388 | 389 | 390 | > 参数 391 | 392 | | 参数 | 默认值 | 必传 | 说明 | 393 | | ------- | ------ | ---- | ---------- | 394 | | keyword | 空 | 否 | 搜索关键字 | 395 | 396 | 397 | 398 | > 例子 399 | 400 | ``` 401 | localhost:3000/post_search?keyword=美女&pageIndex=1&pageSize=2 402 | ``` 403 | 404 | 405 | 406 | > 返回 407 | 408 | ```js 409 | { 410 | "data": [] 411 | } 412 | ``` 413 | 414 | 415 | 416 | ### 搜索推荐 417 | 418 | > 接口类型:【GET】 419 | > 420 | > 接口地址: 421 | 422 | ``` 423 | /post_search_recommend 424 | ``` 425 | 426 | 427 | 428 | > 参数 429 | 430 | | 参数 | 默认值 | 必传 | 说明 | 431 | | ------- | ------ | ---- | ---------- | 432 | | keyword | 空 | 否 | 搜索关键字 | 433 | 434 | 435 | 436 | > 例子 437 | 438 | ``` 439 | localhost:3000/post_search_recommend?keyword=美女 440 | ``` 441 | 442 | 443 | 444 | > 返回 445 | 446 | ```js 447 | { 448 | "data": [] 449 | } 450 | ``` 451 | 452 | 453 | 454 | ### 文章详情 455 | 456 | > 接口类型:【GET】 457 | > 458 | > 接口地址: 459 | 460 | ``` 461 | /post/:id 462 | ``` 463 | 464 | 465 | 466 | > 参数 467 | 468 | | 参数 | 默认值 | 必传 | 说明 | 469 | | ---- | ------ | ---- | ------ | 470 | | :id | 空 | 是 | 文章id | 471 | 472 | 473 | 474 | > 例子 475 | 476 | ``` 477 | localhost:3000/post/1 478 | ``` 479 | 480 | 481 | 482 | > 返回 483 | 484 | ```js 485 | { 486 | "data": {} 487 | } 488 | ``` 489 | 490 | 491 | 492 | ### 评论列表 493 | 494 | > 接口类型:【GET】 495 | > 496 | > 接口地址: 497 | 498 | ``` 499 | /post_comment/:id 500 | ``` 501 | 502 | 503 | 504 | > 参数 505 | 506 | | 参数 | 默认值 | 必传 | 说明 | 507 | | ---- | ------ | ---- | ------ | 508 | | :id | 空 | 是 | 文章id | 509 | 510 | 511 | 512 | > 例子 513 | 514 | ``` 515 | localhost:3000/post_comment/1 516 | ``` 517 | 518 | 519 | 520 | > 返回 521 | 522 | ```js 523 | { 524 | "data": [] 525 | } 526 | ``` 527 | 528 | 529 | 530 | ### 发布评论 531 | 532 | > 接口类型:【POST】 533 | > 534 | > 需要验证:【Authorization 】 535 | > 536 | > 接口地址: 537 | 538 | ``` 539 | /post_comment/:id 540 | ``` 541 | 542 | 543 | 544 | > 参数 545 | 546 | | 参数 | 默认值 | 必传 | 说明 | 547 | | --------- | ------ | ---- | -------- | 548 | | :id | 空 | 是 | 文章id | 549 | | content | 空 | 是 | 评论内容 | 550 | | parent_id | 空 | 否 | 回复id | 551 | 552 | 553 | 554 | > 返回 555 | 556 | ```js 557 | { 558 | "message": "评论发布成功" 559 | } 560 | ``` 561 | 562 | 563 | 564 | ### 收藏文章 565 | 566 | > 接口类型:【GET】 567 | > 568 | > 需要验证:【Authorization 】 569 | > 570 | > 接口地址: 571 | 572 | ``` 573 | /post_star/:id 574 | ``` 575 | 576 | 577 | 578 | > 参数 579 | 580 | | 参数 | 默认值 | 必传 | 说明 | 581 | | ---- | ------ | ---- | ------ | 582 | | :id | 空 | 是 | 文章id | 583 | 584 | 585 | 586 | > 返回 587 | 588 | ```js 589 | { 590 | "message": "收藏成功" 591 | } 592 | ``` 593 | 594 | 595 | 596 | ### 点赞文章 597 | 598 | > 接口类型:【GET】 599 | > 600 | > 需要验证:【Authorization 】 601 | > 602 | > 接口地址: 603 | 604 | ``` 605 | /post_like/:id 606 | ``` 607 | 608 | 609 | 610 | > 参数 611 | 612 | | 参数 | 默认值 | 必传 | 说明 | 613 | | ---- | ------ | ---- | ------ | 614 | | :id | 空 | 是 | 文章id | 615 | 616 | 617 | 618 | > 返回 619 | 620 | ```js 621 | { 622 | "message": "点赞成功" 623 | } 624 | ``` 625 | 626 | 627 | 628 | ### 发布文章 629 | 630 | `管理后台使用` 631 | 632 | > 接口类型:【POST】 633 | > 634 | > 需要验证:【Authorization 】 635 | > 636 | > 接口地址: 637 | 638 | ``` 639 | /post 640 | ``` 641 | 642 | 643 | 644 | > 参数 645 | 646 | | 参数 | 默认值 | 必传 | 说明 | 647 | | ---------- | ------ | ---- | ---------------- | 648 | | title | 空 | 是 | 文章标题 | 649 | | content | 空 | 是 | 文章内容 | 650 | | categories | 空数组 | 是 | 所属栏目ID集合 | 651 | | cover | 空数组 | 是 | 封面图片ID集合 | 652 | | type | 1 | 是 | 1为文章,2为视频 | 653 | 654 | 655 | 656 | > 例子 657 | 658 | ```js 659 | $.ajax({ 660 | ..., 661 | data: { 662 | title: "标题", 663 | content: "内容", 664 | categories: [ 665 | {id: 1}, 666 | {id: 2} 667 | ], 668 | cover: [ 669 | {id: 8}, 670 | {id: 9}, 671 | {id: 10} 672 | ], 673 | type: 1 674 | } 675 | }) 676 | ``` 677 | 678 | 679 | 680 | > 返回 681 | 682 | ```js 683 | { 684 | "message": "文章发布成功", 685 | "data": {} 686 | } 687 | ``` 688 | 689 | 690 | 691 | ### 编辑文章 692 | 693 | `管理后台使用` 694 | 695 | > 接口类型:【POST】 696 | > 697 | > 需要验证:【Authorization 】 698 | > 699 | > 接口地址: 700 | 701 | ``` 702 | /post_update/:id 703 | ``` 704 | 705 | 706 | 707 | > 参数 708 | 709 | | 参数 | 默认值 | 必传 | 说明 | 710 | | ---------- | ------ | ---- | ---------------- | 711 | | :id | 空 | 是 | 文章 | 712 | | title | 空 | 否 | 文章标题 | 713 | | content | 空 | 否 | 文章内容 | 714 | | categories | 空数组 | 否 | 所属栏目ID集合 | 715 | | cover | 空数组 | 否 | 封面图片ID集合 | 716 | | type | 1 | 否 | 1为文章,2为视频 | 717 | | open | 1 | 否 | 1为打开,0为关闭 | 718 | 719 | 720 | 721 | > 返回 722 | 723 | ```js 724 | { 725 | "message": "文章编辑成功", 726 | } 727 | ``` 728 | 729 | 730 | 731 | ## 文件上传 732 | 733 | > 接口类型:【POST】 734 | > 735 | > 需要验证:【Authorization 】 736 | > 737 | > 接口地址: 738 | 739 | ``` 740 | /upload 741 | ``` 742 | 743 | 744 | 745 | > 参数 746 | 747 | | 参数 | 默认值 | 必传 | 说明 | 748 | | ---- | ------ | ---- | -------- | 749 | | file | 空 | 是 | 文件资源 | 750 | 751 | 752 | 753 | > 返回 754 | 755 | ```js 756 | { 757 | "message": "文件上传成功", 758 | "data": { 759 | "url": "/uploads/image/IMG1568820150584.jpeg", 760 | "uid": 11, 761 | "id": 11 762 | } 763 | } 764 | ``` 765 | 766 | 767 | 768 | ## 栏目 769 | 770 | ### 栏目列表 771 | 772 | > 接口类型:【GET】 773 | > 774 | > 验证:【Authorization 】(该接口不强制要求登录,但是对于登录的用户,加上token信息会返回关注栏目) 775 | > 776 | > 接口地址: 777 | 778 | ``` 779 | /category 780 | ``` 781 | 782 | 783 | 784 | > 字段说明 785 | 786 | | 字段名 | 说明 | 787 | | ------ | -------- | 788 | | is_top | 是否热门 | 789 | 790 | 791 | 792 | > 返回 793 | 794 | ```js 795 | { 796 | "data": [ 797 | { 798 | "id": 999, 799 | "name": "头条", 800 | "is_top": 1 801 | }, 802 | { 803 | "id": 1, 804 | "name": "热点", 805 | "is_top": 1 806 | }, 807 | { 808 | "id": 11, 809 | "name": "女人", 810 | "is_top": 0 811 | }, 812 | 813 | ] 814 | } 815 | ``` 816 | 817 | 818 | 819 | ### 添加栏目 820 | 821 | > 接口类型:【POST】 822 | > 823 | > 需要验证:【Authorization 】 824 | > 825 | > 接口地址: 826 | 827 | ``` 828 | /category 829 | ``` 830 | 831 | 832 | 833 | > 参数 834 | 835 | | 参数 | 默认值 | 必传 | 说明 | 836 | | ------ | ------ | ---- | -------- | 837 | | name | 空 | 是 | 栏目名字 | 838 | | is_top | 0 | 否 | 是否热门 | 839 | 840 | 841 | 842 | > 返回 843 | 844 | ```js 845 | { 846 | "message": "栏目添加成功", 847 | } 848 | ``` 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 新闻头条后台服务 2 | 3 | ## 启动 4 | 5 | ```shell 6 | npm install or yarn install & 7 | npm run start 8 | ``` 9 | 10 | 11 | ## 统一说明 12 | 13 | 以下说明在本套接口中通用。 14 | 15 | 16 | 17 | ### 分页 18 | 19 | > URL参数 20 | 21 | | 参数 | 默认值 | 说明 | 22 | | --------- | ------ | -------- | 23 | | pageIndex | 1 | 当前页数 | 24 | | pageSize | 10 | 数据条数 | 25 | 26 | 27 | 28 | > 例子 29 | 30 | ``` 31 | http://localhost:3000/list?pageIndex=1&pageSize=10 32 | ``` 33 | 34 | 35 | 36 | ### 授权认证 37 | 38 | > headers头信息参数 39 | 40 | | 参数 | 默认值 | 说明 | 41 | | ------------- | ------ | ------------------- | 42 | | Authorization | 空 | 用户登录返回的token | 43 | 44 | 45 | 46 | ### 状态码 47 | 48 | | 状态码 | 说明 | 49 | | ------ | -------- | 50 | | 200 | 成功 | 51 | | 400 | 请求错误 | 52 | | 401 | 未授权 | 53 | | 403 | 没有权限 | 54 | 55 | 56 | 57 | ## 用户 58 | 59 | ### 登录 60 | 61 | > 接口类型:【POST】 62 | > 63 | > 接口地址: 64 | 65 | ``` 66 | /login 67 | ``` 68 | 69 | 70 | 71 | > 参数 72 | 73 | | 参数 | 默认值 | 说明 | 必传 | 74 | | -------- | ------ | ------------- | ---- | 75 | | username | 空 | 用户名 / 手机 | 是 | 76 | | password | 空 | 密码 | 是 | 77 | 78 | 79 | 80 | ### 注册 81 | 82 | > 接口类型:【POST】 83 | > 84 | > 接口地址: 85 | 86 | ``` 87 | /register 88 | ``` 89 | 90 | 91 | 92 | > 参数 93 | 94 | | 参数 | 默认值 | 说明 | 必传 | 95 | | -------- | ------ | ------------- | ---- | 96 | | username | 空 | 用户名 / 手机 | 是 | 97 | | password | 空 | 密码 | 是 | 98 | | nickname | 空 | 昵称 | 是 | 99 | 100 | 101 | 102 | ### 用户详情 103 | 104 | > 接口类型:【GET】 105 | > 106 | > 需要验证:【Authorization 】 107 | > 108 | > 接口地址: 109 | 110 | ``` 111 | /user/:id 112 | ``` 113 | 114 | 115 | 116 | > URL动态参数 117 | 118 | | 参数 | 默认值 | 说明 | 必传 | 119 | | ---- | ------ | ------ | ---- | 120 | | :id | 空 | 用户id | 是 | 121 | 122 | 123 | 124 | > 返回 125 | 126 | 用户详情对象 127 | 128 | ```js 129 | { 130 | "message": "获取成功", 131 | "data": { 132 | "id": 1, // 唯一id 133 | "username": "10086", 134 | "password": "123", 135 | "nickname": "官方认证火星网友", 136 | "head_img": "", // 头像 137 | "gender": 1, // 性别,男1,女0 138 | "post_comments": 0, // 发布评论条数 139 | "post_star": 0, // 收藏文章条数 140 | } 141 | } 142 | ``` 143 | 144 | 145 | 146 | ### 编辑用户信息 147 | 148 | > 接口类型:【POST】 149 | > 150 | > 需要验证:【Authorization 】 151 | > 152 | > 接口地址: 153 | 154 | ``` 155 | /user_update/:id 156 | ``` 157 | 158 | 159 | 160 | > 参数 161 | 162 | | 参数 | 默认值 | 必传 | 说明 | 163 | | -------- | ------ | ---- | ------ | 164 | | :id | 空 | 是 | 用户id | 165 | | username | 空 | 否 | 用户名 | 166 | | password | 空 | 否 | 密码 | 167 | | nickname | 空 | 否 | 昵称 | 168 | | head_img | 空 | 否 | 头像 | 169 | | gender | 空 | 否 | 性别 | 170 | 171 | 172 | 173 | > 返回 174 | 175 | ```js 176 | { 177 | "message": "修改成功", 178 | "data": {} 179 | } 180 | ``` 181 | 182 | 183 | 184 | ### 关注用户 185 | 186 | > 接口类型:【GET】 187 | > 188 | > 需要验证:【Authorization 】 189 | > 190 | > 接口地址: 191 | 192 | ``` 193 | /user_follows/:id 194 | ``` 195 | 196 | 197 | 198 | > 参数 199 | 200 | | 参数 | 默认值 | 必传 | 说明 | 201 | | ---- | ------ | ---- | ------------ | 202 | | :id | 空 | 是 | 关注的用户id | 203 | 204 | 205 | 206 | > 返回 207 | 208 | ```js 209 | { 210 | "message": "关注成功" 211 | } 212 | ``` 213 | 214 | 215 | 216 | ### 取消关注用户 217 | 218 | > 接口类型:【GET】 219 | > 220 | > 需要验证:【Authorization 】 221 | > 222 | > 接口地址: 223 | 224 | ``` 225 | /user_unfollow/:id 226 | ``` 227 | 228 | 229 | 230 | > 参数 231 | 232 | | 参数 | 默认值 | 必传 | 说明 | 233 | | ---- | ------ | ---- | ------------ | 234 | | :id | 空 | 是 | 关注的用户id | 235 | 236 | 237 | 238 | > 返回 239 | 240 | ```js 241 | { 242 | "message": "取消关注成功" 243 | } 244 | ``` 245 | 246 | 247 | 248 | ### 用户关注列表 249 | 250 | > 接口类型:【GET】 251 | > 252 | > 需要验证:【Authorization 】 253 | > 254 | > 接口地址: 255 | 256 | ``` 257 | /user_follows 258 | ``` 259 | 260 | 261 | 262 | > 返回 263 | 264 | ```js 265 | { 266 | "data": [ 267 | { 268 | "id": 3, 269 | "username": "100862", 270 | "password": "123", 271 | "nickname": "娱乐在线", 272 | "head_img": "/uploads/image/IMG1568705287936.jpeg" 273 | } 274 | ] 275 | } 276 | ``` 277 | 278 | 279 | 280 | ### 用户评论列表 281 | 282 | > 接口类型:【GET】 283 | > 284 | > 需要验证:【Authorization 】 285 | > 286 | > 接口地址: 287 | 288 | ``` 289 | /user_comments 290 | ``` 291 | 292 | 293 | 294 | > 返回 295 | 296 | ```js 297 | { 298 | "message": "", 299 | "data": [ 300 | "id": 1, 301 | "content": "啊信是张信哲吗?张信哲是不是的张学友弟弟?", 302 | "post": {} 303 | ] 304 | } 305 | ``` 306 | 307 | 308 | 309 | ### 收藏文章列表 310 | 311 | > 接口类型:【GET】 312 | > 313 | > 需要验证:【Authorization 】 314 | > 315 | > 接口地址: 316 | 317 | ``` 318 | /user_star 319 | ``` 320 | 321 | 322 | 323 | > 返回 324 | 325 | ```js 326 | { 327 | "message": "", 328 | "data": [ 329 | { 330 | "id": 1, 331 | "title": "阿信分享《说好不哭》幕后故事:只听一次就配唱", 332 | "content": "", 333 | "type": 1, // 文章类型 334 | "cover": [], // 封面 335 | "user": {}, // 文章作者 336 | "comments": [] // 文章评论 337 | } 338 | ] 339 | } 340 | ``` 341 | 342 | 343 | 344 | ## 新闻文章 345 | 346 | ### 文章列表 347 | 348 | > 接口类型:【GET】 349 | > 350 | > 需要验证:【Authorization 】(该接口不强制要求登录,但如果访问的是关注栏目,就必须要在头信息加上token) 351 | > 352 | > 接口地址: 353 | 354 | ``` 355 | /post 356 | ``` 357 | 358 | 359 | 360 | > 参数 361 | 362 | | 参数 | 默认值 | 必传 | 说明 | 363 | | -------- | ------ | ---- | ------ | 364 | | category | 空 | 否 | 栏目id | 365 | 366 | 367 | 368 | > 例子 369 | 370 | ``` 371 | localhost:3000/post?pageIndex=1&pageSize=2&category=8 372 | ``` 373 | 374 | 375 | 376 | > 返回 377 | 378 | ```js 379 | { 380 | "data": [] 381 | } 382 | ``` 383 | 384 | 385 | 386 | ### 搜索文章 387 | 388 | > 接口类型:【GET】 389 | > 390 | > 接口地址: 391 | 392 | ``` 393 | /post_search 394 | ``` 395 | 396 | 397 | 398 | > 参数 399 | 400 | | 参数 | 默认值 | 必传 | 说明 | 401 | | ------- | ------ | ---- | ---------- | 402 | | keyword | 空 | 否 | 搜索关键字 | 403 | 404 | 405 | 406 | > 例子 407 | 408 | ``` 409 | localhost:3000/post_search?keyword=美女&pageIndex=1&pageSize=2 410 | ``` 411 | 412 | 413 | 414 | > 返回 415 | 416 | ```js 417 | { 418 | "data": [] 419 | } 420 | ``` 421 | 422 | 423 | 424 | ### 搜索推荐 425 | 426 | > 接口类型:【GET】 427 | > 428 | > 接口地址: 429 | 430 | ``` 431 | /post_search_recommend 432 | ``` 433 | 434 | 435 | 436 | > 参数 437 | 438 | | 参数 | 默认值 | 必传 | 说明 | 439 | | ------- | ------ | ---- | ---------- | 440 | | keyword | 空 | 否 | 搜索关键字 | 441 | 442 | 443 | 444 | > 例子 445 | 446 | ``` 447 | localhost:3000/post_search_recommend?keyword=美女 448 | ``` 449 | 450 | 451 | 452 | > 返回 453 | 454 | ```js 455 | { 456 | "data": [] 457 | } 458 | ``` 459 | 460 | 461 | 462 | ### 文章详情 463 | 464 | > 接口类型:【GET】 465 | > 466 | > 接口地址: 467 | 468 | ``` 469 | /post/:id 470 | ``` 471 | 472 | 473 | 474 | > 参数 475 | 476 | | 参数 | 默认值 | 必传 | 说明 | 477 | | ---- | ------ | ---- | ------ | 478 | | :id | 空 | 是 | 文章id | 479 | 480 | 481 | 482 | > 例子 483 | 484 | ``` 485 | localhost:3000/post/1 486 | ``` 487 | 488 | 489 | 490 | > 返回 491 | 492 | ```js 493 | { 494 | "data": {} 495 | } 496 | ``` 497 | 498 | 499 | 500 | ### 评论列表 501 | 502 | > 接口类型:【GET】 503 | > 504 | > 接口地址: 505 | 506 | ``` 507 | /post_comment/:id 508 | ``` 509 | 510 | 511 | 512 | > 参数 513 | 514 | | 参数 | 默认值 | 必传 | 说明 | 515 | | ---- | ------ | ---- | ------ | 516 | | :id | 空 | 是 | 文章id | 517 | 518 | 519 | 520 | > 例子 521 | 522 | ``` 523 | localhost:3000/post_comment/1 524 | ``` 525 | 526 | 527 | 528 | > 返回 529 | 530 | ```js 531 | { 532 | "data": [] 533 | } 534 | ``` 535 | 536 | 537 | 538 | ### 发布评论 539 | 540 | > 接口类型:【POST】 541 | > 542 | > 需要验证:【Authorization 】 543 | > 544 | > 接口地址: 545 | 546 | ``` 547 | /post_comment/:id 548 | ``` 549 | 550 | 551 | 552 | > 参数 553 | 554 | | 参数 | 默认值 | 必传 | 说明 | 555 | | --------- | ------ | ---- | -------- | 556 | | :id | 空 | 是 | 文章id | 557 | | content | 空 | 是 | 评论内容 | 558 | | parent_id | 空 | 否 | 回复id | 559 | 560 | 561 | 562 | > 返回 563 | 564 | ```js 565 | { 566 | "message": "评论发布成功" 567 | } 568 | ``` 569 | 570 | 571 | 572 | ### 收藏文章 573 | 574 | > 接口类型:【GET】 575 | > 576 | > 需要验证:【Authorization 】 577 | > 578 | > 接口地址: 579 | 580 | ``` 581 | /post_star/:id 582 | ``` 583 | 584 | 585 | 586 | > 参数 587 | 588 | | 参数 | 默认值 | 必传 | 说明 | 589 | | ---- | ------ | ---- | ------ | 590 | | :id | 空 | 是 | 文章id | 591 | 592 | 593 | 594 | > 返回 595 | 596 | ```js 597 | { 598 | "message": "收藏成功" 599 | } 600 | ``` 601 | 602 | 603 | 604 | ### 点赞文章 605 | 606 | > 接口类型:【GET】 607 | > 608 | > 需要验证:【Authorization 】 609 | > 610 | > 接口地址: 611 | 612 | ``` 613 | /post_like/:id 614 | ``` 615 | 616 | 617 | 618 | > 参数 619 | 620 | | 参数 | 默认值 | 必传 | 说明 | 621 | | ---- | ------ | ---- | ------ | 622 | | :id | 空 | 是 | 文章id | 623 | 624 | 625 | 626 | > 返回 627 | 628 | ```js 629 | { 630 | "message": "点赞成功" 631 | } 632 | ``` 633 | 634 | 635 | 636 | ### 发布文章 637 | 638 | `管理后台使用` 639 | 640 | > 接口类型:【POST】 641 | > 642 | > 需要验证:【Authorization 】 643 | > 644 | > 接口地址: 645 | 646 | ``` 647 | /post 648 | ``` 649 | 650 | 651 | 652 | > 参数 653 | 654 | | 参数 | 默认值 | 必传 | 说明 | 655 | | ---------- | ------ | ---- | ---------------- | 656 | | title | 空 | 是 | 文章标题 | 657 | | content | 空 | 是 | 文章内容 | 658 | | categories | 空数组 | 是 | 所属栏目ID集合 | 659 | | cover | 空数组 | 是 | 封面图片ID集合 | 660 | | type | 1 | 是 | 1为文章,2为视频 | 661 | 662 | 663 | 664 | > 例子 665 | 666 | ```js 667 | $.ajax({ 668 | ..., 669 | data: { 670 | title: "标题", 671 | content: "内容", 672 | categories: [ 673 | {id: 1}, 674 | {id: 2} 675 | ], 676 | cover: [ 677 | {id: 8}, 678 | {id: 9}, 679 | {id: 10} 680 | ], 681 | type: 1 682 | } 683 | }) 684 | ``` 685 | 686 | 687 | 688 | > 返回 689 | 690 | ```js 691 | { 692 | "message": "文章发布成功", 693 | "data": {} 694 | } 695 | ``` 696 | 697 | 698 | 699 | ### 编辑文章 700 | 701 | `管理后台使用` 702 | 703 | > 接口类型:【POST】 704 | > 705 | > 需要验证:【Authorization 】 706 | > 707 | > 接口地址: 708 | 709 | ``` 710 | /post_update/:id 711 | ``` 712 | 713 | 714 | 715 | > 参数 716 | 717 | | 参数 | 默认值 | 必传 | 说明 | 718 | | ---------- | ------ | ---- | ---------------- | 719 | | :id | 空 | 是 | 文章 | 720 | | title | 空 | 否 | 文章标题 | 721 | | content | 空 | 否 | 文章内容 | 722 | | categories | 空数组 | 否 | 所属栏目ID集合 | 723 | | cover | 空数组 | 否 | 封面图片ID集合 | 724 | | type | 1 | 否 | 1为文章,2为视频 | 725 | | open | 1 | 否 | 1为打开,0为关闭 | 726 | 727 | 728 | 729 | > 返回 730 | 731 | ```js 732 | { 733 | "message": "文章编辑成功", 734 | } 735 | ``` 736 | 737 | 738 | 739 | ## 文件上传 740 | 741 | > 接口类型:【POST】 742 | > 743 | > 需要验证:【Authorization 】 744 | > 745 | > 接口地址: 746 | 747 | ``` 748 | /upload 749 | ``` 750 | 751 | 752 | 753 | > 参数 754 | 755 | | 参数 | 默认值 | 必传 | 说明 | 756 | | ---- | ------ | ---- | -------- | 757 | | file | 空 | 是 | 文件资源 | 758 | 759 | 760 | 761 | > 返回 762 | 763 | ```js 764 | { 765 | "message": "文件上传成功", 766 | "data": { 767 | "url": "/uploads/image/IMG1568820150584.jpeg", 768 | "uid": 11, 769 | "id": 11 770 | } 771 | } 772 | ``` 773 | 774 | 775 | 776 | ## 栏目 777 | 778 | ### 栏目列表 779 | 780 | > 接口类型:【GET】 781 | > 782 | > 验证:【Authorization 】(该接口不强制要求登录,但是对于登录的用户,加上token信息会返回关注栏目) 783 | > 784 | > 接口地址: 785 | 786 | ``` 787 | /category 788 | ``` 789 | 790 | 791 | 792 | > 字段说明 793 | 794 | | 字段名 | 说明 | 795 | | ------ | -------- | 796 | | is_top | 是否热门 | 797 | 798 | 799 | 800 | > 返回 801 | 802 | ```js 803 | { 804 | "data": [ 805 | { 806 | "id": 999, 807 | "name": "头条", 808 | "is_top": 1 809 | }, 810 | { 811 | "id": 1, 812 | "name": "热点", 813 | "is_top": 1 814 | }, 815 | { 816 | "id": 11, 817 | "name": "女人", 818 | "is_top": 0 819 | }, 820 | 821 | ] 822 | } 823 | ``` 824 | 825 | 826 | 827 | ### 添加栏目 828 | 829 | > 接口类型:【POST】 830 | > 831 | > 需要验证:【Authorization 】 832 | > 833 | > 接口地址: 834 | 835 | ``` 836 | /category 837 | ``` 838 | 839 | 840 | 841 | > 参数 842 | 843 | | 参数 | 默认值 | 必传 | 说明 | 844 | | ------ | ------ | ---- | -------- | 845 | | name | 空 | 是 | 栏目名字 | 846 | | is_top | 0 | 否 | 是否热门 | 847 | 848 | 849 | 850 | > 返回 851 | 852 | ```js 853 | { 854 | "message": "栏目添加成功", 855 | } 856 | ``` 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | "ts-jest": { 4 | tsConfig: "tsconfig.json" 5 | } 6 | }, 7 | moduleFileExtensions: [ 8 | "ts", 9 | "js" 10 | ], 11 | transform: { 12 | "^.+\\.(ts|tsx)$": "ts-jest" 13 | }, 14 | testMatch: [ 15 | "**/test/**/*.test.(ts|js)" 16 | ], 17 | testEnvironment: "node" 18 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-node-koa", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "watch-ts": "tsc -w", 7 | "watch-node": "nodemon dist/server.js", 8 | "build-ts": "tsc", 9 | "start": "npm run build-ts && node dist/server.js", 10 | "dev": "tsc && concurrently -k -n \"TypeScript,Node\" -c \"yellow.bold,cyan.bold\" \"npm run watch-ts\" \"npm run watch-node\"", 11 | "migration": "typeorm migration:create -n PostRefactoring", 12 | "test": "jest --coverage --verbose", 13 | "watch-test": "npm run test -- --watchAll" 14 | }, 15 | "author": "", 16 | "license": "MIT", 17 | "dependencies": { 18 | "2015": "0.0.1", 19 | "@types/chai": "^4.2.2", 20 | "@types/fs-extra": "^8.0.0", 21 | "@types/jsonwebtoken": "^8.3.3", 22 | "@types/koa": "^2.0.49", 23 | "@types/koa-router": "^7.0.42", 24 | "@types/koa-static": "^4.0.1", 25 | "@types/koa2-cors": "^2.0.1", 26 | "@types/node": "^12.7.4", 27 | "@types/supertest": "^2.0.8", 28 | "colors": "^1.3.3", 29 | "fs-extra": "^8.1.0", 30 | "koa": "^2.8.1", 31 | "koa-body": "^4.1.1", 32 | "koa-jwt": "^3.6.0", 33 | "koa-router": "^7.4.0", 34 | "koa-static": "^5.0.0", 35 | "koa2-cors": "^2.0.6", 36 | "reflect-metadata": "^0.1.13", 37 | "sqlite3": "^4.1.0", 38 | "typeorm": "^0.2.19" 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^24.0.18", 42 | "chai": "^4.2.0", 43 | "concurrently": "^4.1.2", 44 | "jest": "^24.9.0", 45 | "nodemon": "^1.19.2", 46 | "supertest": "^4.0.2", 47 | "ts-jest": "^24.1.0", 48 | "ts-node": "^8.3.0", 49 | "typescript": "^3.6.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/uploads/image/IMG1574774534983.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574774534983.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574774541633.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574774541633.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574774561941.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574774561941.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574774815528.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574774815528.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574774837702.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574774837702.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775069740.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775069740.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775080376.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775080376.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775227050.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775227050.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775231781.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775231781.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775238151.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775238151.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775254251.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775254251.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775256162.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775256162.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775257796.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775257796.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775375773.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775375773.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775401425.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775401425.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775510327.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775510327.gif -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775535619.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775535619.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775558535.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775558535.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574775629434.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574775629434.gif -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776142245.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776142245.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776180697.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776180697.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776400848.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776400848.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776407835.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776407835.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776425240.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776425240.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776440926.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776440926.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776442400.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776442400.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776443812.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776443812.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776893961.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776893961.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776903789.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776903789.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574776923264.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574776923264.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574777267029.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574777267029.png -------------------------------------------------------------------------------- /public/uploads/image/IMG1574778073476.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574778073476.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574778103632.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574778103632.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1574778154464.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1574778154464.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585366114265.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585366114265.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585366218773.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585366218773.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585366276083.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585366276083.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585366772829.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585366772829.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585366789206.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585366789206.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585367338663.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585367338663.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585367372860.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585367372860.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585532993213.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585532993213.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585533240658.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585533240658.jpeg -------------------------------------------------------------------------------- /public/uploads/image/IMG1585535507988.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/IMG1585535507988.jpeg -------------------------------------------------------------------------------- /public/uploads/image/default_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/image/default_avatar.jpg -------------------------------------------------------------------------------- /public/uploads/media/MEDIA1574777220928.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/public/uploads/media/MEDIA1574777220928.mp4 -------------------------------------------------------------------------------- /src/api/category/controllers/category.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | import { getRepository } from "typeorm"; 3 | import authorize from "../../../middleware/authorize"; 4 | import { Get, Post } from "../../../middleware/request"; 5 | import Response from "../../../utils/response"; 6 | 7 | import PostEntity from "../../post/entity/post"; 8 | import Category from "../entity/category"; 9 | 10 | export default class CategoryController { 11 | 12 | @authorize({required: false}) 13 | @Get("/category") 14 | static async findCategories(ctx: BaseContext){ 15 | const cateRepository = getRepository(Category); 16 | const data:any = await cateRepository.find(); 17 | const user = ctx.state.user; 18 | 19 | data.unshift({ 20 | id: 999, 21 | name: "头条", 22 | is_top: 1 23 | }); 24 | 25 | if(user){ 26 | data.unshift({ 27 | id: 0, 28 | name: "关注", 29 | is_top: 1 30 | }); 31 | } 32 | 33 | ctx.body = { 34 | data 35 | } 36 | } 37 | 38 | @authorize() 39 | @Post("/category") 40 | static async createCategory(ctx: BaseContext){ 41 | // const postRepository = getRepository(PostEntity); 42 | const cateRepository = getRepository(Category); 43 | const params = ctx.request.body; 44 | 45 | try{ 46 | const cateToSaved = { 47 | ...params 48 | } 49 | 50 | await cateRepository.save(cateToSaved); 51 | 52 | ctx.body = { 53 | message: "栏目添加成功" 54 | } 55 | 56 | }catch(err){ 57 | console.log(err); 58 | return new Response(400, "栏目添加失败,请检查参数").toObject(ctx); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/api/category/entity/category.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, ManyToMany, JoinTable, CreateDateColumn, PrimaryGeneratedColumn} from "typeorm"; 2 | import Post from "../../post/entity/post"; 3 | 4 | @Entity() 5 | export default class Category { 6 | 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column() 11 | name: string; 12 | 13 | @Column({type: "int", default: 0}) 14 | is_top: number; 15 | 16 | @ManyToMany(type => Post, post=> post.categories) 17 | @JoinTable() 18 | posts: Post[]; 19 | 20 | @CreateDateColumn() 21 | create_date: Date; 22 | } -------------------------------------------------------------------------------- /src/api/post/controllers/post.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | import { getRepository, Like } from "typeorm"; 3 | import authorize from "../../../middleware/authorize"; 4 | import { Get, Post } from "../../../middleware/request"; 5 | import Response from "../../../utils/response"; 6 | 7 | import PostEntity from "../entity/post"; 8 | import Comment from "../entity/comment"; 9 | import User from "../../user/entity/user"; 10 | import Category from "../../category/entity/category"; 11 | 12 | const createCommentsParent = function (){ 13 | const cmtRepository = getRepository(Comment); 14 | 15 | return async function _loop(item:any){ 16 | const p = item.parent; 17 | if(p){ 18 | const parent: Comment = await cmtRepository.findOne( {id: p.id}, {relations: ["parent", "user"]}); 19 | item.parent = parent; 20 | await _loop(item.parent); 21 | } 22 | return item; 23 | } 24 | } 25 | 26 | export default class PostController { 27 | 28 | @authorize({required: false}) 29 | @Get('/post') 30 | static async findPost(ctx: BaseContext){ 31 | const postRepository = getRepository(PostEntity); 32 | let {pageIndex, pageSize, category, keyword} = ctx.request.query; 33 | const cid = +category; 34 | 35 | pageIndex = pageIndex || 1; 36 | pageSize = pageSize || 10; 37 | 38 | const start = (pageIndex - 1) * pageSize; 39 | const limit = pageIndex * pageSize; 40 | 41 | try{ 42 | 43 | let data: any = []; 44 | let total: number = 0; 45 | // 如果cid为0,表示获取关注的文章 46 | if(cid === 0 && ctx.state.user){ 47 | const uid = ctx.state.user.id; 48 | const userRepository = getRepository(User); 49 | const user = await userRepository.findOne({id: uid}, { relations: ['follows'] }); 50 | 51 | for(let i = 0, item; item = user.follows[i++];){ 52 | const res = await postRepository 53 | .createQueryBuilder('post') 54 | .where("post.open = :open", {open: 1}) 55 | .innerJoinAndSelect( 56 | 'post.user', 57 | 'user', 58 | 'user.id = :follow', 59 | { follow: item.id } 60 | ) 61 | .leftJoinAndSelect( 62 | 'post.categories', 63 | 'category' 64 | ) 65 | .leftJoinAndSelect( 66 | 'post.comments', 67 | 'comment' 68 | ) 69 | .leftJoinAndSelect( 70 | 'post.cover', 71 | 'upload' 72 | ) 73 | .orderBy("post.id", "DESC") 74 | .getMany(); 75 | 76 | data = data.concat(res); 77 | } 78 | 79 | total = data.length; 80 | data = data.slice(start, limit); 81 | }else{ 82 | const last_arg:any = cid && cid !== 999 ? { categoryId: cid } : ''; 83 | const three_arg = cid && cid !== 999 ? 'category.id = :categoryId' : ''; 84 | 85 | if(!ctx.state.postTotal || cid !== ctx.state.cid ){ 86 | // 是否有获取count的方法 , typeorm? 87 | const firstData = await postRepository 88 | .createQueryBuilder('post') 89 | .where("post.open = :open", {open: 1}) 90 | .innerJoinAndSelect( 91 | 'post.categories', 92 | 'category', 93 | three_arg, 94 | last_arg, 95 | ) 96 | .leftJoinAndSelect( 97 | 'post.user', 98 | 'user' 99 | ) 100 | .leftJoinAndSelect( 101 | 'post.comments', 102 | 'comment' 103 | ) 104 | .leftJoinAndSelect( 105 | 'post.cover', 106 | 'upload' 107 | ).getMany(); 108 | 109 | ctx.state.postTotal = firstData.length; 110 | ctx.state.cid = cid 111 | } 112 | 113 | data = await postRepository 114 | .createQueryBuilder('post') 115 | .where("post.open = :open", {open: 1}) 116 | .innerJoinAndSelect( 117 | 'post.categories', 118 | 'category', 119 | three_arg, 120 | last_arg, 121 | ) 122 | .leftJoinAndSelect( 123 | 'post.user', 124 | 'user' 125 | ) 126 | .leftJoinAndSelect( 127 | 'post.comments', 128 | 'comment' 129 | ) 130 | .leftJoinAndSelect( 131 | 'post.cover', 132 | 'upload' 133 | ) 134 | .orderBy("post.id", "DESC") 135 | .skip(start) 136 | .take(pageSize) 137 | .getMany(); 138 | } 139 | 140 | data = data.map( (v:any) => { 141 | const {comments, ...props} = v; 142 | props.comment_length = comments.length; 143 | return props; 144 | }) 145 | 146 | ctx.body = { 147 | data, 148 | total: total || ctx.state.postTotal 149 | } 150 | }catch(err){ 151 | console.log(err) 152 | return new Response(400, "文章列表获取失败").toObject(ctx); 153 | } 154 | } 155 | 156 | @Get("/post_search") 157 | static async findPostByKeyword(ctx: BaseContext){ 158 | const postRepository = getRepository(PostEntity); 159 | let {keyword, pageIndex, pageSize} = ctx.request.query; 160 | 161 | pageIndex = pageIndex || 1; 162 | pageSize = pageSize || 10; 163 | 164 | const start = (pageIndex - 1) * pageSize; 165 | 166 | try{ 167 | // 未分页,可能存在无效的文章 168 | // const data = await postRepository.find({ 169 | // title: Like(`%${keyword}%`), 170 | // }); 171 | 172 | // 获取所有,加上关联条件确保都是有效的文章,是否有count方法? 173 | let totalData = await postRepository 174 | .createQueryBuilder("post") 175 | .where("post.title like :keyword", {keyword: '%' + keyword + '%' }) 176 | .andWhere("post.open = :open", {open: 1}) 177 | .leftJoinAndSelect( 178 | 'post.user', 179 | 'user' 180 | ) 181 | .leftJoinAndSelect( 182 | 'post.comments', 183 | 'comment' 184 | ) 185 | .leftJoinAndSelect( 186 | 'post.cover', 187 | 'upload' 188 | ) 189 | .getMany(); 190 | 191 | // 分页访问。当然可以优化 192 | let data = await postRepository 193 | .createQueryBuilder("post") 194 | .where("post.title like :keyword", {keyword: '%' + keyword + '%' }) 195 | .andWhere("post.open = :open", {open: 1}) 196 | .leftJoinAndSelect( 197 | 'post.user', 198 | 'user' 199 | ) 200 | .leftJoinAndSelect( 201 | 'post.comments', 202 | 'comment' 203 | ) 204 | .leftJoinAndSelect( 205 | 'post.cover', 206 | 'upload' 207 | ) 208 | .orderBy("post.id", "DESC") 209 | .skip(start) 210 | .take(pageSize) 211 | .getMany(); 212 | 213 | data = data.map( (v:any) => { 214 | const {comments, ...props} = v; 215 | props.comment_length = comments.length; 216 | return props; 217 | }) 218 | 219 | ctx.body = { 220 | data, 221 | total: totalData.length 222 | } 223 | }catch(err){ 224 | return new Response(400, "文章列表获取失败").toObject(ctx); 225 | } 226 | } 227 | 228 | @Get("/post_search_recommend") 229 | static async findPostRecommend(ctx: BaseContext){ 230 | const postRepository = getRepository(PostEntity); 231 | let {keyword} = ctx.request.query; 232 | 233 | try{ 234 | // 未分页 235 | const data = await postRepository.find({ 236 | title: Like(`%${keyword}%`), 237 | }); 238 | 239 | ctx.body = { 240 | data 241 | } 242 | }catch(err){ 243 | return new Response(400, "文章列表获取失败").toObject(ctx); 244 | } 245 | } 246 | 247 | @authorize({required: false}) 248 | @Get('/post/:id') 249 | static async findPostById(ctx: BaseContext){ 250 | const postRepository = getRepository(PostEntity); 251 | const userRepository = getRepository(User); 252 | const id = +ctx.params.id; 253 | 254 | try{ 255 | const post = await postRepository.findOne({id}, { 256 | relations: ["like_users", "user", "comments", "cover", "categories"] 257 | }); 258 | const {comments, like_users, ...props} = post; 259 | const data: any = { 260 | ...props, 261 | has_follow: false, 262 | has_star: false, 263 | has_like: false 264 | }; 265 | 266 | // 转换成评论条数 267 | if(Array.isArray(comments)){ 268 | data.comment_length = comments.length; 269 | } 270 | 271 | // 判断当前用户是否关注该作者 272 | const user = ctx.state.user; 273 | 274 | if(user){ 275 | const uid = user.id; 276 | 277 | if(Array.isArray(like_users)){ 278 | data.has_like = like_users.some(v => { 279 | return v.id === uid; 280 | }); 281 | 282 | data.like_length = like_users.length; 283 | } 284 | 285 | const self = await userRepository.findOne({id: uid}, { 286 | relations: ["follows", "post_star"] 287 | }); 288 | 289 | data.has_star = self.post_star.some(v => { 290 | return v.id === id; 291 | }) 292 | 293 | data.has_follow = self.follows.some(v => { 294 | return v.id === post.user.id; 295 | }) 296 | } 297 | 298 | ctx.body = { 299 | message: !post ? "文章不存在" : "", 300 | data 301 | } 302 | }catch(err){ 303 | return new Response(400, "文章不存在").toObject(ctx); 304 | } 305 | } 306 | 307 | @Get("/post_comment/:id") 308 | static async getComment(ctx: BaseContext){ 309 | const postRepository = getRepository(PostEntity); 310 | const cmtRepository = getRepository(Comment); 311 | const id = +ctx.params.id; 312 | let {pageIndex, pageSize} = ctx.request.query; 313 | 314 | pageIndex = pageIndex || 1; 315 | pageSize = pageSize || 10; 316 | 317 | const start = (pageIndex - 1) * pageSize; 318 | //const limit = pageIndex * pageSize; 319 | 320 | try{ 321 | const post = await postRepository.findOne({id}); 322 | const comments = await cmtRepository.find({ 323 | relations: ["parent", "user"], 324 | where: { post}, 325 | skip: start, 326 | take: pageSize, 327 | order: { 328 | id: "DESC" 329 | } 330 | }); 331 | const commentsParent = createCommentsParent(); 332 | 333 | const data:any = []; 334 | for(let i = 0, item; item = comments[i++];){ 335 | // 楼层数据 336 | const floor = await commentsParent(item); 337 | data.push(floor); 338 | } 339 | 340 | ctx.body = { 341 | data 342 | } 343 | 344 | }catch(err){ 345 | console.log(err) 346 | return new Response(400, "获取评论失败").toObject(ctx); 347 | } 348 | } 349 | 350 | @authorize() 351 | @Post('/post_comment/:id') 352 | static async postComment(ctx: BaseContext){ 353 | const postRepository = getRepository(PostEntity); 354 | const cmtRepository = getRepository(Comment); 355 | const id = +ctx.params.id; 356 | const params = ctx.request.body; 357 | 358 | try{ 359 | const post = await postRepository.findOne({id}); 360 | 361 | if(!post){ 362 | return new Response(400, "评论失败,文章不存在").toObject(ctx); 363 | } 364 | 365 | const cmtToSaved = { 366 | ...params, 367 | post_id: id, 368 | post_title: post.title, 369 | user: ctx.state.user, 370 | post 371 | } 372 | 373 | // 回复 374 | const pid: number = +params.parent_id; 375 | if(pid){ 376 | const reply: Comment = await cmtRepository.findOne({id: pid}); 377 | 378 | if(reply){ 379 | cmtToSaved.parent = reply; 380 | } 381 | } 382 | 383 | const data = await cmtRepository.save(cmtToSaved); 384 | 385 | ctx.body = { 386 | message: "评论发布成功" 387 | } 388 | }catch(err){ 389 | console.log(err) 390 | return new Response(400, "评论失败").toObject(ctx); 391 | } 392 | } 393 | 394 | @authorize() 395 | @Get('/post_star/:id') 396 | static async postStar(ctx: BaseContext){ 397 | const postRepository = getRepository(PostEntity); 398 | const userRepository = getRepository(User); 399 | const user = ctx.state.user; 400 | const id = +ctx.params.id; 401 | 402 | try{ 403 | const post = await postRepository.findOne({id}); 404 | const self = await userRepository.findOne({ id: user.id }, { relations: ['post_star'] }); 405 | 406 | if(!post){ 407 | return new Response(400, "收藏失败,文章不存在").toObject(ctx); 408 | return; 409 | } 410 | 411 | const restPost = self.post_star.filter(v => { 412 | return v.id !== post.id; 413 | }); 414 | const isStar = restPost.length === self.post_star.length; 415 | 416 | // if(self.post_star.some(v => { 417 | // return v.id === id; 418 | // })){ 419 | // return ctx.body = { 420 | // message: "已收藏" 421 | // }; 422 | // } 423 | 424 | const userToSaved = { 425 | ...self, 426 | post_star: isStar ? [...self.post_star, post] : restPost 427 | } 428 | 429 | const data = await userRepository.save(userToSaved); 430 | 431 | ctx.body = { 432 | message: isStar ? "收藏成功" : "取消成功", 433 | } 434 | }catch(err){ 435 | console.log(err); 436 | return new Response(400, "收藏失败,文章不存在").toObject(ctx); 437 | } 438 | 439 | } 440 | 441 | @authorize() 442 | @Get('/post_like/:id') 443 | static async postLike(ctx: BaseContext){ 444 | const postRepository = getRepository(PostEntity); 445 | const user = ctx.state.user; 446 | const id = +ctx.params.id; 447 | 448 | try{ 449 | const post = await postRepository.findOne({id}, {relations: ['like_users']}); 450 | 451 | if(!post){ 452 | return new Response(400, "点赞失败,文章不存在").toObject(ctx); 453 | return; 454 | } 455 | 456 | const restUsers = post.like_users.filter(v => { 457 | return v.id !== user.id; 458 | }); 459 | const isLike = restUsers.length === post.like_users.length; 460 | 461 | // if(post.like_users.some(v => { 462 | // return v.id === user.id; 463 | // })){ 464 | // return ctx.body = { 465 | // message: "已经点赞" 466 | // }; 467 | // } 468 | 469 | const postToSaved = { 470 | ...post, 471 | like_users: isLike ? [...post.like_users, user] : restUsers 472 | } 473 | 474 | await postRepository.save(postToSaved); 475 | 476 | ctx.body = { 477 | message: isLike ? "点赞成功" : "取消成功", 478 | } 479 | }catch(err){ 480 | return new Response(400, "点赞失败").toObject(ctx); 481 | } 482 | } 483 | 484 | @authorize({isAdmin: true}) 485 | @Post('/post') 486 | static async createPost(ctx: BaseContext){ 487 | const postRepository = getRepository(PostEntity); 488 | const params = ctx.request.body; 489 | 490 | const postToSaved = { 491 | ...params, 492 | user: ctx.state.user, 493 | comments: [], 494 | like_users: [] 495 | } 496 | 497 | try{ 498 | const data = await postRepository.save(postToSaved); 499 | 500 | ctx.body = { 501 | message: "文章发布成功", 502 | data, 503 | } 504 | }catch(err){ 505 | console.log(err); 506 | return new Response(400, "文章发布失败,请检查参数").toObject(ctx); 507 | } 508 | } 509 | 510 | @authorize({isAdmin: true}) 511 | @Post("/post_update/:id") 512 | static async updatePost(ctx: BaseContext){ 513 | const postRepository = getRepository(PostEntity); 514 | // const cateRepository = getRepository(Category); 515 | const params = ctx.request.body; 516 | const id = +ctx.params.id; 517 | 518 | try{ 519 | const post = await postRepository.findOne({id}, {relations: ['like_users']}); 520 | 521 | if(!post){ 522 | return new Response(400, "编辑文章失败,文章不存在").toObject(ctx); 523 | return; 524 | } 525 | 526 | // categories是栏目id的集合 527 | //const {categories, ...props} = params; 528 | 529 | // if(categories && Array.isArray(categories)){ 530 | // props.categories = []; 531 | 532 | // for(let i = 0, cid; cid = categories[i++];){ 533 | // const c = await cateRepository.findOne({id: cid}); 534 | // props.categories.push(c); 535 | // } 536 | // } 537 | 538 | const postToSaved = { 539 | id, 540 | ...params 541 | } 542 | 543 | await postRepository.save(postToSaved); 544 | 545 | ctx.body = { 546 | message: "文章编辑成功" 547 | } 548 | 549 | }catch(err){ 550 | console.log(err); 551 | return new Response(400, "编辑文章失败,请检查参数").toObject(ctx); 552 | } 553 | } 554 | } -------------------------------------------------------------------------------- /src/api/post/entity/comment.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, JoinColumn, OneToOne, ManyToOne, CreateDateColumn, PrimaryGeneratedColumn} from "typeorm"; 2 | import User from "../../user/entity/user"; 3 | import Post from "./post"; 4 | 5 | @Entity() 6 | export default class PostComment { 7 | 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @Column() 12 | content: string; 13 | 14 | @ManyToOne(type => PostComment) 15 | parent: PostComment 16 | 17 | @ManyToOne(type => User, user => user.post_comments) 18 | user: User; 19 | 20 | @ManyToOne(type => Post, post => post.comments) 21 | post: Post; 22 | 23 | @CreateDateColumn() 24 | create_date: Date; 25 | } -------------------------------------------------------------------------------- /src/api/post/entity/post.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, CreateDateColumn, ManyToOne, OneToMany, ManyToMany, JoinTable, PrimaryGeneratedColumn} from "typeorm"; 2 | import User from "../../user/entity/user"; 3 | import Category from "../../category/entity/category"; 4 | import Upload from "../../upload/entity/upload"; 5 | import Comment from "./comment"; 6 | 7 | @Entity() 8 | export default class Post { 9 | 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column() 14 | title: string; 15 | 16 | @Column("text") 17 | content: string; 18 | 19 | @Column({type: 'int', default: 1}) 20 | type: number 21 | 22 | @Column({type: 'int', default: 1}) 23 | open: number 24 | 25 | @ManyToMany(type => Upload) 26 | @JoinTable() 27 | cover: Upload[]; 28 | 29 | @ManyToOne(type => User, user => user.posts) 30 | user: User; 31 | 32 | @OneToMany(type => Comment, comment => comment.post) 33 | comments: Comment[]; 34 | 35 | @ManyToMany(type => User, user => user.like_posts) 36 | @JoinTable() 37 | like_users: User[]; 38 | 39 | @ManyToMany(type => Category, category => category.posts) 40 | @JoinTable() 41 | categories: Category[]; 42 | 43 | @CreateDateColumn() 44 | create_date: Date; 45 | } -------------------------------------------------------------------------------- /src/api/upload/controllers/upload.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import fsa from "fs-extra"; 4 | 5 | import { BaseContext } from "koa"; 6 | import {getRepository} from "typeorm"; 7 | import Upload from "../entity/upload"; 8 | import { Post } from "../../../middleware/request"; 9 | import authorize from "../../../middleware/authorize"; 10 | import Exception from "../../../utils/response"; 11 | import { PUBLIC_PATCH } from "../../../config/constant"; 12 | 13 | export default class UploadController { 14 | 15 | @authorize() 16 | @Post('/upload') 17 | static async uploadFile(ctx: BaseContext, next: any){ 18 | const uploadRepository = getRepository(Upload); 19 | 20 | try{ 21 | const file = ctx.request.files.file 22 | const fileType = file.type.split("\/")[1] || ""; 23 | let filePath = "" 24 | 25 | if(['jpg', 'jpeg', 'jiff', 'png', 'gif'].indexOf(fileType) > -1){ 26 | fsa.ensureDirSync(path.join(PUBLIC_PATCH,`/uploads/image`)); 27 | filePath = `/uploads/image/IMG${Date.now()}.${fileType}`; 28 | }else if(['mp4', 'mp3', 'avi', 'rmvb'].indexOf(fileType) > -1){ 29 | fsa.ensureDirSync(path.join(PUBLIC_PATCH,`/uploads/media`)); 30 | filePath = `/uploads/media/MEDIA${Date.now()}.${fileType}`; 31 | }else{ 32 | return new Exception(400, '未知的文件格式').toObject(ctx); 33 | } 34 | 35 | const reader = fs.createReadStream(file.path); 36 | const stream = fs.createWriteStream(path.join( 37 | PUBLIC_PATCH, 38 | filePath 39 | )); 40 | reader.pipe(stream); 41 | 42 | const fileToSaved = { 43 | url: filePath, 44 | uid: ctx.state.user.id 45 | } 46 | const data = await uploadRepository.save(fileToSaved) 47 | 48 | ctx.body = { 49 | message: "文件上传成功", 50 | data 51 | } 52 | }catch(err){ 53 | console.log(err) 54 | return new Exception(400, '文件上传失败').toObject(ctx); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/api/upload/entity/upload.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, PrimaryGeneratedColumn, CreateDateColumn} from "typeorm"; 2 | 3 | @Entity() 4 | export default class Upload { 5 | 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | url: string; 11 | 12 | @Column() 13 | uid: number; 14 | 15 | @CreateDateColumn() 16 | create_date: Date 17 | } -------------------------------------------------------------------------------- /src/api/user/controllers/authorization.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | import { getRepository } from "typeorm"; 3 | import { sign } from "jsonwebtoken"; 4 | import { Get, Post } from "../../../middleware/request"; 5 | import Response from "../../../utils/response"; 6 | import User from "../entity/user"; 7 | import Role from "../entity/role"; 8 | import {TOKEN_SECRET} from "../../../config/constant"; 9 | 10 | export default class Authorization { 11 | 12 | @Post("/login") 13 | static async login(ctx: BaseContext){ 14 | const userRepository = getRepository(User); 15 | const {username, password} = ctx.request.body; 16 | const user:User = await userRepository.findOne({username}, { 17 | relations: ['role'] 18 | }); 19 | 20 | if(user && user.password === password){ 21 | const {password, ...profile} = user; 22 | 23 | new Response(200, "登录成功", { 24 | token: sign({...profile}, TOKEN_SECRET), 25 | user: profile 26 | }).toObject(ctx); 27 | 28 | }else{ 29 | return new Response(400, "用户或密码错误").toObject(ctx); 30 | } 31 | } 32 | 33 | @Post("/register") 34 | static async register(ctx: BaseContext){ 35 | const userRepository = getRepository(User); 36 | const roleRepository = getRepository(Role); 37 | const params = ctx.request.body; 38 | try{ 39 | const role = await roleRepository.findOne({permissions: 1}); 40 | 41 | // build up entity user to be saved 42 | const userInstance:User = new User(); 43 | const userToSaved: User = { 44 | ...userInstance, 45 | ...params, 46 | role 47 | } 48 | const username = userToSaved.username; 49 | const validResulte = User.validate(userToSaved); 50 | 51 | if(validResulte.valid === false){ 52 | return new Response(400, validResulte.err_message).toObject(ctx); 53 | } 54 | 55 | const isExsit = await userRepository.findOne({ username }); 56 | if(!isExsit){ 57 | const user = await userRepository.save(userToSaved); 58 | console.log(user) 59 | delete user.password; 60 | ctx.body = { 61 | message: "注册成功", 62 | data: user 63 | }; 64 | } else { 65 | return new Response(400, '用户名已经存在').toObject(ctx); 66 | } 67 | }catch(err){ 68 | return new Response(400, '注册失败,请检查参数', err.message).toObject(ctx); 69 | } 70 | } 71 | 72 | // @authorize({isAdmin: true}) 73 | @Get("/_users") 74 | static async getUser(ctx:any, next:any){ 75 | new Response(200, {a: 1}).toObject(ctx) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/api/user/controllers/role.ts: -------------------------------------------------------------------------------- 1 | 2 | import { BaseContext } from "koa"; 3 | import {getRepository} from "typeorm"; 4 | 5 | import { Get, Post } from "../../../middleware/request"; 6 | import authorize from "../../../middleware/authorize"; 7 | 8 | import Role from "../entity/role"; 9 | 10 | interface Permissions { 11 | nomarl: number; 12 | admin: number; 13 | } 14 | 15 | export default class RoleController { 16 | 17 | @Get("/insert_role") 18 | async insertRole(ctx: BaseContext){ 19 | const roleRepository = getRepository(Role); 20 | const data = await roleRepository.find(); 21 | 22 | if(!data || data.length === 0){ 23 | const roles: Permissions | any = { 24 | nomarl: 1, 25 | admin: 256 26 | } 27 | 28 | try{ 29 | for(let key in roles){ 30 | await roleRepository.save({ 31 | type: key, 32 | permissions: roles[key] 33 | }) 34 | } 35 | }catch(err){ 36 | console.log(err) 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/api/user/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | import {getRepository} from "typeorm"; 3 | 4 | import { Get, Post } from "../../../middleware/request"; 5 | import authorize from "../../../middleware/authorize"; 6 | import Response from "../../../utils/response"; 7 | 8 | import User from "../entity/user"; 9 | import PostComment from "../../post/entity/comment"; 10 | 11 | export default class UserController { 12 | // can use this.userDB instead of getRepository(User) without static property 13 | // https://auth0.com/blog/building-and-securing-a-koa-and-angular2-app-with-jwt/ 14 | // private userDB; 15 | 16 | @authorize() 17 | @Get("/user/:id") 18 | static async findUser(ctx: BaseContext){ 19 | const userRepository = getRepository(User); 20 | const id = +ctx.params.id; 21 | 22 | if( id !== ctx.state.user.id){ 23 | return new Response(403, "用户信息验证失败!").toObject(ctx); 24 | } 25 | 26 | try{ 27 | const user = await userRepository.findOne({ id }, { 28 | relations: ['post_comments', 'post_star', 'role', 'follows'] 29 | }); 30 | const {post_comments, post_star, follows, password, ...props } = user; 31 | const data = { 32 | ...props, 33 | post_comments: post_comments.length, 34 | post_star: post_star.length, 35 | post_follows: follows.length 36 | } 37 | 38 | ctx.body = { 39 | message: "获取成功", 40 | data 41 | }; 42 | }catch(err){ 43 | console.log(err); 44 | return new Response(401, "获取用户信息失败").toObject(ctx); 45 | } 46 | } 47 | 48 | @authorize() 49 | @Get("/user_follows/:id") 50 | static async userFollows(ctx: BaseContext){ 51 | const userRepository = getRepository(User); 52 | const user = ctx.state.user; 53 | const id = +ctx.params.id; 54 | 55 | try{ 56 | const self = await userRepository.findOne({id: user.id}, { relations: ['follows'] }); 57 | const follow = await userRepository.findOne({id}); 58 | 59 | if(!user){ 60 | return new Response(401, "关注失败,用户不存在").toObject(ctx); 61 | } 62 | 63 | const isExist = self.follows.some((v: any) => { 64 | return v.id === id; 65 | }); 66 | 67 | if(isExist){ 68 | return ctx.body = { 69 | message: "已关注" 70 | }; 71 | } 72 | 73 | const userToSaved = { 74 | ...user, 75 | follows: [ ...self.follows, follow] 76 | } 77 | 78 | await userRepository.save(userToSaved) 79 | 80 | ctx.body = { 81 | message: "关注成功" 82 | } 83 | 84 | }catch(err){ 85 | console.log(err); 86 | return new Response(401, "关注失败").toObject(ctx); 87 | } 88 | } 89 | 90 | @authorize() 91 | @Get("/user_unfollow/:id") 92 | static async userUnfollow(ctx: BaseContext){ 93 | const userRepository = getRepository(User); 94 | const user = ctx.state.user; 95 | const uid = +ctx.params.id; 96 | 97 | try{ 98 | const self = await userRepository.findOne({id: user.id}, { relations: ['follows'] }); 99 | 100 | if(!user){ 101 | return new Response(401, "关注失败,用户不存在").toObject(ctx); 102 | } 103 | 104 | const restFollows = self.follows.filter((v: any) => { 105 | return v.id !== uid; 106 | }); 107 | 108 | if(restFollows.length === self.follows.length ){ 109 | return ctx.body = { 110 | message: "未关注该用户" 111 | }; 112 | } 113 | 114 | const userToSaved = { 115 | ...self, 116 | follows: restFollows 117 | } 118 | 119 | await userRepository.save(userToSaved) 120 | 121 | ctx.body = { 122 | message: "取消关注成功" 123 | } 124 | 125 | }catch(err){ 126 | console.log(err); 127 | return new Response(401, "取消关注失败").toObject(ctx); 128 | } 129 | } 130 | 131 | @authorize() 132 | @Get("/user_follows") 133 | static async findUserFollows(ctx: BaseContext){ 134 | const userRepository = getRepository(User); 135 | const {id} = ctx.state.user; 136 | 137 | try{ 138 | const user = await userRepository.findOne({id}, { relations: ['follows'] }); 139 | const data = user.follows; 140 | 141 | ctx.body = { 142 | data 143 | } 144 | }catch(err){ 145 | console.log(err) 146 | return new Response(401, "查询错误").toObject(ctx); 147 | } 148 | } 149 | 150 | @authorize() 151 | @Get("/user_comments") 152 | static async findUserComments(ctx: BaseContext){ 153 | const cmtRepository = getRepository(PostComment); 154 | const {id} = ctx.state.user; 155 | let {pageIndex, pageSize} = ctx.request.query; 156 | 157 | pageIndex = pageIndex || 1; 158 | pageSize = pageSize || 10; 159 | 160 | const start = (pageIndex - 1) * pageSize; 161 | // const limit = pageIndex * pageSize; 162 | 163 | try{ 164 | const data = await cmtRepository.find({ 165 | relations: ['post', 'parent', 'parent.user'], 166 | where: { user: id }, 167 | skip: start, 168 | take: pageSize 169 | }) 170 | 171 | ctx.body = { 172 | data 173 | }; 174 | }catch(err){ 175 | console.log(err) 176 | return new Response(401, "查询错误").toObject(ctx); 177 | } 178 | } 179 | 180 | @authorize() 181 | @Get("/user_star") 182 | static async findUserStars(ctx: BaseContext){ 183 | const userRepository = getRepository(User); 184 | const {id} = ctx.state.user; 185 | 186 | try{ 187 | const user = await userRepository.findOne({ id }, { 188 | relations: ['post_star', 'post_star.cover', 'post_star.user', 'post_star.comments'] 189 | }); 190 | let {post_star} = user; 191 | 192 | post_star = post_star.map((v:any) => { 193 | delete v.user.password; 194 | v.comments = v.comments.length 195 | return v; 196 | }) 197 | 198 | ctx.body = { 199 | data: post_star 200 | }; 201 | }catch(err){ 202 | console.log(err); 203 | return new Response(401, "获取用户信息失败").toObject(ctx); 204 | } 205 | } 206 | 207 | @authorize() 208 | @Post("/user_update/:id") 209 | static async updateUser(ctx: BaseContext, next: any){ 210 | const userRepository = getRepository(User); 211 | const id = +ctx.params.id; 212 | 213 | if( id !== ctx.state.user.id){ 214 | return new Response(403, "用户信息验证失败!").toObject(ctx); 215 | } 216 | 217 | try{ 218 | const userUpdate: User= ctx.request.body; 219 | const userBefore = await userRepository.findOne({ id }); 220 | const userAfter = await userRepository.save({ 221 | id, 222 | ...userUpdate 223 | }); 224 | 225 | const { password, ...user } = { ...userBefore, ...userAfter }; 226 | 227 | ctx.body = { 228 | message: "修改成功", 229 | data: user 230 | }; 231 | }catch(err){ 232 | return new Response(401, "修改失败,请检查参数名称!").toObject(ctx); 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /src/api/user/entity/role.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, OneToMany, ManyToMany, JoinTable, PrimaryGeneratedColumn} from "typeorm"; 2 | import User from "./user" 3 | 4 | @Entity() 5 | export default class Role { 6 | @PrimaryGeneratedColumn() 7 | id: number; 8 | 9 | @Column() 10 | type: string; 11 | 12 | @Column({type: "int", default: 0}) 13 | permissions: number; 14 | 15 | @OneToMany(type => User, user => user.role) 16 | users: User[] 17 | } -------------------------------------------------------------------------------- /src/api/user/entity/user.ts: -------------------------------------------------------------------------------- 1 | import {Entity, Column, OneToMany, ManyToOne, ManyToMany, JoinTable, PrimaryGeneratedColumn, CreateDateColumn} from "typeorm"; 2 | import PostComment from "../../post/entity/comment"; 3 | import Post from "../../post/entity/post"; 4 | import Role from "./role" 5 | 6 | @Entity() 7 | export default class User { 8 | 9 | @PrimaryGeneratedColumn() 10 | id: number; 11 | 12 | @Column({unique: true, length: 11}) 13 | username: string; 14 | 15 | @Column({length: 18}) 16 | password: string 17 | 18 | @Column({length: 10}) 19 | nickname: string 20 | 21 | @Column({default: ""}) 22 | head_img: string 23 | 24 | @Column({type: "int", default: 1}) 25 | gender: number; 26 | 27 | @ManyToOne(type => Role, role => role.users) 28 | role: Role 29 | 30 | @ManyToMany(type => User) 31 | @JoinTable() 32 | follows: User[] 33 | 34 | @OneToMany(type => PostComment, comment => comment.user) 35 | post_comments: PostComment[] 36 | 37 | @OneToMany(type => Post, post => post.user) 38 | posts: Post[] 39 | 40 | @OneToMany(type => Post, post => post.like_users) 41 | like_posts: Post[] 42 | 43 | @ManyToMany(type => Post) 44 | @JoinTable() 45 | post_star: Post[]; 46 | 47 | @CreateDateColumn() 48 | create_date: Date 49 | 50 | constructor(){ 51 | this.head_img = "/uploads/image/default_avatar.jpg"; 52 | } 53 | 54 | static validate(user: any){ 55 | const rules: any = { 56 | 'username': { 57 | rule: /^1[0-9]{10}/, 58 | err_message: "用户名格式错误", 59 | }, 60 | 'password': { 61 | rule: /^[0-9A-Za-z_]{6,18}/, 62 | err_message: "密码格式错误", 63 | }, 64 | 'nickname': { 65 | rule: /^[0-9A-Za-z_\u4e00-\u9fa5]{2,6}/, 66 | err_message: "昵称格式错误", 67 | } 68 | } 69 | 70 | let valid = true; 71 | let err_message = "" 72 | Object.keys(rules).forEach((key: any)=> { 73 | if(!valid) return; 74 | if(!user[key]){ 75 | throw new Error(key + ' 值不能为空') 76 | } 77 | if( !(rules[key].rule.test(user[key])) ){ 78 | valid = false; 79 | err_message = rules[key].err_message 80 | } 81 | }) 82 | 83 | return { 84 | valid, 85 | err_message 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import Koa from "koa"; 2 | import koaBody from 'koa-body'; 3 | import koaStatic from "koa-static"; 4 | import cors from "koa2-cors"; 5 | 6 | import connectionDatabase from "./config/database/connection"; 7 | import {router} from "./middleware/request"; 8 | import { PUBLIC_PATCH } from "./config/constant"; 9 | 10 | const app = new Koa(); 11 | const db = connectionDatabase(); 12 | 13 | import ("./api/user/controllers/user").then(Factor => { new Factor.default() }); 14 | import ("./api/user/controllers/authorization").then(Factor => { new Factor.default() }); 15 | import ("./api/user/controllers/role").then(Factor => { new Factor.default() }); 16 | import ("./api/upload/controllers/upload").then(Factor => { new Factor.default() }); 17 | import ("./api/post/controllers/post").then(Factor => { new Factor.default() }); 18 | import ("./api/category/controllers/category").then(Factor => { new Factor.default() }); 19 | 20 | app.use(koaStatic( PUBLIC_PATCH )); 21 | app.use(koaBody({ multipart: true })); 22 | app.use(cors()); 23 | app.use(router.routes()) 24 | 25 | export default app; 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/config/constant.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export const APP_PATCH = process.cwd(); 4 | 5 | export const PORT = 3000; 6 | 7 | export const TOKEN_SECRET = "GGGLHF"; 8 | 9 | export const PUBLIC_PATCH = path.join(APP_PATCH, "./public") -------------------------------------------------------------------------------- /src/config/database/connection.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import {createConnection} from "typeorm"; 3 | 4 | export default async function connectionDatabse(){ 5 | const connection = await createConnection({ 6 | type: "sqlite", 7 | database: "./temp/sqlite.db", 8 | synchronize: true, 9 | entities: ["dist/api/**/entity/*.js"], 10 | logging: ["query", "error"], 11 | logger: "file", 12 | migrationsTableName: "migration_table", 13 | migrations: [ 14 | __dirname + "/migrations/*{.js,.ts}" 15 | ], 16 | cli: { 17 | "migrationsDir": "migration" 18 | } 19 | }) 20 | 21 | //await connection.runMigrations(); 22 | 23 | return connection; 24 | } 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/middleware/authorize.ts: -------------------------------------------------------------------------------- 1 | import {BaseContext} from "koa"; 2 | import { verify } from "jsonwebtoken"; 3 | import Response from "../utils/response"; 4 | import { TOKEN_SECRET } from "../config/constant"; 5 | 6 | interface Params { 7 | required?: Boolean; 8 | isAdmin?: Boolean; 9 | } 10 | 11 | const authorize = (params = {}) => { 12 | const {required, isAdmin} = params; 13 | 14 | return (context: any, key: string, desc: PropertyDescriptor) => { 15 | 16 | context[key].authorize = (ctx: BaseContext, next: any) => { 17 | try{ 18 | const token = ctx.request.headers['authorization']; 19 | if(required === false){ 20 | if(token){ 21 | ctx.state.user = verify(token.replace('Bearer ', ''), TOKEN_SECRET); 22 | } 23 | return next(); 24 | } 25 | 26 | // default 27 | if(required !== false){ 28 | if(token){ 29 | ctx.state.user = verify(token.replace('Bearer ', ''), TOKEN_SECRET); 30 | // 是否是管理员 31 | if(isAdmin === true && ctx.state.user.role.type !== 'admin'){ 32 | return new Response(403, "用户不是管理员").toObject(ctx); 33 | } 34 | return next(); 35 | } 36 | } 37 | 38 | return new Response(403, "用户信息验证失败").toObject(ctx); 39 | }catch(err){ 40 | console.log(err); 41 | return new Response(403, "用户信息验证失败").toObject(ctx); 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default authorize; -------------------------------------------------------------------------------- /src/middleware/request.ts: -------------------------------------------------------------------------------- 1 | import Router from "koa-router"; 2 | import {BaseContext} from "koa"; 3 | 4 | export const router = new Router(); 5 | 6 | const authMiddleware = (controller: any) => { 7 | return (ctx: BaseContext, next: any) => { 8 | const authorize = controller.authorize; 9 | 10 | // need to be verify token 11 | if(authorize && typeof authorize === 'function'){ 12 | return authorize.call(null, ctx, next) 13 | } 14 | 15 | return next(); 16 | }; 17 | } 18 | 19 | export const Get = (path: string) => { 20 | return ( 21 | context: any, 22 | key: string, 23 | desc: PropertyDescriptor 24 | ) => { 25 | const controller = context[key]; 26 | router.get(path, authMiddleware(controller), controller); 27 | } 28 | } 29 | 30 | export const Post = (path: string) => { 31 | return ( 32 | context: any, 33 | key: string, 34 | desc: PropertyDescriptor 35 | ) => { 36 | const controller = context[key]; 37 | router.post(path, authMiddleware(controller), controller); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import app from "./app"; 2 | import colors from "colors"; 3 | import {PORT} from "./config/constant"; 4 | 5 | const server = app.listen(PORT || 3000, () => { 6 | console.log(); 7 | console.log(colors.cyan(`服务器启动成功`)); 8 | console.log(colors.cyan(`服务器本地地址:http://127.0.0.1:${PORT}`)); 9 | console.log(); 10 | }); 11 | 12 | export default server; -------------------------------------------------------------------------------- /src/utils/response.ts: -------------------------------------------------------------------------------- 1 | import { BaseContext } from "koa"; 2 | 3 | interface BackRef { 4 | statusCode: number; 5 | message: string | Object; 6 | data?:Object 7 | } 8 | 9 | export default class Response { 10 | private _statusCode: BackRef['statusCode']; 11 | private _message: BackRef['message']; 12 | private _data: BackRef["data"]; 13 | 14 | constructor( 15 | statusCode: BackRef['statusCode'], 16 | message: BackRef['message'], 17 | data?: BackRef['data'] 18 | ) { 19 | 20 | this._statusCode = statusCode; 21 | const type = typeof message; 22 | 23 | if(type === 'string'){ 24 | this._message = message; 25 | }else if(type === "object"){ 26 | this._data = message; 27 | } 28 | 29 | if(data){ 30 | this._data = data; 31 | } 32 | } 33 | 34 | get statusCode(): number { 35 | return this._statusCode; 36 | } 37 | 38 | public toObject(context?: BaseContext): BackRef { 39 | // 只能返回字符串 40 | // return context.throw(new Error(this.message), 403) 41 | 42 | // 可自定义返回 43 | context.response.status = this._statusCode; 44 | return context.body = { 45 | statusCode: this._statusCode, 46 | message: this._message, 47 | data: this._data 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /temp/sqlite.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/temp/sqlite.db -------------------------------------------------------------------------------- /temp/sqlite.db.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hsian/Typescript-Node-Koa2/ad9fd84f0b5db0ebe3c405488218a969f8d6fd7e/temp/sqlite.db.bak -------------------------------------------------------------------------------- /temp/sqlite.db.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | CREATE TABLE IF NOT EXISTS "post_comment" ( 3 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 4 | "content" varchar NOT NULL, 5 | "userId" integer, 6 | "postId" integer, 7 | "parentId" integer, 8 | CONSTRAINT "FK_c7fb3b0d1192f17f7649062f672" FOREIGN KEY("postId") REFERENCES "post"("id") ON DELETE NO ACTION ON UPDATE NO ACTION, 9 | CONSTRAINT "FK_5675870bd3124aeeaa476256062" FOREIGN KEY("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION, 10 | CONSTRAINT "FK_8018bc65c89f9b88fdb38d02710" FOREIGN KEY("parentId") REFERENCES "post_comment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION 11 | ); 12 | CREATE TABLE IF NOT EXISTS "migrations" ( 13 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 14 | "timestamp" bigint NOT NULL, 15 | "name" varchar NOT NULL 16 | ); 17 | CREATE TABLE IF NOT EXISTS "user_post_star_post" ( 18 | "userId" integer NOT NULL, 19 | "postId" integer NOT NULL, 20 | CONSTRAINT "FK_c5c365c016809e8aa4cfae53f65" FOREIGN KEY("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 21 | PRIMARY KEY("userId","postId"), 22 | CONSTRAINT "FK_a62933d561c31fc90c5c2e8e7d2" FOREIGN KEY("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION 23 | ); 24 | CREATE TABLE IF NOT EXISTS "user_follows_user" ( 25 | "userId_1" integer NOT NULL, 26 | "userId_2" integer NOT NULL, 27 | CONSTRAINT "FK_911345dc417fb10f25f7644cc60" FOREIGN KEY("userId_1") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 28 | CONSTRAINT "FK_da5eb1d232421542d4fb33ac417" FOREIGN KEY("userId_2") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 29 | PRIMARY KEY("userId_1","userId_2") 30 | ); 31 | CREATE TABLE IF NOT EXISTS "post_categories_category" ( 32 | "postId" integer NOT NULL, 33 | "categoryId" integer NOT NULL, 34 | CONSTRAINT "FK_93b566d522b73cb8bc46f7405bd" FOREIGN KEY("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 35 | CONSTRAINT "FK_a5e63f80ca58e7296d5864bd2d3" FOREIGN KEY("categoryId") REFERENCES "category"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 36 | PRIMARY KEY("postId","categoryId") 37 | ); 38 | CREATE TABLE IF NOT EXISTS "post_like_users_user" ( 39 | "postId" integer NOT NULL, 40 | "userId" integer NOT NULL, 41 | CONSTRAINT "FK_e0c6eb857edb4374d07a7175a3d" FOREIGN KEY("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 42 | CONSTRAINT "FK_701dd0cfc5bc38ede4f8a11257f" FOREIGN KEY("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 43 | PRIMARY KEY("postId","userId") 44 | ); 45 | CREATE TABLE IF NOT EXISTS "post_cover_upload" ( 46 | "postId" integer NOT NULL, 47 | "uploadId" integer NOT NULL, 48 | CONSTRAINT "FK_b9a00cd8d070cce5c13d43dbf50" FOREIGN KEY("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 49 | CONSTRAINT "FK_f69ff461a7d7a83b64e5ac5189a" FOREIGN KEY("uploadId") REFERENCES "upload"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 50 | PRIMARY KEY("postId","uploadId") 51 | ); 52 | CREATE TABLE IF NOT EXISTS "category_posts_post" ( 53 | "categoryId" integer NOT NULL, 54 | "postId" integer NOT NULL, 55 | PRIMARY KEY("categoryId","postId"), 56 | CONSTRAINT "FK_3a1f3735235af2f4b702a3d3987" FOREIGN KEY("categoryId") REFERENCES "category"("id") ON DELETE CASCADE ON UPDATE NO ACTION, 57 | CONSTRAINT "FK_0cb77d79c53f0759b8153ec8a62" FOREIGN KEY("postId") REFERENCES "post"("id") ON DELETE CASCADE ON UPDATE NO ACTION 58 | ); 59 | CREATE TABLE IF NOT EXISTS "post" ( 60 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 61 | "title" varchar NOT NULL, 62 | "content" text NOT NULL, 63 | "type" integer NOT NULL DEFAULT (1), 64 | "userId" integer, 65 | CONSTRAINT "FK_5c1cf55c308037b5aca1038a131" FOREIGN KEY("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION 66 | ); 67 | CREATE TABLE IF NOT EXISTS "user" ( 68 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 69 | "username" varchar(11) NOT NULL, 70 | "password" varchar(18) NOT NULL, 71 | "nickname" varchar(10) NOT NULL, 72 | "head_img" varchar NOT NULL DEFAULT (''), 73 | "gender" integer NOT NULL DEFAULT (1), 74 | CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE("username") 75 | ); 76 | CREATE TABLE IF NOT EXISTS "upload" ( 77 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 78 | "url" varchar NOT NULL, 79 | "uid" integer NOT NULL 80 | ); 81 | CREATE TABLE IF NOT EXISTS "category" ( 82 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 83 | "name" varchar NOT NULL, 84 | "is_top" integer NOT NULL DEFAULT (0) 85 | ); 86 | INSERT INTO "user" ("id","username","password","nickname","head_img","gender") VALUES (1,'10086','123','官方认证火星网友','',1), 87 | (2,'100861','123','广州新闻','/uploads/image/IMG1568705287936.jpeg',1), 88 | (3,'100862','123','娱乐在线','/uploads/image/IMG1568705287936.jpeg',1), 89 | (4,'100863','123','新华网','/uploads/image/IMG1568705287936.jpeg',1), 90 | (5,'100864','123','火星时报','/uploads/image/IMG1568705287936.jpeg',1), 91 | (6,'100865','123','银河护卫队','/uploads/image/IMG1568705287936.jpeg',1), 92 | (7,'100866','123','鲨鱼联盟','/uploads/image/IMG1568705287936.jpeg',1), 93 | (8,'100867','123','药材铺老板','/uploads/image/IMG1568705287936.jpeg',1), 94 | (9,'100868','123','大型止血贴','/uploads/image/IMG1568705287936.jpeg',1), 95 | (10,'100869','123','三杯鸡养殖户','/uploads/image/IMG1568705287936.jpeg',1), 96 | (11,'10010','123','woshishui','',1); 97 | INSERT INTO "upload" ("id","url","uid") VALUES (1,'/uploads/image/IMG1568705287936.jpeg',1), 98 | (2,'http://cms-bucket.ws.126.net/2019/09/17/703782e03135454781ae73ef602e71ba.jpeg?imageView&thumbnail=750x0&quality=85&type=jpg&interlace=1',1), 99 | (3,'http://cms-bucket.ws.126.net/2019/09/17/176598c5fa6d4810b25299ba51d9dada.png?imageView&thumbnail=750x0&quality=85&type=png&interlace=1',1), 100 | (4,'http://cms-bucket.ws.126.net/2019/09/17/124857fc2da04deca549bdf9e1fff676.jpeg?imageView&thumbnail=750x0&quality=85&type=jpg&interlace=1',1), 101 | (5,'https://timgmb04.bdimg.com/timg?searchbox_feed&quality=100&wh_rate=0&size=b576_324&ref=http%3A%2F%2Fwww.baidu.com&sec=1568739067&di=612dd27cae470b93b01a4b32ef72fbac&src=http%3A%2F%2Fpic.rmb.bdstatic.com%2Fe18c6ffa079441431f8988ca4c3ac106.jpeg',1), 102 | (6,'http://cms-bucket.ws.126.net/2019/09/18/3020045071c34d2ea4a574bdccee7136.png?imageView&thumbnail=750x0&quality=85&type=png&interlace=1',0), 103 | (7,'https://timgmb01.bdimg.com/timg?searchbox_feed&quality=100&wh_rate=0&size=b576_324&ref=http%3A%2F%2Fwww.baidu.com&sec=1568739067&di=13e00a6373de7a1a7b0dc83df25a8289&src=http%3A%2F%2Fpic.rmb.bdstatic.com%2Fb856dcd6884d81c688088626a9b8da60.jpeg',0), 104 | (8,'https://cms-bucket.ws.126.net/2019/09/17/47eb99111b8d48bfad3c0b377e5ac58c.png?imageView&thumbnail=234y146&quality=45&interlace=1&enlarge=1&type=webp',0), 105 | (9,'https://cms-bucket.ws.126.net/2019/09/17/0d8d568a0bf94cb29201e6b4892ca653.png?imageView&thumbnail=234y146&quality=45&interlace=1&enlarge=1&type=webp',0), 106 | (10,'https://cms-bucket.ws.126.net/2019/09/17/a7ed36f9236d49919f025268f51662e3.png?imageView&thumbnail=234y146&quality=45&interlace=1&enlarge=1&type=webp',0), 107 | (11,'/uploads/image/IMG1568820150584.jpeg',11), 108 | (12,'https://cms-bucket.ws.126.net/2019/09/20/5f7cbdce73d5470aa9145cac0d146568.png?imageView&thumbnail=234y146&quality=45&interlace=1&enlarge=1&type=webp',0), 109 | (13,'http://cms-bucket.ws.126.net/2019/09/23/1129a4a737c5429d92f5d7478d19873d.jpeg?imageView&thumbnail=750x0&quality=85&type=jpg&interlace=1',0), 110 | (14,'/uploads/image/IMG1569379339406.png',11), 111 | (15,'/uploads/image/IMG1569379484583.png',11), 112 | (16,'/uploads/image/IMG1569379522459.png',11), 113 | (17,'/uploads/image/IMG1569380554436.png',11), 114 | (18,'/uploads/image/IMG1569380907715.png',11), 115 | (19,'/uploads/image/IMG1569381049794.jpeg',11), 116 | (20,'/uploads/image/IMG1569381068121.png',11), 117 | (21,'/uploads/image/IMG1569381163794.jpeg',11), 118 | (22,'/uploads/image/IMG1569382889487.png',11), 119 | (23,'/uploads/image/IMG1569393358174.jpeg',11), 120 | (24,'/uploads/image/IMG1569393370103.png',11); 121 | INSERT INTO "category" ("id","name","is_top") VALUES (1,'热点',1), 122 | (2,'娱乐',1), 123 | (3,'体育',1), 124 | (4,'财经',1), 125 | (5,'汽车',1), 126 | (6,'军事',1), 127 | (7,'男人',1), 128 | (8,'视频',1), 129 | (9,'科技',1), 130 | (10,'手机',1), 131 | (11,'女人',0), 132 | (12,'数码',0), 133 | (13,'段子',0), 134 | (14,'时尚',0), 135 | (15,'游戏',0), 136 | (16,'教育',0), 137 | (17,'健康',0), 138 | (18,'旅游',0), 139 | (19,'房产',0); 140 | INSERT INTO "post" ("id","title","content","type","userId") VALUES (1,'阿信分享《说好不哭》幕后故事:只听一次就配唱','

9月17日,阿信在个人社交平台上谈与周杰伦的合作新曲《说好不哭》,他写道:“音乐人的快乐,就是这么朴实无华,且保密。晚点再来跟大家分享这次合作,许多不能说的秘密。”周杰伦也在评论区打趣道:“文笔总是这么好”,阿信也搞怪回复“我有目击到你虐待绞尽脑汁的方文山”,二人隔空互动大秀友情。晚间,阿信发长文谈与周杰伦新歌合作。阿信称金曲奖典礼结束,在周杰伦的录音师完成了新歌《说好不哭》自己的部分。并自评与周杰伦的合作“朴实无华且随性”。

阿信全文:

音乐人的合作,就是这么朴实无华且随性。“哎唷,今天晚上要不要来合唱一下?”,“好啊,我晚上颁个奖就过去!”那天是金曲奖,典礼结束,直接轻装潜入杰伦的录音室,诺大的公司,只见到作词人方文山、录音师Gary、还有大家的哥中之哥。

那是大家少有机会看到的杰伦,一个存在了20年的却不轻易示人,在录音室里专心致意,全神凝聚的周杰伦。“好噢来听一下噢”,密密麻麻的编曲轨道播放,第一次听见这首注定成为经典的歌。这是杰伦只让我听了一次,就把我关进配唱间。一小时后,在杰伦的配唱之下完成了我的部分。“哎唷,是不是来吃个宵夜不错!”会议室的电视回放着当晚的金曲奖,我们吃着外带回来的干面、蛋花汤、当然,还有奶茶。“哎唷,原来你是去颁金曲奖”,原来埋首创作的哥,完全不知道今天是金曲奖,如果没有看到分轨上,一字排开数十轨亲自演唱的细腻和声,你不会知道他花了多少时间在作品上。我喜欢这种感觉,当大家庆祝着过去的成就,而录音室里的我们,正打造全新的作品,奔向未来。

20年,飞逝的青春,留下此刻的你我。很开心参与杰伦这次合作,过去的眼泪,未来的仿徨,希望这首歌陪伴你,说好不哭。

(责任编辑:杨明_NV5736)

',1,2), 141 | (2,'52岁王祖贤餐厅独自就餐被拍 长发披肩颜值依旧','

网易娱乐9月17日报道 近日,有网友晒出了一组在餐厅偶遇王祖贤的照片。当天王祖贤一身黄色纱质上衣配牛仔裤,整个人看上去十分年轻漂亮,虽然素颜亮相依旧难掩气质。当天王祖贤一个人出来就餐,在餐桌上只是默默的刷手机,全神贯注丝毫不受外界的影响。

在王祖贤准备离开的时候,有粉丝上前求签名,王祖贤一脸灿笑开心答应,并且还在粉丝的手机壳上签名。

据了解,2001年,王祖贤宣布退出演艺圈,2003年又复出拍了一部电影,之后就到加拿大开始了隐居生活。

红方观点

女神选择了自己喜欢的生活方式,她喜欢就好。

蓝方观点

曾经红极一时,现在一个人生活难免有些落寞。

9981
801

(责任编辑:马文静_NBJS9027)

',1,3), 142 | (3,'曝詹妮弗·劳伦斯现身婚姻登记处秘密完婚','

网易娱乐9月17日报道 据香港媒体报道,奥斯卡金像影后詹妮弗·劳伦斯(Jennifer Lawrence)与未婚夫Cooke Maroney昨日被拍到穿便服现身纽约市曼哈顿婚姻注册署,在旁有两名保安、一名摄影师以及一名好友,未婚夫更被拍到手持一张疑似结婚证书的白纸,因而被怀疑二人已秘密结婚!

有目击者留言说:“当你去领结婚证,而詹妮弗·劳伦斯就在你眼前路过并去结婚,市政厅是个好去处!“有指二人计划于下个月大搞婚礼派对庆祝。詹妮弗·劳伦斯与Cooke于今年2月订婚,早前詹妮弗·劳伦斯接受受访时表示一直都未准备结婚,直至认识Cooke之后就想嫁给他,她说:“他是我最好的朋友,感觉好荣幸成为Maroney家族一份子。”詹妮弗·劳伦斯曾先后与乐队Coldplay主音Chris Martin、影星尼古拉斯霍特(Nicholas Hoult)以及导演Darren Aronofsky谈过恋爱。

(责任编辑:杨明_NV5736)

',1,3), 143 | (4,'美女一首《亲爱的姑娘我爱你》,听得耳朵都酥了,不自觉就爱上了','https://vd4.bdstatic.com/mda-jigg7aap5hce06x7/sc/mda-jigg7aap5hce06x7.mp4?auth_key=1568740877-0-0-b643e61321c36c256a6985628ccce099&bcevod_channel=searchbox_feed&abtest=all',2,3), 144 | (5,'华为首发计算战略 推出全球最快AI训练集群Atlas900','

网易科技讯 9月18日消息,今天华为全联接大会在上海召开。大会上,华为副董事长胡厚崑表示华为现在状态非常不错,并且宣布了华为全新计算战略。

华为副董事长胡厚崑大会上提到,过去半年来,华为是顶着巨大压力过来的。华为现在状态非常不错,就像上海的天气一样,秋高气爽,云淡风轻。

“感谢在这个关键时刻对华为的支持。你们对我们的支持,给了华为淡定从容的定力,华为不会让你们失望的。”胡厚崑表示。

华为过去30年一直在做联接,但绝对不仅仅只做连接。胡厚崑表示,构建智能世界的两大关键技术就是联接和计算,就像两个孪生兄弟一样。其实,华为对计算的投入有10年时间了。面向未来,华为越来越意识到,计算是必须持续投入的领域。进入智能时代,计算将无处不在。

胡厚崑表示,未来十年将迎来计算新蓝图。来自Gartner的数据显示,未来5年计算产业每年将达2万亿美元市场。

因此,华为宣布了新的计算产业战略包括架构创新、投资全场景处理器、商业策略“有所为有所不为”、构建开放生态四部分。

在架构创新方面,主要是华为的达芬奇架构,这是打造智能战略的基础。

在处理器方面,华为意识到只有打造出更有竞争力的处理器,才能更有竞争力。华为已经推出了鲲鹏、昇腾、麒麟、鸿鹄四大系列处理器。

胡厚崑表示,未来华为将面向更多场景,推出更多处理器。

在商业策略方面,华为要有所为有所不为。

胡厚崑明确:“华为不准备独立对外销售处理器,但华为会向用户提供打包云服务,将AI模组和软件进行开放和开源;华为不做应用,但华为会投入专门的工具帮助伙伴做迁移。”

在生态建设方面,2015年华为发布了第一个版本沃土计划,经过四年发展,华为沃土计划发展了超过130万个开发者。

胡厚崑今天宣布了新一轮沃土计划,投资15亿美元,进一步扩大开发者,扩大到500万人的规模。

去年华为提出了全栈全场景AI解决方案,今天,胡厚崑表示该战略已经全面落地。同时,发布了Atlas900全球最快的AI训练集群。

在衡量AI计算能力的金标准ResNet-50模型训练中,Atlas 900只用了59.8秒就完成了训练,这比原来的世界记录还快了10秒。“这是什么概念?相当于短跑冠军跑完终点,喝完一瓶水才等到第二名。”胡厚崑提到。

胡厚崑分享了华为联合上海天文台与SKA共同打造的一个天文探索的案例。在南半球有20万颗星星,用人眼是看不见这么多星星的,当前条件下,天文学家要从这20万颗星星中,找出某种特征的星体,相当困难,需要169天的工作量。现在用上Atlas 900,只用10秒,就从20万颗星星中检索出了相应特征的星体。

胡厚崑表示,Atlas 900的超强算力已经部署在华为云上,欢迎各行业,各领域使用。(崔玉贤)

(责任编辑:乔俊婧_NBJ11279)

',1,4), 145 | (7,'性感女歌手一首DJ《情火》,动感激情,听了一遍又一遍','https://vd4.bdstatic.com/mda-jigm2w3xxpfgfg9y/sc/mda-jigm2w3xxpfgfg9y.mp4?auth_key=1568816604-0-0-52d6f09c249827e9247fc0ef48f50024&bcevod_channel=searchbox_feed&abtest=all',2,4), 146 | (8,'最适合学外语的7部动画片 你看过几个?','

英语变成和Word一样普遍的必备技能时,学一门二外就被许多人提上了日程。

法语、日语、西班牙语、乃至德语、意大利语都成了心水的选项。

法语多浪漫啊,学好日语还可以去漫画圣地打卡。西语的机关枪语速、德语与英语的“亲戚关系”、意大利语难发的大舌音,每一个都让人好奇。

但考虑到英语学了十来年也谈不上精通,再加一门二外,岂不是捡芝麻丢西瓜?

别担心。都说小孩子是学语言最快最轻松的。他们常看的儿童节目也是重要的语言输入来源之一。

像小朋友一样学习一门语言

Kids’ cartoons—even the silliest or action-oriented ones—are meant to be educational to some degree. Because they’re aimed at children, they introduce and reinforce important cultural aspects that adult dramas take for granted.

儿童卡通片,哪怕是有很多动作场面的蠢蠢的那种,也是有着教育意义的。正因为卡通片瞄准的受众是儿童,动画片会介绍和强调重要的文化背景,这类知识在成人电视剧里通常是一带而过。

动画片很容易跟上

The basic plot lines of kid’s cartoons are fairly simple and take place in familiar, everyday settings, such as schools and homes. That means you’ll become familiar with all the everyday phrases. The characters speak more slowly, and the dialogue is typically standard, casual language, without the idiomatic phrases or regional dialects that can make it hard to understand.

儿童卡通的剧情线一般都很简单,故事场景也都是熟悉的日常生活场景,比方说学校和家里。也就是说你可以通过看动画片来熟悉日常短语的表达。动画角色的语速比较慢,对话也都是典型的标准非正式语言,不会牵涉到艰涩的习语和方言。

动画片本身就很有趣

Think Peppa Pig, the pink piggy who has gained worldwide popularity. There are even videos showing an American child speaking in a British accent to her mom after binge-watching the popular cartoon.

想想《小猪佩奇》,作为儿童卡通居然火遍全球。网上甚至还有视频显示一个美国小孩儿跟她妈妈说话带着英国口音,就是因为看多了这部动画。

刚开始学一门语言时,你发不准某个音的样子不是跟小朋友很像吗?来看看他们都看哪些节目吧↓↓↓

01?英语

The Simpsons《辛普森一家》

The Simpsons in the ''90s was smart, culturally savvy, and unbelievably entertaining TV about an average American family. At its height, no show — animated or otherwise — could reach The Simpsons'' greatness. From the cleverness of ''Marge vs. the Monorail'' to everything Lisa Simpson ever said, ''90s era The Simpsons was fearless and game-changing television.

《辛普森一家》是上世纪90年代一部很优秀且文化含义丰富的娱乐性电视卡通。它讲述的是一个普通美国家庭的故事。在该动画的鼎盛时期,没有哪一部动画片能企及《辛普森一家》的伟大成就。从《玛姬与单轨火车》的巧妙设计,到卡通角色丽萨·辛普森所说的所有台词,90年代的《辛普森一家》所向披靡,改变了电视行业的面貌。

02英语

King of the Hill《乡巴佬希尔一家的幸福生活》

King of the Hill depicts an average middle-class family and their lives in a typical American town. It documents the Hills'' day-to-day-lives in the small Texas town of Arlen, exploring modern themes such as parent-child relationships, friendship, loyalty, and justice. As an animated sitcom, however, King of the Hill''s scope is generally larger than that of a regular sitcom.

《乡巴佬希尔一家的幸福生活》描绘了一个普通的中产阶级家庭和他们在一个典型的美国小镇上的生活。它记录了希尔在德克萨斯小镇阿伦的日常生活,探讨了诸多现代主题,如亲子关系、友谊、忠诚和正义。然而,作为一部动画情景喜剧,《乡巴佬希尔一家的幸福生活》的视野比普通情景喜剧要更大。

You most likely had to have parental supervision to watch King of the Hill, but the show''s depiction of small town life was so accurate and warm, it was worth sitting through the ''adult'' moments with mom and dad in the room.

你最好是在父母的陪同之下观看《乡巴佬希尔一家的幸福生活》。这部动画对于小镇生活的刻画十分准确,又温暖人心。哪怕是里面有一些所谓“少儿不宜”的内容,它也值得你和父母坐在家里一起看完。

03法语

Il était une fois《曾几何时》

Il était une fois, also called Once Upon a Time, is a French educational animation franchise, created by Procidis. There are seven distinct series, each focusing on different aspects of knowledge. These are mostly historical, with Once Upon a Time… Man being focused on the overall history of mankind, and most of the others are more focused on specified historical fields, such as the lives and exploits of the explorers or inventors. Life, however, featured an explanation on the workings of the body.

Ilétait une fois,又名《曾几何时》,是一部法国教育动画,由Procidis创建。该动画有七个不同的系列,每一个系列集中讲述人类知识的一个方面。这些知识大多是与历史有关的,比方说《曾几何时……人类》关注的就是整个人类的历史,而其他大多数系列关注的也是特定的历史切片,比如说探险家或发明家的丰功伟绩与生活。此外,另一个系列《曾几何时……生命》的关注点就是解释人体的运行机制。

04日语

サザエさん(さざえさん)–Sazae-san《海螺小姐》

Sazae-san is more than a kids’ show. It’s a cultural institution, a national treasure and the longest-running animated series in the world, ever!

《海螺小姐》不仅仅是一部儿童动画。它已经成了一种文化,一种国家财富,同时也是世界上播放时间最长的动画连续剧。

It’s about a typical Japanese大家族(だいかぞく –big family) living together in Tokyo. The central character isサザエ, an outspoken but somewhat clumsy woman in her early 20s, living with her husband, son, father, mother, brother and sister all under one roof.

该动画讲述的是住在东京的一个大家族的故事。动画的主角是サザエ(Sazae),一位20岁出头,有点笨拙却又大大咧咧的女孩儿,她和自己的丈夫、儿子、父母、还有兄弟姐妹住在同一个屋檐下。

All of the characters are colorful and funny, and all are named after fish! (サザエis horned turban, a seafood delicacy.)サザエさんstarted off as a comic strip, but is now better-known as a cartoon. It is not unusual for families to plan their Sundays around its 6:30 p.m. time slot.

这部卡通的所有角色都很立体,也很有趣,而且所有角色的名字都和鱼类有关(サザエ就是“海螺”)。《海螺小姐》起初是连载漫画,后来才被制作成动画而广为人知。每个周日的下午六点半很多家庭都会围坐在一起看这部动画。

05德语

Janoschs Traumstunde《雅诺思的梦幻时刻》

Janoschs Traumstunde is a German animated children''s television series that originally ran from 1986 to 1990. It is based on the works of German artist and children''s book author Janosch.

《雅诺思的梦幻时刻》最初是于1986-1990年在德国播出的少儿动画电视剧。该动画是从德国艺术家及童书作家雅诺思的作品衍生出来的。

It is a critically acclaimed and universally beloved cartoon series. Most of the episodes tell a number of self-contained stories, featuring popular characters from the author’s children’s books, such as “Tiger und B?r” and others.

该动画广受业内人士好评,同时也深受大众喜爱。大多数的剧集讲述的都是独立的故事,故事中会出现作者童书中人气很高的角色,比方说老虎和熊等。

06德语

Kpt’n Blaub?r《蓝熊船长》

Based on the comics by Walter Moers, this German cartoon series deals with the adventures of the eponymous Captain Blaub?r, a blue bear spinning yarns about his journeys on the Seven Seas. Each episode is framed by a little story where the captain interacts with his grandchildren, and at its center there is an animated cartoon detailing the often hilarious tales of the old seaman.

这部动画片以沃尔特·莫尔斯的漫画为基础,讲述了蓝熊船长的冒险故事,他喜欢夸夸其谈地讲述自己在七大洋上旅行的事迹。该动画的每一集都有一个小故事作为框架,故事中的船长和他的孙子孙女们互动。故事的核心通常是蓝熊船长这位老海员的滑稽轶事。

07?西班牙语

El Oso Yogui《瑜伽熊》

Yogi Bear is an anthropomorphic funny animal who has appeared in numerous comic books, animated television shows and films. He was created by Hanna-Barbera.

瑜伽熊是一个搞笑的拟人化动物角色,它曾出现在许多漫画书、动画电视节目和电影中。该角色由汉娜·巴贝拉创作。

The plot of most of Yogi''s cartoons centered on his antics in the fictional Jellystone Park, a variant of the real Yellowstone National Park. Yogi, accompanied by his constant companion Boo-Boo Bear, would often try to steal picnic baskets from campers in the park, much to the displeasure of Park Ranger Smith. Yogi''s girlfriend, Cindy Bear, sometimes appeared and usually disapproved of Yogi''s antics.

瑜珈熊动画的大部分情节都集中于他在 “酱石公园”里的滑稽行为之上,“酱石公园”对应的是真实世界里的黄石国家公园。瑜珈熊通常和他的同伴博博熊一起出现,他们经常试图从公园露营者那里偷野餐篮子,惹得公园管理员史密斯很不高兴。瑜珈熊的女朋友—辛迪熊,有时也出现在剧集里,通常很鄙夷瑜珈熊的滑稽行为。

看到这里,你种草了哪一门语言呢?

趁头发还有这么多,赶紧装作小朋友,学起来吧。

Notes

sitcom n.情景喜剧

reinforce v.加强;使更强烈

binge v.大吃大喝;狂欢作乐

depict v.描绘;描画;描述

acclaim n./v.赞扬;欢呼;欢迎

eponymous adj.(与标题)同名的

anthropomorphic adj.人格化的

antic adj.滑稽的;古怪的

variant n.变体;变形

(责任编辑:杨卉_NQ4978)

',2,5), 147 | (9,'双语阅读:当代青年网恋观察 奔现就像买彩票','

As we spend more and more of our lives online, inevitably we discover ourselves flirting with people we have never met in real life - you might even call it having a crush.

我们花在网上的时间越来越多,自然而然就会更容易和现实中没见过的人撩起来,有人甚至会说“我网恋了”。

There’s something about the dynamism of social media that feels both more truthful and more mysterious. You have lots of intimate knowledge of a person but you can’t see them.

社交媒体的运行机制会让人觉得既有非常真实的一面,也有十分神秘的一面。你知道很多屏幕那头人的秘密,但你却从未见过他/她。

01奔现和买彩票差不多

When we would eventually meet, sometimes it was pure magic, one of these rare times in a life when everything finally seemed to fit together and I felt I''d met my other half. Other times it was… well, less magic, because the spark in the person didn''t match the connection we made online.

有时候和网撩对象见面会很有化学反应,这种反应一辈子可能都少见,好像一切都很合拍,找到真爱了的感觉。不过有的时候见面就……呃,没那么合适,面对面的交流还没有网聊的时候有火花四溅的感觉。

Sometimes the other person and I would feel the same way, whether we were overwhelmed or underwhelmed. Even worse than both of us being disappointed was when one of us would be thrilled while the other was not, which would eventually result in hurt feelings on both sides.

有时候对方和我想得一样,不管是有被惊喜到还是感觉很无聊。但比我俩都很失望还要更糟糕的情况是这样的:我俩其中一位感觉对方很不错,但对方却无感。这种情况最终会伤害双方。

The first time I met an online love it was magic, and as a longtime romantic I simply assumed it would feel like that every time. So I was shocked when the second time I met someone I had connected with online, there was no magic, no spark, none of what I was so sure would happen because it had happened once before.

我第一次见网友的经历就很棒,非常浪漫,以至于我误以为网聊奔现都应该是这种美好的感觉。所以第二次奔现的时候就很幻灭,两人之间完全没有火花,根本不是我之前经历的那种浪漫状况。

On the bright side, she and I have been good friends ever since. And the few more times I have met someone online and then met in person some time later, the experience has been somewhere in between the first and second times—some spark but not fireworks.

不过也不完全是坏事儿,我和她后来成了朋友。后来几次我跟人网聊之后再奔现,感觉都处在最初两次的感受之间——有点化学反应,但并非一见钟情。

02糟糕,被发现了

Once I had a crush on a guy. From the internet. And I was secretly watching a YouTube video that he had posted. And my finger slipped and I accidentally clicked ''like'' or ''thumbs up'' or whatever.

有次我对一个网撩的男生有好感。然后我就在网上看他发的视频,结果一不小心手滑,按了赞!

And this was the first time I realized that YouTube was directly, horrifyingly linked to my Gmail. So there was my face, next to a thumbs up of this video that I was secretly, creepily watching. And I freaked out. And was like ''Undo! Undo! Undo!!!

那时我才知道油管账号是直接跟我的邮箱绑定的。所以我的邮箱头像就出现在了视频底下的“点赞区”,显示出我在偷偷看他的视频。我吓坏了,疯狂找:“撤回,有没有撤回键!”

So then I clicked ''thumbs down'' thinking that would undo it somehow, but no, obviously that just meant that now my face was on the YouTube video next to a thumbs down icon and it was very embarrassing, and then when we met in real life, I pretended to barely know who he was even though obviously I''d watched his video.

结果我就点了“踩”……想着应该可以抵消那个赞。但是并没有,我的头像只是从“按赞区”移动到了“按踩区”,尴尬。之后我和他奔现的时候,我都装作不太了解他的样子,哪怕很显然我看过他的视频。

03看过你的历史,我们才有未来

I started a new job about a year ago and was surprised to discover that my boss, the editor-in-chief, was a pretty young, kinda hot dude - in a hot dad way. But we never really interact in person since he''s not my direct boss.

一年前我换了份新工作,很惊喜地发现我的上司——也就是主编——还是蛮年轻的帅大叔。但其实我们并没有太多接触,因为他并不是我的直属上司。

But in my first week he started following me on Twitter, which I was really flattered by because he only follows a few hundred people on Twitter. Then I wrote something and he tweeted about it, so I faved that.

但工作的第一周他就在推特上关注了我,我有点受宠若惊,因为他也就关注了几百个人而已。我发过的推文也被他转发过,然后我就给他的转发点了赞。

Then I realized I was favoriting quite a few of his tweets, and then things got weird where I was like faving his tweets on Saturdays, or going back to old tweets and faving those. I think I thought I was being subtle and saying ''oh, heyyyyy,'' except it wasn''t subtle at all.

后来我发现我给他的很多条推文都点过赞。之后就越来越诡异了,周六休息日给他点赞也就算了,我还翻到他的历史推文里按赞。我内心想的是,这是很隐晦的表示“诶,我对你有意思”。但其实这么做简直昭然若揭啊。

04朋友的朋友才是中奖的那个

In 2009 I befriended a guy whom one of my other friends had gone out with a few times. Once he accepted my Facebook friend request, I naturally went to look at all of his recent photos. In his photos was another guy that caught my attention, so I went and looked at his profile and lurked behind all of his photos. ''What a babe,'' I thought. I was immediately obsessed.

2009年我加了一个男生,我有个朋友跟他出去玩儿过几次。他刚一接受我的脸书好友申请,我就自觉跑到他的相册里翻看。照片里有个男生吸引了我的注意力,然后我就点进去看他的主页,鬼鬼祟祟地翻看他的照片。“好帅啊”,我心想。我感觉我恋爱了。

I decided to take my virtual stalking offline by messaging our mutual friend and not-so-subtly asking for him to hook us up. Go big or go home, right?

我决定到线下去追这个男生。然后我就给我俩共同的朋友发了消息,明目张胆地叫他给我俩牵线搭桥。要么就玩大的,要么就别玩,不是吗?

To make a long story short, yadda yadda yadda, Billy and I got married in December 2013!

总而言之,啦啦啦哈哈哈哈,我和比利在2013年12月完婚啦!

There is always going to be a divide, however, between our public persona(e)—whether presented via Twitter or Facebook—and who we are in the physical realm.

我们用来社交的公共人格——无论是推特还是脸书上的形象——和现实中的我们肯定是有区别的。

On social media, you get all the fun, interesting parts of someone without having to deal with the things that are difficult or dull about them. They are not presenting to you, for the most part—or at least in any way that actually affects you—their idiosyncrasies, emotional unavailability, or the way they chew with their mouth open.

在社媒上,你只会看到别人生活中有趣好玩的部分,不会看到他们生活中的困难和乏味。绝大多数时候,至少为了不影响到你,他们不会向你展示他们的小癖好、不愿传递的情感、以及大声吧唧嘴的样子。

If you want my advice, don''t avoid making online connections—they can be marvelous experiences while they last. But I would recommend trying to meet each other before your feelings become so intense that you''ll be seriously heartbroken if that initial meeting doesn''t go well.

如果要我给建议,我会说一定要建立网撩关系——如果能长久,那必定是超棒的经历。不过我也会建议说,尽量在两人聊得热火朝天之前就奔现,这么做可以避免过高的期望值在不甚满意的初见打击下碎成一地的情况。

Think of it as having a crush for a long time: you may idealize someone to such an extent that when you finally get to meet them, you can''t help but be disappointed by the real person you actually meet.

想想暗恋某个人很长一段时间的体验:你会把对方理想化,程度越来越深。等到最终和对方奔现的时候,见到真实的人,你根本扛不住失望的心情。

(责任编辑:郑娟_NQ0738)

',1,8), 148 | (10,'耿直!关晓彤大方回应私服争议:时好时坏吧','

近日,关晓彤接受某时尚媒体采被问及私服问题时,大方回应称,“(私服)时好时坏吧,因为我的衣服老把自己分好多节。”同时她还劝诫道:“个高的女生不适合get我的同款。”

对此,网友留言纷纷表示:“性格不错”、“智商情商都在线的女生”。

(责任编辑:李思_NBJ11322)

',1,8), 149 | (11,'关晓彤回家吃饭了','

近日,关晓彤接受某时尚媒体采被问及私服问题时,大方回应称,“(私服)时好时坏吧,因为我的衣服老把自己分好多节。”同时她还劝诫道:“个高的女生不适合get我的同款。”

对此,网友留言纷纷表示:“性格不错”、“智商情商都在线的女生”。

(责任编辑:李思_NBJ11322)

',1,8), 150 | (12,'关晓彤,下雨了回家收衣服吧','

近日,关晓彤接受某时尚媒体采被问及私服问题时,大方回应称,“(私服)时好时坏吧,因为我的衣服老把自己分好多节。”同时她还劝诫道:“个高的女生不适合get我的同款。”

对此,网友留言纷纷表示:“性格不错”、“智商情商都在线的女生”。

(责任编辑:李思_NBJ11322)

',1,8); 151 | 152 | INSERT INTO "post_comment" ("id","content","userId","postId","parentId") VALUES (1,'123',11,1,NULL), 153 | (2,'234',11,1,NULL), 154 | (3,'111',11,1,NULL), 155 | (4,'444',11,1,2), 156 | (5,'222',11,1,3), 157 | (6,'22',11,1,3), 158 | (7,'333',11,1,3), 159 | (8,'444',11,1,7), 160 | (9,'555',11,1,7), 161 | (10,'666',11,1,7); 162 | INSERT INTO "user_post_star_post" ("userId","postId") VALUES (5,1), 163 | (5,2), 164 | (11,2), 165 | (11,5), 166 | (11,4), 167 | (11,8), 168 | (11,1); 169 | INSERT INTO "user_follows_user" ("userId_1","userId_2") VALUES (11,6), 170 | (11,3), 171 | (11,2); 172 | INSERT INTO "post_categories_category" ("postId","categoryId") VALUES (1,1), 173 | (1,2), 174 | (2,1), 175 | (2,2), 176 | (2,11), 177 | (2,14), 178 | (3,1), 179 | (3,2), 180 | (3,11), 181 | (3,14), 182 | (4,1), 183 | (4,2), 184 | (4,7), 185 | (4,8), 186 | (4,11), 187 | (4,14), 188 | (5,1), 189 | (5,2), 190 | (5,7), 191 | (5,9), 192 | (5,12), 193 | (5,14), 194 | (5,16), 195 | (7,1), 196 | (7,2), 197 | (7,7), 198 | (7,8), 199 | (7,11), 200 | (7,14), 201 | (7,16), 202 | (7,17), 203 | (8,1), 204 | (8,10), 205 | (8,12), 206 | (8,13), 207 | (8,16), 208 | (8,17), 209 | (9,1), 210 | (9,4), 211 | (9,7), 212 | (9,13), 213 | (9,16), 214 | (9,17), 215 | (10,2), 216 | (10,14), 217 | (11,2), 218 | (11,14), 219 | (12,1), 220 | (12,2), 221 | (12,14); 222 | INSERT INTO "post_like_users_user" ("postId","userId") VALUES (1,5), 223 | (2,11), 224 | (8,11), 225 | (1,11); 226 | INSERT INTO "post_cover_upload" ("postId","uploadId") VALUES (1,2), 227 | (2,3), 228 | (3,4), 229 | (4,5), 230 | (5,6), 231 | (7,7), 232 | (8,8), 233 | (8,9), 234 | (8,10), 235 | (9,12), 236 | (10,13), 237 | (11,13), 238 | (12,9), 239 | (12,12), 240 | (12,13); 241 | 242 | CREATE INDEX IF NOT EXISTS "IDX_a62933d561c31fc90c5c2e8e7d" ON "user_post_star_post" ( 243 | "postId" 244 | ); 245 | CREATE INDEX IF NOT EXISTS "IDX_c5c365c016809e8aa4cfae53f6" ON "user_post_star_post" ( 246 | "userId" 247 | ); 248 | CREATE INDEX IF NOT EXISTS "IDX_da5eb1d232421542d4fb33ac41" ON "user_follows_user" ( 249 | "userId_2" 250 | ); 251 | CREATE INDEX IF NOT EXISTS "IDX_911345dc417fb10f25f7644cc6" ON "user_follows_user" ( 252 | "userId_1" 253 | ); 254 | CREATE INDEX IF NOT EXISTS "IDX_a5e63f80ca58e7296d5864bd2d" ON "post_categories_category" ( 255 | "categoryId" 256 | ); 257 | CREATE INDEX IF NOT EXISTS "IDX_93b566d522b73cb8bc46f7405b" ON "post_categories_category" ( 258 | "postId" 259 | ); 260 | CREATE INDEX IF NOT EXISTS "IDX_701dd0cfc5bc38ede4f8a11257" ON "post_like_users_user" ( 261 | "userId" 262 | ); 263 | CREATE INDEX IF NOT EXISTS "IDX_e0c6eb857edb4374d07a7175a3" ON "post_like_users_user" ( 264 | "postId" 265 | ); 266 | CREATE INDEX IF NOT EXISTS "IDX_f69ff461a7d7a83b64e5ac5189" ON "post_cover_upload" ( 267 | "uploadId" 268 | ); 269 | CREATE INDEX IF NOT EXISTS "IDX_b9a00cd8d070cce5c13d43dbf5" ON "post_cover_upload" ( 270 | "postId" 271 | ); 272 | CREATE INDEX IF NOT EXISTS "IDX_0cb77d79c53f0759b8153ec8a6" ON "category_posts_post" ( 273 | "postId" 274 | ); 275 | CREATE INDEX IF NOT EXISTS "IDX_3a1f3735235af2f4b702a3d398" ON "category_posts_post" ( 276 | "categoryId" 277 | ); 278 | COMMIT; 279 | -------------------------------------------------------------------------------- /test/user.test.ts: -------------------------------------------------------------------------------- 1 | import request from "supertest" 2 | import server from "../src/server" 3 | import { expect } from "chai" 4 | 5 | describe("POST /login", () => { 6 | it("should return some defined error message with valid parameters", (done) => { 7 | return request(server).post("/login") 8 | .expect(200) 9 | .end(function(err, res) { 10 | expect(res.error).not.to.be.undefined; 11 | done(); 12 | }); 13 | 14 | }); 15 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "skipLibCheck":true, 9 | "target": "es6", 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "sourceMap": true, 13 | "outDir": "dist", 14 | "baseUrl": ".", 15 | "paths": { 16 | "*": [ 17 | "node_modules/*", 18 | "src/types/*" 19 | ] 20 | } 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ] 25 | } --------------------------------------------------------------------------------