├── .dockerignore ├── .env ├── .env.development ├── .env.production ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .swcrc ├── Dockerfile.dev ├── Dockerfile.prod ├── README.html ├── README.md ├── docker-compose.app.dev.yml ├── docker-compose.app.prod.yml ├── docker-compose.db.yml ├── docker-compose.dev.yml ├── docker-compose.prod.yml ├── docker-compose.redis.yml ├── local ├── jwtRS256.key └── jwtRS256.key.pub ├── nest-cli.json ├── package.json ├── pnpm-lock.yaml ├── prisma ├── dbml │ └── schema.dbml ├── migrations │ ├── 20231202062932_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── redis.conf ├── scripts └── generate-jwt-keys ├── sql └── all.sql ├── src ├── @types │ ├── index.d.ts │ └── wrapper.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── main.ts ├── modules │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── dtos │ │ │ ├── export-login.dto.ts │ │ │ ├── login-account.dto.ts │ │ │ └── register-account.ts │ │ └── strategies │ │ │ ├── jwt-access.strategy.ts │ │ │ ├── jwt-refresh.strategy.ts │ │ │ └── local.strategy.ts │ ├── department │ │ ├── department.controller.ts │ │ ├── department.module.ts │ │ ├── department.service.ts │ │ └── dto │ │ │ ├── create-department.dto.ts │ │ │ ├── export-department-list.dto.ts │ │ │ ├── export-department.dto.ts │ │ │ ├── query-department.dto.ts │ │ │ └── update-department.dto.ts │ ├── goods-category │ │ ├── dto │ │ │ ├── create-goods-category.dto.ts │ │ │ ├── export-goods-category-list.ts │ │ │ ├── export-goods-category.dto.ts │ │ │ ├── query-goods-category.dto.ts │ │ │ └── update-goods-category.dto.ts │ │ ├── goods-category.controller.ts │ │ ├── goods-category.module.ts │ │ └── goods-category.service.ts │ ├── goods-info │ │ ├── dto │ │ │ ├── create-goods-info.dto.ts │ │ │ ├── export-address-sale.dto.ts │ │ │ ├── export-amout-list.dto.ts │ │ │ ├── export-category-count.dto.ts │ │ │ ├── export-category-favor.dto.ts │ │ │ ├── export-category-sale.dto.ts │ │ │ ├── export-goods-info-list.dto.ts │ │ │ ├── export-goods-info.dto.ts │ │ │ ├── export-sale-top-10.dto.ts │ │ │ ├── query-goods-info.dto.ts │ │ │ └── update-goods-info.dto.ts │ │ ├── goods-info.controller.ts │ │ ├── goods-info.module.ts │ │ └── goods-info.service.ts │ ├── menus │ │ ├── dto │ │ │ ├── create-menu.dto.ts │ │ │ ├── export-menu.dto.ts │ │ │ └── update-menu.dto.ts │ │ ├── menus.controller.ts │ │ ├── menus.module.ts │ │ └── menus.service.ts │ ├── qrcode │ │ ├── qrcode.controller.ts │ │ ├── qrcode.module.ts │ │ └── qrcode.service.ts │ ├── roles │ │ ├── dto │ │ │ ├── assign-role.dto.ts │ │ │ ├── create-role.dto.ts │ │ │ ├── export-role-list.dto.ts │ │ │ ├── export-role.dto.ts │ │ │ ├── query-role.dto.ts │ │ │ └── update-role.dto.ts │ │ ├── roles.controller.ts │ │ ├── roles.module.ts │ │ └── roles.service.ts │ ├── story │ │ ├── dto │ │ │ ├── create-story.dto.ts │ │ │ ├── export-story-list.dto.ts │ │ │ ├── export-story.dto.ts │ │ │ ├── query-story.dto.ts │ │ │ └── update-story.dto.ts │ │ ├── story.controller.ts │ │ ├── story.module.ts │ │ └── story.service.ts │ └── users │ │ ├── dtos │ │ ├── create-user.dto.ts │ │ ├── export-user-list.dto.ts │ │ ├── export-user.dto.ts │ │ ├── query-user.dto.ts │ │ └── update-user.dto.ts │ │ ├── users.controller.ts │ │ ├── users.module.ts │ │ └── users.service.ts ├── seed.ts ├── shared │ ├── config │ │ ├── index.ts │ │ ├── loadEnv.config.ts │ │ └── validateEnv.config.ts │ ├── decorators │ │ ├── cache.decorator.ts │ │ ├── expose-not-null.decorator.ts │ │ ├── get-current-user-id.decorator.ts │ │ ├── get-current-user.decorator.ts │ │ ├── index.ts │ │ ├── public.decorator.ts │ │ ├── record-time.decorator.ts │ │ ├── require-permission.decorator.ts │ │ ├── transform-number-to-boolean.ts │ │ ├── validate-array.decorator.ts │ │ ├── validate-date.decorator.ts │ │ └── validate-string-number.decorator.ts │ ├── dtos │ │ ├── base-api-response.dto.ts │ │ ├── base-pagination.dto.ts │ │ ├── base-query.dto.ts │ │ └── index.ts │ ├── enums │ │ ├── decorator.enum.ts │ │ ├── env.enum.ts │ │ ├── index.ts │ │ ├── permission.enum.ts │ │ ├── prisma-error-code.enum.ts │ │ ├── redis-key.enum.ts │ │ ├── scan-status.enum.ts │ │ └── strategy.enum.ts │ ├── filters │ │ ├── all-exceptions.filter.ts │ │ └── index.ts │ ├── global │ │ └── index.ts │ ├── guards │ │ ├── index.ts │ │ ├── jwt-access.guard.ts │ │ ├── jwt-local.guard.ts │ │ ├── jwt-refresh.guard.ts │ │ └── permission-auth.guard.ts │ ├── interceptors │ │ ├── index.ts │ │ └── transform.interceptor.ts │ ├── interfaces │ │ ├── index.ts │ │ └── jwt-payload.interface.ts │ ├── logger │ │ ├── index.ts │ │ ├── log.ts │ │ ├── logger.module.ts │ │ └── logger.service.ts │ ├── middleware │ │ ├── index.ts │ │ └── log.middleware.ts │ ├── prisma │ │ ├── index.ts │ │ ├── prisma.module.ts │ │ └── prisma.service.ts │ ├── redis │ │ ├── index.ts │ │ ├── redis.module.ts │ │ └── redis.service.ts │ ├── shared.module.ts │ ├── shared.service.ts │ ├── upload │ │ ├── index.ts │ │ ├── interfaces │ │ │ └── file.interface.ts │ │ ├── upload.controller.ts │ │ ├── upload.module.ts │ │ └── upload.service.ts │ └── utils │ │ ├── date.ts │ │ ├── filer-empty.ts │ │ ├── generate-tree.ts │ │ ├── handle-error.ts │ │ ├── index.ts │ │ ├── random.ts │ │ └── request-info.ts └── swagger.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | !README.md 3 | node_modules/ 4 | [a-c].txt 5 | .git/ 6 | .DS_Store 7 | .vscode/ 8 | .eslintrc 9 | .prettierrc 10 | .prettierignore 11 | dist/ 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="mysql://root:123456@localhost:3306/cms" 2 | TZ=Asia/Shanghai 3 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | APP_PORT=3000 2 | APP_ENV=development 3 | 4 | DATABASE_URL="mysql://root:123456@mysql:3306/cms?timezone=Asia/Shanghai" 5 | 6 | 7 | DB_USERNAME=root 8 | DB_PASSWORD=123456 9 | 10 | REDIS_PORT=6379 11 | REDIS_HOST=redis 12 | REDIS_PASSWORD=123456 13 | 14 | DB_SYNC=true 15 | 16 | 17 | # 打印的日志级别 18 | LOG_LEVEL=info 19 | # 是否打印时间戳 20 | TIMESTAMP=true 21 | 22 | LOG_ON=true 23 | 24 | JWT_PUBLIC_KEY='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FR 25 | OEFNSUlCQ2dLQ0FRRUF2RFJlVm1vVVdORFdrWUlNZnArTQpVM3dUbk5PalAxczV2a0E5UnRZL2RJ 26 | c0VRM2wyejJhcjFTT0lTam54TldxYUpjajdqMzJFaXlLTTlzejdrcUtBCi9NT2luZ1o2d0ppZk1r 27 | UXFvMmRYWEduWjFFVXFFU0FyMHZzMGlxL3p5dW9tUTNIbzBhU2p0UElpNUlRSXprNmEKT0RwdTVn 28 | cEc2R05qVUgvYlJ5QTJaMDFHTGpvNkk1cDEvVU8vZEZzaEQvL0krc0hPcDM0djUrS2ozQVFSOCtL 29 | egpnVnF6VWRhajh4cVFGSUdSQjJRZEp3eG5MZzE0ZXZFdnFyaUs1V3VGY1d4bG5mZ1RNYkppSGJ3 30 | c2pOeDZuRDV2Cjc2YzF0SC9aN2todSs1eEtHd2J4OHI2SittUE5JcTQzRG9mVE5vZFBRWFZvZTY3 31 | MHBrbnJLT0daVm9CUkRyUUsKWlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==' 32 | 33 | JWT_PRIVATE_KEY='LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBdkRSZVZtb1VX 34 | TkRXa1lJTWZwK01VM3dUbk5PalAxczV2a0E5UnRZL2RJc0VRM2wyCnoyYXIxU09JU2pueE5XcWFK 35 | Y2o3ajMyRWl5S005c3o3a3FLQS9NT2luZ1o2d0ppZk1rUXFvMmRYWEduWjFFVXEKRVNBcjB2czBp 36 | cS96eXVvbVEzSG8wYVNqdFBJaTVJUUl6azZhT0RwdTVncEc2R05qVUgvYlJ5QTJaMDFHTGpvNgpJ 37 | NXAxL1VPL2RGc2hELy9JK3NIT3AzNHY1K0tqM0FRUjgrS3pnVnF6VWRhajh4cVFGSUdSQjJRZEp3 38 | eG5MZzE0CmV2RXZxcmlLNVd1RmNXeGxuZmdUTWJKaUhid3NqTng2bkQ1djc2YzF0SC9aN2todSs1 39 | eEtHd2J4OHI2SittUE4KSXE0M0RvZlROb2RQUVhWb2U2NzBwa25yS09HWlZvQlJEclFLWlFJREFR 40 | QUJBb0lCQUZNM2pLY0ZESzRnMlY5SgpjNkRoaHppNjJpa3o0ekQyYzFmT0s4b1FuY280VmRCSCt1 41 | TEY4U0N6TDJZeXJKY0Q1ZGpqUDJnNUJjeEhvTERYCi9qemVJYzZoNmx1WlhkbWZJblVsY3YwQmly 42 | MVFDSU0xZWQ1TXJWUUN3ejYzZ2tLc3VmS0VnWCtCSHVtNVR2aGYKOFV6WHNKVkFNUjBDV2t3Uzlp 43 | ejMrOHM5VVJEbmRNWE1TUjA4ZG1kY3BqdFpLcWc2UzFzdHFTdXkrNzdJOWNuTQp5ZjZpem1vWDkx 44 | SW54RDg1RVFLdks0bDgzK1pJbUxQbU5GL21EbWlkV3BVemxYTEFldWRFMjduVCtHeFAxVlFPCnhQ 45 | Uk1OUmo3eXhKS1lyZEI1V1NoSjhiOWNjaGV4NWRQckNoVm5kU1RmN2FrWjVQYkhBc0lGM0RFbHJ4 46 | NGtXeUcKd3dGc29Ga0NnWUVBdzYrWVdOTDB2VWVZaFVLS0UxZUJxYlhKQVZLTk4ySDIyN3U3M1Fi 47 | OEJsQXRaK2V6bGdBZwp4ckswV2dpQVZpUUpPeXZseDREKzdSb3JGbG5IVk41OVgvdWJ3MUhMMTVQ 48 | VzhGd28vL01zVGd4ZEpIL3JMeEh3CmNWdGVGaHZ6ZXRId3dNQXo5Y1YvRjVKQ09RQjF4THd1ZlJZ 49 | cUVPS2ludEFZS0w0Qi80SmZSUXNDZ1lFQTlqWjIKNjNZRkp4WTM0L1JNeVYzR0Z3WFp5bWVMZ1V3 50 | eENabGdtOTRhKytGR3VZODJzc25IVno3SDZZeHBVbWdYQmMzNAp3dHNsYk8yUDRnaGhTbkFTLzQ3 51 | MmJSd3J0aC9odU9WaEM5bnpFNHhSRk1ZUmpzVlZkVmYwdTh1NGFZOXdnNU96CjBBT0pKSmNITFQ5 52 | ekVkZUpMWFZIUFdnellRdUhRUmZHdGZtR3RFOENnWUE2dVRBN3Y1cklUbnI3eXBzSzhPQ3QKWUNz 53 | ZzVYZ1JYYW1xQ1MxNFI4ZEwwYlcrajY2NTNmSDREdHJHaGZTVlpSME1EOEZWM0dVa0hBMUFHTk1U 54 | cWV6dgo1OTYzZjQxdmRTTTBZRVBCZzJVUlN1Nk1ySUtVVG9yY1NiSHphcEhua3Fid0FQM1d6Rnky 55 | WXlMU1hrdjVMUXU2ClovTlp1OWxYVlZWYXJLS0czY1hmUHdLQmdFbGJOTmMybEpadXNqeENuMVlu 56 | a0V0dnZOSG5ROU5FSmlBY0NJS28KREJZYnFuekN4S05FSnhaQmFFS1hTUkg2WFZHbmFvUnowcFMv 57 | dXV5M1huVlZLdlBsWGxwbjJFWXMvWTJmR2VqMApDTTQ4TWpRa1J6cm9aR3ZQeHVUazc0N1Q0OHZ3 58 | QjlUa0dNcEVEU2xZRENxMnN2Sk9UT2xvMEZZTG4wS3ZtTTdECkIzVlZBb0dBTEQ5Y2ZYK0VXaisx 59 | UjJ0amZ0OUVRc2NUaWtXY2FzUkl3Z0JoNHpJK3lKajhENlorVTBwcEJYa28KckpqUWQzK3VCSDhO 60 | T3FUREFmVE5yMTA3bmZhanFoTjhpTjRWS3AvbEtNRnpMdFNlT25sY2JGN2VMUjQxamdZaApIZ3lB 61 | RCs4MHVCRnhua1hiZUF4akExNHlIc2QrZnhodG10RUIvRG04R0pVRmVaWFdmUG89Ci0tLS0tRU5E 62 | IFJTQSBQUklWQVRFIEtFWS0tLS0tCg==' 63 | 64 | JWT_ACCESS_TOKEN_EXPIRES_IN="1h" 65 | JWT_REFRESH_TOKEN_EXPIRES_IN="7d" 66 | 67 | 68 | UPLOAD_ADDRESS = "http://localhost:3000" 69 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | APP_PORT=3000 2 | 3 | APP_ENV=production 4 | 5 | DATABASE_URL="mysql://root:123456@mysql:3306/cms?timezone=Asia/Shanghai" 6 | 7 | DB_HOST=mysql 8 | DB_PORT=3306 9 | 10 | REDIS_PORT=6379 11 | REDIS_HOST=redis 12 | REDIS_PASSWORD=123456 13 | 14 | DB_USERNAME=root 15 | DB_DATABASE=cms 16 | DB_PASSWORD=123456 17 | 18 | DB_SYNC=false 19 | 20 | 21 | # 打印的日志级别 22 | LOG_LEVEL=info 23 | # 是否打印时间戳 24 | TIMESTAMP=true 25 | 26 | LOG_ON=true 27 | 28 | JWT_PUBLIC_KEY='LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0NCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NB 29 | UThBTUlJQkNnS0NBUUVBcy84ZTAvckhPeldhVk40MFM3RlkNCmVqRDR0c2l3RGZ0VEtLZDh3Nk9U 30 | NklFNCtSUGFUamsrSVR6TzVqaVBBMSszR1NseGdNajZpRFR0VlZON09lSXoNCmJMQTNGTTdrK256 31 | YXVWQkdnejAvNFNpSFVoanVQS1dTRGQvcXlIKzhvSzJjVThYQndIVHZSRlNhUTA4aXVTY1INCjUw 32 | LzJZZ1l4TmdHVEFnQWIreFB1TXNydE4rOVZZOFZvL0JmSk5rTlJvajZoUUFSMDVHQmVNcS9xbmEy 33 | VFV6YloNCk5DYXFGd2ZlYm5hMWJRbkhjY3lwc3hWditpSjFnTnhLR0ttR0d1cUJUSGFsa004TDhC 34 | Y1RRc25tZ01wQ3hmcnoNCmF5alhPTWh6cWhoR1lrR0svaHJrWkhlL0lVcnNINkN1Mjg1ZXU4L29D 35 | aEp1azN6TXNhdjd6RWxYTUp6WnBKNXUNClhRSURBUUFCDQotLS0tLUVORCBQVUJMSUMgS0VZLS0t 36 | LS0NCg==' 37 | 38 | JWT_PRIVATE_KEY='LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcy84ZTAvckhP 39 | eldhVk40MFM3RlllakQ0dHNpd0RmdFRLS2Q4dzZPVDZJRTQrUlBhClRqaytJVHpPNWppUEExKzNH 40 | U2x4Z01qNmlEVHRWVk43T2VJemJMQTNGTTdrK256YXVWQkdnejAvNFNpSFVoanUKUEtXU0RkL3F5 41 | SCs4b0syY1U4WEJ3SFR2UkZTYVEwOGl1U2NSNTAvMllnWXhOZ0dUQWdBYit4UHVNc3J0Tis5VgpZ 42 | OFZvL0JmSk5rTlJvajZoUUFSMDVHQmVNcS9xbmEyVFV6YlpOQ2FxRndmZWJuYTFiUW5IY2N5cHN4 43 | VnYraUoxCmdOeEtHS21HR3VxQlRIYWxrTThMOEJjVFFzbm1nTXBDeGZyemF5alhPTWh6cWhoR1lr 44 | R0svaHJrWkhlL0lVcnMKSDZDdTI4NWV1OC9vQ2hKdWszek1zYXY3ekVsWE1KelpwSjV1WFFJREFR 45 | QUJBb0lCQVFDdGFSV25Qa3poYzFQNwpmWlErZEY1OFlsL0xvVzlVR1JUVjd2NlpjU2ZYY1YrVlFC 46 | WXJGS1VSMm9hdWNFRUFEQi9Yb3dEU0JVNnhHT1NxCk51SmFNdDQrVVVyUHhqLytQM2x0M2JRQlRt 47 | b1RuenMzMGdMNzlMdlFtRENXOTlHZnI0TVRoa3VUQWxWZHJIQnUKZG5oS2p5U0ZpSmtqZVRuQ0FT 48 | UkRZaDJTK0hHZEJoeFVPRmtXTllXVmZJN3NUMk5iMkRyc3NacmZlRUNhNXhVagpQVWhrMWlXdEZW 49 | OS9JZU5xOXF0WWpsSFJnZThkV0NBaFJ2YWhKMkkrVnk3ZzAxKzZ1RWJVcXh5T1ZsMVZQTWlhClNT 50 | bTNuS1l1Zml5eG9mdkJVOWRnejFlQzR0VFZ6ZEJ2T2hyYXRIUmFHUzJwRGVyazBqU2Z3RGtGS3F6 51 | OGJsUkYKSi80Rm95bFZBb0dCQU4wN2ZDa05tbUN6MG85NEFzY2tHS0RjZVM5NTdvMnpnZWJFbXpR 52 | V0lpdFV1bzNSQmxHYQpLQkJHQXZ1eDEvckYrcE1uSkhPRmVXZ1orSkw4SzZieWZmeTN0SCt2bjUv 53 | OHkvU25DbWY5ZjV6cE95Zlp3S0xFCmxPNEdveEdYcnd4UXNraTJnWjV3RktyS3cxVjZhMzhWL2Q0 54 | T2dUUHJ2YTEza3o0ZFNDekR1cXlYQW9HQkFOQkkKcUFvSFlLL2ZkYlpUNTNiRlFDbVNPTXlraWFG 55 | ck9IRnkrZ0YwMmxreFRzY0tjWFlpK2lyYXNGd01PeDNKM0tHcApwTUZoWXUwQUF1SVJEM2FTbnhX 56 | VVBNcXBZTmozazVwTTdIL016bmNPamczREIzUWRkcUNTSGdsOFRtclRMUHlBCk5WZzVhWlNVblhO 57 | ck1rRms4Qzl5R091UXY2WlNmZ0VuNE0zREtUY3JBb0dBQlVYWHJiclBSU0xFRC90U1JhRE0Kblhq 58 | andvZnJjYUVucFhKbUtKV21kdFhzSkZrcEIyVGZNNVFYbWh3aEE2OFlTODJSQnRmVmp1K2ZoeEZQ 59 | a1FrUwppNlZ0UGVYWHJoNStWZlJ0UVJFL3EyTzdyelYzYSttM2l2ekpnaS9WVWp2U0kvMkZsVnNp 60 | Z3ZlV05FRllzb3k1Cm9rTkNQUlNQVHNVYzAwd3JYalhFQ3owQ2dZQk1SeCtMTkxKN0Zrb2tzTXVt 61 | Mm93cDdVdngzaHd0U25nTVRFQTAKd2xlb3JIOGVNN3ZqdU9HSFNqbW1MRENHTTBRaXpGN3pGemhF 62 | ZFdtWTR3aVhzeENodFgwaDl5L3BwWm1mdTZZdApFNU5WVkxZL1lmcGIwdUo3NGFjd2NCN0R6bnkr 63 | S0RIaEVuMlJGWEFvTWN6ZzJCZUNPTFhacDFRWWxFTmpKdmlVClFuSFlxd0tCZ0RYcStaa1Vnd1dO 64 | RmtqbytyQldpRXVlcmNsLzdnQkNtaFd1NE14NmxhYmlvS0ltUnBRMVkreDMKR2xHNzlmVWozMjEy 65 | YlFScWhHcWw5MHhCTEFHSm81cW9ZT1U2YUxReW5McUVDWmdXT0RJMzIxSVl1b01QUm1QcwpTSU1w 66 | TC9xQTRocHNrVGlNVHZnUC9DYXUzRkhWRW1GQWVUdm1PKzI2TWhTdWdMWGcvUWN6Ci0tLS0tRU5E 67 | IFJTQSBQUklWQVRFIEtFWS0tLS0tCg==' 68 | 69 | JWT_ACCESS_TOKEN_EXPIRES_IN="1h" 70 | JWT_REFRESH_TOKEN_EXPIRES_IN="7d" 71 | 72 | UPLOAD_ADDRESS = "http://localhost:3000" 73 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["@typescript-eslint/eslint-plugin"], 4 | extends: [ 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended", 7 | ], 8 | root: true, 9 | env: { 10 | node: true, 11 | jest: true, 12 | }, 13 | ignorePatterns: [".eslintrc.js", "tsconfig.json"], 14 | rules: { 15 | "@typescript-eslint/interface-name-prefix": "off", 16 | "@typescript-eslint/explicit-function-return-type": "off", 17 | "@typescript-eslint/explicit-module-boundary-types": "off", 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "@typescript-eslint/no-var-requires": "off", 20 | "linebreak-style": [0, "error", "windows"], 21 | "@typescript-eslint/no-unused-vars": "off", 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | build/ 5 | tmp/ 6 | temp/ 7 | 8 | 9 | dist/ 10 | logs/ 11 | files/ 12 | redis/ 13 | mysql/ 14 | docker/ 15 | 16 | ormlogs.log 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | echo "husky lint-staged start" 5 | pnpm lint-staged 6 | echo "husky lint-staged end" 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "tabWidth": 2, 4 | "singleQuote": false, 5 | "trailingComma": "all", 6 | "semi": true 7 | } 8 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/swcrc", 3 | "sourceMaps": true, 4 | "jsc": { 5 | "parser": { 6 | "syntax": "typescript", 7 | "decorators": true, 8 | "dynamicImport": true 9 | }, 10 | "paths": { 11 | "@/*": ["./dist/*"] 12 | }, 13 | "target": "es2022" 14 | }, 15 | "minify": true 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:18 AS development 2 | 3 | #指定工作目录 4 | WORKDIR /usr/src/app 5 | 6 | #复制package.json和pnpm-lock.yaml 到工作目录 这里单独复制是为了利用缓存 7 | COPY package.json ./ 8 | COPY pnpm-lock.yaml ./ 9 | COPY prisma ./prisma 10 | 11 | RUN npm config set registry https://registry.npmmirror.com/ 12 | 13 | RUN npm install -g pnpm 14 | 15 | RUN pnpm install 16 | 17 | RUN pnpx prisma generate 18 | 19 | 20 | COPY . . 21 | 22 | #设置默认环境变量 23 | ARG APP_ENV=development 24 | #暴露环境变量 25 | ENV NODE_ENV=${APP_ENV} 26 | 27 | RUN pnpm build 28 | 29 | FROM node:18 AS production 30 | 31 | 32 | ARG APP_ENV=development 33 | ENV NODE_ENV=${APP_ENV} 34 | 35 | #指定工作目录 36 | WORKDIR /usr/src/app 37 | 38 | RUN npm install -g pnpm 39 | 40 | COPY --from=development /usr/src/app/package.json ./ 41 | COPY --from=development /usr/src/app/pnpm-lock.yaml ./ 42 | COPY --from=development /usr/src/app/dist ./dist 43 | COPY --from=development /usr/src/app/prisma ./prisma 44 | 45 | RUN npm config set registry https://registry.npmmirror.com/ 46 | RUN pnpm install --production 47 | 48 | RUN pnpx prisma generate 49 | 50 | EXPOSE 3000 51 | 52 | 53 | CMD ["node", "dist/main"] 54 | -------------------------------------------------------------------------------- /Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine3.16 AS development 2 | 3 | #指定工作目录 4 | WORKDIR /usr/src/app 5 | 6 | #复制package.json和pnpm-lock.yaml 到工作目录 这里单独复制是为了利用缓存 7 | COPY package.json ./ 8 | COPY pnpm-lock.yaml ./ 9 | COPY prisma ./prisma 10 | 11 | RUN npm config set registry https://registry.npmmirror.com/ 12 | 13 | RUN npm install -g pnpm 14 | 15 | RUN pnpm install 16 | 17 | RUN pnpx prisma generate 18 | 19 | COPY . . 20 | 21 | #设置默认环境变量 22 | ARG APP_ENV=development 23 | #暴露环境变量 24 | ENV NODE_ENV=${APP_ENV} 25 | 26 | RUN pnpm build 27 | 28 | FROM node:18-alpine3.16 AS production 29 | 30 | 31 | ARG APP_ENV=development 32 | ENV NODE_ENV=${APP_ENV} 33 | 34 | #指定工作目录 35 | WORKDIR /usr/src/app 36 | 37 | 38 | RUN npm install -g pnpm 39 | 40 | COPY --from=development /usr/src/app/package.json ./ 41 | COPY --from=development /usr/src/app/pnpm-lock.yaml ./ 42 | COPY --from=development /usr/src/app/dist ./dist 43 | COPY --from=development /usr/src/app/prisma ./prisma 44 | RUN npm config set registry https://registry.npmmirror.com/ 45 | 46 | RUN pnpm install --production 47 | 48 | RUN pnpx prisma generate 49 | 50 | EXPOSE 3000 51 | 52 | 53 | CMD ["node", "dist/main"] 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 写在前面,服务器到期,先关了,等优惠再买一个。 2 | 3 | 1. 该项目有两个版本一个是prisma版本,一个是typeorm版本,你可以切换分支,master分支是typeorm版本,这边推荐使用prisma版本(我会优先修复prisma版本)。 4 | 2. 该项目是cms的后端项目,具体前端代码在VUE3-CMS-TS-PINIA在这里可以访问体验网站。具体后端接口可以看apifox里的接口。 5 | 3. 这边推荐使用docker-compose进行环境搭建,如果你是window,你需要使用WSL,如果不使用docker-compose,需要自行安装mysql、redis。 6 | 4. jwt所需要使用的公钥和私钥,需要自行生成,并且在.env.development文件中配置,如果不想则直接使用默认的即可。 7 | 5. 具体操作步骤,请看后面的安装步骤。 8 | 6. 如果有什么问题,可以在issue中提出,我会尽快回复。 9 | 10 | ## 技术栈 11 | 12 | | | | 技术栈 | 描述 | 13 | | --- | ---------- | ------- | ---- | 14 | | 1 | 后端框架 | nest | 完成 | 15 | | 2 | 数据库 | mysql | 完成 | 16 | | 3 | orm | prisma | 完成 | 17 | | 4 | redis | ioredis | 完成 | 18 | | 5 | docker | docker | 完成 | 19 | | 6 | 日志 | winston | 完成 | 20 | | 7 | 编译器 | swc | 完成 | 21 | | 8 | 鉴权 | jwt | 完成 | 22 | | 9 | rbac | rbac | 完成 | 23 | | 10 | 二维码登录 | qrcode | 完成 | 24 | | ... | ... | ... | ... | 25 | 26 | ## 生成jwt的公钥和私钥 27 | 28 | ### 1.通过docker生成脚本 29 | 30 | ```bash 31 | ./scripts/generate-jwt-keys 32 | ``` 33 | 34 | 将输出类似于此的内容。 您只需要将其添加到.env文件中。 35 | 36 | ``` 37 | 为了设置JWT密钥,请将以下值添加到.env文件中: 38 | JWT_PUBLIC_KEY_BASE64="(long base64 content)" 39 | JWT_PRIVATE_KEY_BASE64="(long base64 content)" 40 | ``` 41 | 42 | ### 2.不通过docker生成脚本 43 | 44 | ```bash 45 | $ cd local 46 | $ ssh-keygen -t rsa -b 2048 -m PEM -f jwtRS256.key 47 | # Don't add passphrase 48 | $ openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub 49 | ``` 50 | 51 | 你应该这些密钥文件保存在`./local`目录中,并使用base64编码密钥: 52 | 53 | ```bash 54 | base64 -i local/jwtRS256.key 55 | 56 | base64 -i local/jwtRS256.key.pub 57 | ``` 58 | 59 | 必须在.env中输入密钥文件的base64: 60 | 61 | ```bash 62 | JWT_PUBLIC_KEY_BASE64=这里填入经过base64编码的公钥 63 | JWT_PRIVATE_KEY_BASE64=这里填入经过base64编码的私钥 64 | ``` 65 | 66 | ## 运行项目 67 | 68 | 你可以使用docker运行项目,也可以不使用docker运行项目。 69 | 这边建议使用docker-compose进行运行,如果不使用docker-compose,需要自行安装mysql、redis。 70 | **另外,对于windows用户,如果你要使用docker,你需要使用WSL,进入到linux环境进行创建项目,否则可能遇到一些问题。** 71 | 72 | ### 对于非Docker用户 73 | 74 | 1. 你需要下载mysql、redis,并且在.env、.env.development、.env.production中配置好数据库和redis的连接信息。 75 | 76 | ```bash 77 | DATABASE_URL="mysql://数据库地址:数据库密码@mysql:3306/demo?timezone=Asia/Shanghai" 78 | REDIS_PORT=6379 79 | REDIS_HOST=redis地址 80 | REDIS_PASSWORD=redis密码 81 | ``` 82 | 83 | 2. 配置数据库、redis之后,你需要在项目根目录下执行下面的命令,运行项目。 84 | 85 | ```bash 86 | # 安装依赖 87 | pnpm i 88 | # 生成prisma类型 89 | pnpx prisma generate 90 | #推送数据到数据库 91 | pnpm seed 92 | #运行项目 93 | pnpm start:dev 94 | ``` 95 | 96 | ### 对于Docker用户 97 | 98 | 1. 生产环境下的启动命令 99 | 100 | ```bash 101 | $ pnpm i 102 | # 生成prisma类型 103 | $ pnpx prisma generate; 104 | # 本机没有安装mysql和redis的情况下(仅第一次运行使用,创建mysql,redis以及app容器) 105 | $ sudo docker-compose -f docker-compose.prod.yml up 106 | # 推送数据到数据库(仅第一次运行使用) 107 | $ pnpm seed 108 | # 本机有安装mysql和redis的情况下(第二次以及往后请执行这个) 109 | $ sudo docker compose -f docker-compose.app.prod.yml up 110 | ``` 111 | 112 | 2. 开发环境下的启动命令 113 | 114 | ```bash 115 | # 安装依赖 116 | $ pnpm i 117 | # 生成prisma类型 118 | $ pnpx prisma generate; 119 | # 打包,生成dist,这一步非常重要哦 120 | $ pnpm build 121 | # 本机没有安装mysql和redis的情况下(仅第一次运行使用,创建mysql,redis以及app容器) 122 | $ sudo docker-compose -f docker-compose.dev.yml up 123 | # 推送数据到数据库(仅第一次运行使用) 124 | $ pnpm seed 125 | # 本机有安装mysql和redis的情况下(第二次以及往后请执行这个) 126 | $ sudo docker compose -f docker-compose.app.dev.yml up 127 | 128 | ``` 129 | 130 | 3. 如果不想使用docker-compose,可以使用下面的命令进行运行(这种方法也需要手动安装redis和Mysql)。 131 | 132 | ```bash 133 | # build image 134 | $ docker build -t my-app . 135 | $ docker run -p 3000:3000 --volume 'pwd':/usr/src/app --env-file .env.development my-app 136 | ``` 137 | 138 | ## Prisma操作 139 | 140 | 具体见prisma官网 141 | -------------------------------------------------------------------------------- /docker-compose.app.dev.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.8" 3 | 4 | services: 5 | app: 6 | container_name: cms 7 | build: 8 | context: . 9 | dockerfile: Dockerfile.dev 10 | target: development #指定构建阶段 11 | args: 12 | - APP_ENV 13 | command: pnpm start:dev 14 | env_file: 15 | - .env.development 16 | environment: 17 | - TZ=Asia/Shanghai 18 | ports: 19 | - 3000:3000 20 | -------------------------------------------------------------------------------- /docker-compose.app.prod.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.8" 3 | 4 | services: 5 | app: 6 | container_name: cms 7 | build: 8 | context: . 9 | dockerfile: Dockerfile.prod 10 | target: production #指定构建阶段 11 | args: 12 | - APP_ENV 13 | env_file: 14 | - .env.production 15 | environment: 16 | - TZ=Asia/Shanghai 17 | ports: 18 | - 3000:3000 19 | -------------------------------------------------------------------------------- /docker-compose.db.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.8" 3 | 4 | services: 5 | mysql: 6 | image: mysql 7 | container_name: mysql 8 | restart: always 9 | privileged: true 10 | environment: 11 | MYSQL_ROOT_PASSWORD: 123456 12 | TZ: Asia/Shanghai 13 | MYSQL_DATABASE: cms 14 | ports: 15 | - 3306:3306 16 | volumes: 17 | - ./docker/mysql/data:/var/lib/mysql 18 | - ./docker/mysql/conf:/etc/mysql/conf.d 19 | - ./docker/mysql/logs:/logs 20 | command: 21 | # 将mysql8.0默认密码策略 修改为 原先 策略 (mysql8.0对其默认策略做了更改 会导致密码无法匹配) 22 | # Modify the Mysql 8.0 default password strategy to the original strategy (MySQL8.0 to change its default strategy will cause the password to be unable to match) 23 | --default-authentication-plugin=mysql_native_password 24 | --character-set-server=utf8mb4 25 | --collation-server=utf8mb4_general_ci 26 | --explicit_defaults_for_timestamp=true 27 | --lower_case_table_names=1 28 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.8" 3 | 4 | services: 5 | app: 6 | container_name: cms 7 | build: 8 | context: . 9 | dockerfile: Dockerfile.dev 10 | target: development #指定构建阶段 11 | args: 12 | - APP_ENV 13 | command: pnpm start:dev 14 | env_file: 15 | - .env.development 16 | environment: 17 | - TZ=Asia/Shanghai 18 | ports: 19 | - 3000:3000 20 | #开发时挂载代码 21 | volumes: 22 | - .:/usr/src/app 23 | depends_on: 24 | - mysql 25 | - redis 26 | mysql: 27 | image: mysql 28 | container_name: mysql 29 | restart: always 30 | privileged: true 31 | environment: 32 | MYSQL_ROOT_PASSWORD: 123456 33 | TZ: Asia/Shanghai 34 | MYSQL_DATABASE: cms 35 | ports: 36 | - 3306:3306 37 | volumes: 38 | # 数据挂载 - Data mounting 39 | - ./docker/mysql/data:/var/lib/mysql 40 | command: 41 | # 将mysql8.0默认密码策略 修改为 原先 策略 (mysql8.0对其默认策略做了更改 会导致密码无法匹配) 42 | # Modify the Mysql 8.0 default password strategy to the original strategy (MySQL8.0 to change its default strategy will cause the password to be unable to match) 43 | --default-authentication-plugin=caching_sha2_password 44 | --character-set-server=utf8mb4 45 | --collation-server=utf8mb4_general_ci 46 | --explicit_defaults_for_timestamp=true 47 | --lower_case_table_names=1 48 | 49 | redis: 50 | restart: always 51 | image: redis:7.2.4 52 | hostname: redis 53 | container_name: redis 54 | privileged: true 55 | ports: 56 | # 端口映射 57 | - 6379:6379 58 | volumes: 59 | - ./docker/redis/data:/data:rw 60 | - ./redis.conf:/usr/local/etc/redis/redis.conf 61 | - ./docker/redis/data/logs:/logs 62 | command: "redis-server /usr/local/etc/redis/redis.conf --requirepass 123456 --appendonly yes" 63 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.8" 3 | 4 | services: 5 | app: 6 | container_name: cms 7 | build: 8 | context: . 9 | dockerfile: Dockerfile.prod 10 | target: production #指定构建阶段 11 | args: 12 | - APP_ENV 13 | env_file: 14 | - .env.production 15 | environment: 16 | - TZ=Asia/Shanghai 17 | ports: 18 | - 3000:3000 19 | depends_on: 20 | - mysql 21 | - redis 22 | mysql: 23 | image: mysql 24 | container_name: mysql 25 | restart: always 26 | privileged: true 27 | environment: 28 | MYSQL_ROOT_PASSWORD: 123456 29 | TZ: Asia/Shanghai 30 | MYSQL_DATABASE: cms 31 | ports: 32 | - 3306:3306 33 | volumes: 34 | # 数据挂载 - Data mounting 35 | - ./docker/mysql/data:/var/lib/mysql 36 | command: 37 | # 将mysql8.0默认密码策略 修改为 原先 策略 (mysql8.0对其默认策略做了更改 会导致密码无法匹配) 38 | # Modify the Mysql 8.0 default password strategy to the original strategy (MySQL8.0 to change its default strategy will cause the password to be unable to match) 39 | --default-authentication-plugin=caching_sha2_password 40 | --character-set-server=utf8mb4 41 | --collation-server=utf8mb4_general_ci 42 | --explicit_defaults_for_timestamp=true 43 | --lower_case_table_names=1 44 | 45 | redis: 46 | restart: always 47 | image: redis:7.2.4 48 | hostname: redis 49 | container_name: redis 50 | privileged: true 51 | ports: 52 | # 端口映射 53 | - 6379:6379 54 | volumes: 55 | - ./docker/redis/data:/data:rw 56 | - ./redis.conf:/usr/local/etc/redis/redis.conf 57 | - ./docker/redis/data/logs:/logs 58 | command: "redis-server /usr/local/etc/redis/redis.conf --requirepass 123456 --appendonly yes" 59 | -------------------------------------------------------------------------------- /docker-compose.redis.yml: -------------------------------------------------------------------------------- 1 | # Use root/example as user/password credentials 2 | version: "3.8" 3 | 4 | services: 5 | redis: 6 | restart: always 7 | image: redis:7.2.4 8 | hostname: redis 9 | container_name: redis 10 | privileged: true 11 | ports: 12 | # 端口映射 13 | - 6379:6379 14 | volumes: 15 | - ./docker/redis/data:/data:rw 16 | - ./redis.conf:/usr/local/etc/redis/redis.conf 17 | - ./docker/redis/data/logs:/logs 18 | command: "redis-server /usr/local/etc/redis/redis.conf --requirepass 123456 --appendonly yes" 19 | -------------------------------------------------------------------------------- /local/jwtRS256.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAvDReVmoUWNDWkYIMfp+MU3wTnNOjP1s5vkA9RtY/dIsEQ3l2 3 | z2ar1SOISjnxNWqaJcj7j32EiyKM9sz7kqKA/MOingZ6wJifMkQqo2dXXGnZ1EUq 4 | ESAr0vs0iq/zyuomQ3Ho0aSjtPIi5IQIzk6aODpu5gpG6GNjUH/bRyA2Z01GLjo6 5 | I5p1/UO/dFshD//I+sHOp34v5+Kj3AQR8+KzgVqzUdaj8xqQFIGRB2QdJwxnLg14 6 | evEvqriK5WuFcWxlnfgTMbJiHbwsjNx6nD5v76c1tH/Z7khu+5xKGwbx8r6J+mPN 7 | Iq43DofTNodPQXVoe670pknrKOGZVoBRDrQKZQIDAQABAoIBAFM3jKcFDK4g2V9J 8 | c6Dhhzi62ikz4zD2c1fOK8oQnco4VdBH+uLF8SCzL2YyrJcD5djjP2g5BcxHoLDX 9 | /jzeIc6h6luZXdmfInUlcv0Bir1QCIM1ed5MrVQCwz63gkKsufKEgX+BHum5Tvhf 10 | 8UzXsJVAMR0CWkwS9iz3+8s9URDndMXMSR08dmdcpjtZKqg6S1stqSuy+77I9cnM 11 | yf6izmoX91InxD85EQKvK4l83+ZImLPmNF/mDmidWpUzlXLAeudE27nT+GxP1VQO 12 | xPRMNRj7yxJKYrdB5WShJ8b9cchex5dPrChVndSTf7akZ5PbHAsIF3DElrx4kWyG 13 | wwFsoFkCgYEAw6+YWNL0vUeYhUKKE1eBqbXJAVKNN2H227u73Qb8BlAtZ+ezlgAg 14 | xrK0WgiAViQJOyvlx4D+7RorFlnHVN59X/ubw1HL15PW8Fwo//MsTgxdJH/rLxHw 15 | cVteFhvzetHwwMAz9cV/F5JCOQB1xLwufRYqEOKintAYKL4B/4JfRQsCgYEA9jZ2 16 | 63YFJxY34/RMyV3GFwXZymeLgUwxCZlgm94a++FGuY82ssnHVz7H6YxpUmgXBc34 17 | wtslbO2P4ghhSnAS/472bRwrth/huOVhC9nzE4xRFMYRjsVVdVf0u8u4aY9wg5Oz 18 | 0AOJJJcHLT9zEdeJLXVHPWgzYQuHQRfGtfmGtE8CgYA6uTA7v5rITnr7ypsK8OCt 19 | YCsg5XgRXamqCS14R8dL0bW+j6653fH4DtrGhfSVZR0MD8FV3GUkHA1AGNMTqezv 20 | 5963f41vdSM0YEPBg2URSu6MrIKUTorcSbHzapHnkqbwAP3WzFy2YyLSXkv5LQu6 21 | Z/NZu9lXVVVarKKG3cXfPwKBgElbNNc2lJZusjxCn1YnkEtvvNHnQ9NEJiAcCIKo 22 | DBYbqnzCxKNEJxZBaEKXSRH6XVGnaoRz0pS/uuy3XnVVKvPlXlpn2EYs/Y2fGej0 23 | CM48MjQkRzroZGvPxuTk747T48vwB9TkGMpEDSlYDCq2svJOTOlo0FYLn0KvmM7D 24 | B3VVAoGALD9cfX+EWj+1R2tjft9EQscTikWcasRIwgBh4zI+yJj8D6Z+U0ppBXko 25 | rJjQd3+uBH8NOqTDAfTNr107nfajqhN8iN4VKp/lKMFzLtSeOnlcbF7eLR41jgYh 26 | HgyAD+80uBFxnkXbeAxjA14yHsd+fxhtmtEB/Dm8GJUFeZXWfPo= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /local/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvDReVmoUWNDWkYIMfp+M 3 | U3wTnNOjP1s5vkA9RtY/dIsEQ3l2z2ar1SOISjnxNWqaJcj7j32EiyKM9sz7kqKA 4 | /MOingZ6wJifMkQqo2dXXGnZ1EUqESAr0vs0iq/zyuomQ3Ho0aSjtPIi5IQIzk6a 5 | ODpu5gpG6GNjUH/bRyA2Z01GLjo6I5p1/UO/dFshD//I+sHOp34v5+Kj3AQR8+Kz 6 | gVqzUdaj8xqQFIGRB2QdJwxnLg14evEvqriK5WuFcWxlnfgTMbJiHbwsjNx6nD5v 7 | 76c1tH/Z7khu+5xKGwbx8r6J+mPNIq43DofTNodPQXVoe670pknrKOGZVoBRDrQK 8 | ZQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "typeCheck": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "leo", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "cross-env NODE_ENV=production nest build ", 9 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 10 | "start:dev": "cross-env NODE_ENV=development nest start --builder swc --watch ", 11 | "start:debug": " nest start --builder swc --debug --watch", 12 | "start:prod": "cross-env NODE_ENV=production node dist/main", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:cov": "jest --coverage", 17 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 18 | "test:e2e": "jest --config ./test/jest-e2e.json", 19 | "prepare": "husky install", 20 | "fix": "eslint . --ext .js,.ts,--fix --ignore-path .gitignore", 21 | "commit": "git add . && pnpm cz", 22 | "reset": "prisma migrate reset", 23 | "seed": "prisma db push && ts-node ./src/seed.ts" 24 | }, 25 | "dependencies": { 26 | "@nestjs/common": "^10.0.0", 27 | "@nestjs/config": "^3.1.1", 28 | "@nestjs/core": "^10.0.0", 29 | "@nestjs/jwt": "^10.1.1", 30 | "@nestjs/mapped-types": "*", 31 | "@nestjs/passport": "^10.0.2", 32 | "@nestjs/platform-express": "^10.0.0", 33 | "@nestjs/swagger": "^7.1.13", 34 | "@nestjs/throttler": "^5.0.1", 35 | "@nestjs/typeorm": "^10.0.0", 36 | "@prisma/client": "5.6.0", 37 | "bcryptjs": "^2.4.3", 38 | "class-transformer": "^0.5.1", 39 | "class-validator": "^0.14.0", 40 | "cross-env": "^7.0.3", 41 | "dayjs": "^1.11.10", 42 | "dompurify": "^3.0.6", 43 | "dotenv": "^16.3.1", 44 | "helmet": "^7.1.0", 45 | "husky": "^8.0.3", 46 | "ioredis": "^5.3.2", 47 | "joi": "^17.11.0", 48 | "jsdom": "^22.1.0", 49 | "multer": "1.4.5-lts.1", 50 | "mysql2": "^3.6.2", 51 | "nest-winston": "^1.9.4", 52 | "passport": "^0.6.0", 53 | "passport-jwt": "^4.0.1", 54 | "passport-local": "^1.0.0", 55 | "qrcode": "^1.5.3", 56 | "reflect-metadata": "^0.1.13", 57 | "rxjs": "^7.8.1", 58 | "swagger-ui-express": "^5.0.0", 59 | "uuid": "^9.0.1", 60 | "winston": "^3.11.0", 61 | "winston-daily-rotate-file": "^4.7.1" 62 | }, 63 | "devDependencies": { 64 | "@nestjs/cli": "^10.0.0", 65 | "@nestjs/schematics": "^10.0.0", 66 | "@nestjs/testing": "^10.0.0", 67 | "@swc/cli": "^0.1.62", 68 | "@swc/core": "^1.3.95", 69 | "@types/bcryptjs": "^2.4.6", 70 | "@types/dompurify": "^3.0.5", 71 | "@types/express": "^4.17.17", 72 | "@types/jest": "^29.5.2", 73 | "@types/multer": "^1.4.10", 74 | "@types/node": "^16.11.10", 75 | "@types/passport-jwt": "^3.0.10", 76 | "@types/passport-local": "^1.0.36", 77 | "@types/qrcode": "^1.5.5", 78 | "@types/supertest": "^2.0.12", 79 | "@types/uuid": "^9.0.7", 80 | "@typescript-eslint/eslint-plugin": "^6.0.0", 81 | "@typescript-eslint/parser": "^6.0.0", 82 | "commitizen": "^4.3.0", 83 | "cz-conventional-changelog": "^3.3.0", 84 | "eslint": "^8.42.0", 85 | "eslint-config-prettier": "^9.0.0", 86 | "eslint-plugin-prettier": "^5.0.0", 87 | "jest": "^29.5.0", 88 | "lint-staged": "^15.0.2", 89 | "pnpm": "^8.10.2", 90 | "prettier": "^3.0.0", 91 | "prisma": "^5.6.0", 92 | "prisma-dbml-generator": "^0.10.0", 93 | "run-script-webpack-plugin": "^0.2.0", 94 | "source-map-support": "^0.5.21", 95 | "supertest": "^6.3.3", 96 | "ts-jest": "^29.1.0", 97 | "ts-loader": "^9.4.3", 98 | "ts-node": "10.7.0", 99 | "tsconfig-paths": "^4.2.0", 100 | "typescript": "^5.0.3" 101 | }, 102 | "jest": { 103 | "moduleFileExtensions": [ 104 | "js", 105 | "json", 106 | "ts" 107 | ], 108 | "rootDir": "src", 109 | "testRegex": ".*\\.spec\\.ts$", 110 | "transform": { 111 | "^.+\\.(t|j)s$": "ts-jest" 112 | }, 113 | "collectCoverageFrom": [ 114 | "**/*.(t|j)s" 115 | ], 116 | "coverageDirectory": "../coverage", 117 | "testEnvironment": "node" 118 | }, 119 | "lint-staged": { 120 | "*.{js,ts}": "eslint --fix" 121 | }, 122 | "config": { 123 | "commitizen": { 124 | "path": "./node_modules/cz-conventional-changelog", 125 | "types": { 126 | "🚀 feat": { 127 | "description": "引入新功能", 128 | "title": "Features" 129 | }, 130 | "🐛 fix": { 131 | "description": "修复bug", 132 | "title": "Bug Fixes" 133 | }, 134 | "📝 docs": { 135 | "description": "撰写文档", 136 | "title": "Documentation" 137 | }, 138 | "💄 style": { 139 | "description": "样式修改", 140 | "title": "Styles" 141 | }, 142 | "💬 text": { 143 | "description": "文案修改", 144 | "title": "Texts" 145 | }, 146 | "💩 poo": { 147 | "description": "重写屎一样的代码", 148 | "title": "Code Poop" 149 | }, 150 | "⚡️ perf": { 151 | "description": "性能优化", 152 | "title": "Performance Improvements" 153 | }, 154 | "✅ test": { 155 | "description": "增加测试", 156 | "title": "Tests" 157 | }, 158 | "🏗 build": { 159 | "description": "影响构建系统或外部依赖项的更改", 160 | "title": "Builds" 161 | }, 162 | "✂️ tool": { 163 | "description": "增加开发快乐值的工具", 164 | "title": "Tools" 165 | }, 166 | "💚 ci": { 167 | "description": "对CI配置文件和脚本的更改(示例范围:Travis, Circle, BrowserStack, SauceLabs)", 168 | "title": "Continuous Integrations" 169 | }, 170 | "🧹 chore": { 171 | "description": "日常杂事", 172 | "title": "Chores" 173 | }, 174 | "⏪ revert": { 175 | "description": "回退历史版本", 176 | "title": "Reverts" 177 | }, 178 | "👥 conflict": { 179 | "description": "修改冲突", 180 | "title": "Conflict" 181 | }, 182 | "🚮 delete": { 183 | "description": "删除文件", 184 | "title": "Delete Files" 185 | }, 186 | "🔖 stash": { 187 | "description": "暂存文件", 188 | "title": "Stash Files" 189 | } 190 | } 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /prisma/dbml/schema.dbml: -------------------------------------------------------------------------------- 1 | //// ------------------------------------------------------ 2 | //// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | //// ------------------------------------------------------ 4 | 5 | Table User { 6 | id Int [pk, increment, note: '用户id'] 7 | name String [unique, not null, note: '用户账号'] 8 | password String [not null, note: '用户密码'] 9 | realName String [note: '用户真名'] 10 | cellphone String [note: '用户手机号码'] 11 | enable Boolean [default: true, note: '是否启用'] 12 | ip String [note: '用户登录ip'] 13 | isDelete Boolean [default: false, note: '是否删除'] 14 | createAt DateTime [default: `now()`, not null, note: '用户创建时间'] 15 | updateAt DateTime [not null, note: '用户更新时间'] 16 | departmentId Int [note: '用户所在部门id'] 17 | department Department 18 | roles user_role [not null, note: '用户所拥有的角色'] 19 | 20 | Note: '用户表' 21 | } 22 | 23 | Table Role { 24 | id Int [pk, increment, note: '角色id'] 25 | name String [unique, not null, note: '角色名称'] 26 | intro String [note: '角色描述'] 27 | enable Boolean [default: true, note: '是否启用'] 28 | isDelete Boolean [default: false, note: '是否删除'] 29 | createAt DateTime [default: `now()`, not null, note: '角色创建时间'] 30 | updateAt DateTime [not null, note: '角色更新时间'] 31 | users user_role [not null, note: '角色下的用户'] 32 | menus role_menu [not null, note: '角色下的菜单'] 33 | 34 | Note: '角色表' 35 | } 36 | 37 | Table Department { 38 | id Int [pk, increment, note: '部门id'] 39 | name String [unique, not null, note: '部门名称'] 40 | leader String [note: '部门领导'] 41 | enable Boolean [default: true, note: '是否启用'] 42 | isDelete Boolean [default: false, note: '是否删除'] 43 | createAt DateTime [default: `now()`, not null, note: '部门创建时间'] 44 | updateAt DateTime [not null, note: '部门更新时间'] 45 | parentId Int [note: '父级id'] 46 | parnet Department [note: '父级部门'] 47 | children Department [not null, note: '子级部门'] 48 | user User [not null, note: '部门下的用户'] 49 | 50 | Note: '部门表' 51 | } 52 | 53 | Table Menu { 54 | id Int [pk, increment, note: '菜单id'] 55 | name String [unique, not null, note: '菜单名称'] 56 | type Int [not null, note: '菜单层级'] 57 | url String [note: '菜单路径'] 58 | icon String [note: '菜单图标'] 59 | sort Int [note: '菜单排序'] 60 | permission String [note: '菜单权限'] 61 | enable Boolean [default: true, note: '是否启用'] 62 | isDelete Boolean [default: false, note: '是否删除'] 63 | createAt DateTime [default: `now()`, not null, note: '菜单创建时间'] 64 | updateAt DateTime [not null, note: '菜单更新时间'] 65 | parentId Int [note: '父级id'] 66 | parent Menu 67 | children Menu [not null] 68 | roles role_menu [not null, note: '菜单下的角色'] 69 | 70 | Note: '菜单表' 71 | } 72 | 73 | Table user_role { 74 | userId Int [not null, note: '用户id'] 75 | user User [not null] 76 | roleId Int [not null, note: '角色id'] 77 | role Role [not null] 78 | 79 | indexes { 80 | (userId, roleId) [pk] 81 | } 82 | 83 | Note: '用户角色表' 84 | } 85 | 86 | Table role_menu { 87 | roleId Int [not null, note: '角色id'] 88 | role Role [not null] 89 | menuId Int [not null, note: '菜单id'] 90 | menu Menu [not null] 91 | 92 | indexes { 93 | (roleId, menuId) [pk] 94 | } 95 | 96 | Note: '角色菜单表' 97 | } 98 | 99 | Table goods_category { 100 | id Int [pk, increment, note: '分类id'] 101 | name String [unique, not null, note: '分类名称'] 102 | enable Boolean [default: true, note: '是否启用'] 103 | isDelete Boolean [default: false, note: '是否删除'] 104 | createAt DateTime [default: `now()`, not null, note: '分类创建时间'] 105 | updateAt DateTime [not null, note: '分类更新时间'] 106 | goods goods_info [not null, note: '分类下的商品'] 107 | 108 | Note: '商品分类表' 109 | } 110 | 111 | Table goods_info { 112 | id Int [pk, increment, note: '商品id'] 113 | name String [not null, note: '商品名称'] 114 | desc String [not null, note: '商品描述'] 115 | oldPrice Float [not null, note: '商品原价'] 116 | newPrice Float [not null, note: '商品现价'] 117 | imgUrl String [not null, note: '商品照片url'] 118 | inventoryCount Int [not null, note: '商品库存'] 119 | saleCount Int [note: '商品销量'] 120 | favorCount Int [note: '商品收藏数'] 121 | address String [not null, note: '商品地址'] 122 | status Boolean [default: true, note: '商品状态'] 123 | isDelete Boolean [default: false, note: '是否删除'] 124 | createAt DateTime [default: `now()`, not null, note: '商品创建时间'] 125 | updateAt DateTime [not null, note: '商品更新时间'] 126 | categoryId Int [note: '商品分类id'] 127 | category goods_category 128 | 129 | Note: '商品信息表' 130 | } 131 | 132 | Ref: User.departmentId > Department.id 133 | 134 | Ref: Department.parentId - Department.id 135 | 136 | Ref: Menu.parentId - Menu.id 137 | 138 | Ref: user_role.userId > User.id 139 | 140 | Ref: user_role.roleId > Role.id 141 | 142 | Ref: role_menu.roleId > Role.id 143 | 144 | Ref: role_menu.menuId > Menu.id 145 | 146 | Ref: goods_info.categoryId > goods_category.id -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "mysql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | binaryTargets = ["native","linux-musl"] 7 | } 8 | 9 | datasource db { 10 | provider = "mysql" 11 | url= env("DATABASE_URL") 12 | } 13 | 14 | 15 | ///用户表 16 | model User{ 17 | ///用户id 18 | id Int @id @default(autoincrement()) 19 | ///用户账号 20 | name String @unique 21 | ///用户密码 22 | password String 23 | ///用户真名 24 | realname String? 25 | ///用户手机号码 26 | cellphone String? 27 | ///是否启用 28 | enable Boolean? @default(true) 29 | ///用户登录ip 30 | ip String? 31 | ///用户头像 32 | avatar String? 33 | ///是否删除 34 | isDelete Boolean? @default(false) 35 | ///用户创建时间 36 | createAt DateTime @default(now()) 37 | ///用户更新时间 38 | updateAt DateTime @updatedAt 39 | 40 | ///用户所在部门id 41 | departmentId Int? 42 | department Department? @relation(fields:[departmentId],references:[id]) 43 | 44 | ///用户所拥有的角色 45 | userRole UserRole[] 46 | 47 | @@index([departmentId]) 48 | @@map("user") 49 | } 50 | 51 | 52 | ///角色表 53 | model Role{ 54 | ///角色id 55 | id Int @id @default(autoincrement()) 56 | ///角色名称 57 | name String @unique 58 | ///角色描述 59 | intro String? 60 | ///是否启用 61 | enable Boolean? @default(true) 62 | ///是否删除 63 | isDelete Boolean? @default(false) 64 | ///角色创建时间 65 | createAt DateTime @default(now()) 66 | ///角色更新时间 67 | updateAt DateTime @updatedAt 68 | 69 | ///角色下的用户 70 | userRole UserRole[] 71 | 72 | ///角色下的菜单 73 | roleMenu RoleMenu[] 74 | 75 | @@map("role") 76 | } 77 | 78 | 79 | 80 | ///部门表 81 | model Department { 82 | ///部门id 83 | id Int @id @default(autoincrement()) 84 | ///部门名称 85 | name String @unique 86 | ///部门领导 87 | leader String? 88 | ///是否启用 89 | enable Boolean? @default(true) 90 | ///是否删除 91 | isDelete Boolean? @default(false) 92 | ///部门创建时间 93 | createAt DateTime @default(now()) 94 | ///部门更新时间 95 | updateAt DateTime @updatedAt 96 | 97 | ///父级id 98 | parentId Int? 99 | ///父级部门 100 | parnet Department? @relation("parent",fields:[parentId],references:[id]) 101 | ///子级部门 102 | children Department[] @relation("parent") 103 | 104 | ///部门下的用户 105 | user User[] 106 | 107 | @@map("department") 108 | } 109 | 110 | 111 | ///菜单表 112 | model Menu{ 113 | ///菜单id 114 | id Int @id @default(autoincrement()) 115 | ///菜单名称 116 | name String @unique 117 | ///菜单层级 118 | type Int 119 | ///菜单路径 120 | url String? 121 | ///菜单图标 122 | icon String? 123 | ///菜单排序 124 | sort Int? 125 | ///菜单权限 126 | permission String? 127 | ///是否启用 128 | enable Boolean? @default(true) 129 | ///是否删除 130 | isDelete Boolean? @default(false) 131 | ///菜单创建时间 132 | createAt DateTime @default(now()) 133 | ///菜单更新时间 134 | updateAt DateTime @updatedAt 135 | 136 | ///父级id 137 | parentId Int? 138 | parent Menu? @relation("parent",fields:[parentId],references:[id]) 139 | children Menu[] @relation("parent") 140 | 141 | ///菜单下的角色 142 | roleMenu RoleMenu[] 143 | 144 | @@map("menu") 145 | } 146 | 147 | 148 | ///用户角色表 149 | model UserRole{ 150 | ///用户id 151 | userId Int 152 | user User @relation(fields:[userId],references:[id]) 153 | ///角色id 154 | roleId Int 155 | role Role @relation(fields:[roleId],references:[id]) 156 | 157 | @@index([userId]) 158 | @@index([roleId]) 159 | ///联合主键 160 | @@id([userId,roleId]) 161 | @@map("user_role") 162 | } 163 | 164 | ///角色菜单表 165 | model RoleMenu{ 166 | ///角色id 167 | roleId Int 168 | role Role @relation(fields:[roleId],references:[id]) 169 | ///菜单id 170 | menuId Int 171 | menu Menu @relation(fields:[menuId],references:[id]) 172 | ///联合主键 173 | 174 | @@index([roleId]) 175 | @@index([menuId]) 176 | @@id([roleId,menuId]) 177 | @@map("role_menu") 178 | } 179 | 180 | 181 | ///商品分类表 182 | model GoodsCategory{ 183 | ///分类id 184 | id Int @id @default(autoincrement()) 185 | ///分类名称 186 | name String @unique 187 | ///是否启用 188 | enable Boolean? @default(true) 189 | ///是否删除 190 | isDelete Boolean? @default(false) 191 | ///分类创建时间 192 | createAt DateTime @default(now()) 193 | ///分类更新时间 194 | updateAt DateTime @updatedAt 195 | 196 | ///分类下的商品 197 | goods GoodsInfo[] 198 | 199 | @@map("goods_category") 200 | } 201 | 202 | 203 | ///商品信息表 204 | model GoodsInfo{ 205 | ///商品id 206 | id Int @id @default(autoincrement()) 207 | ///商品名称 208 | name String 209 | ///商品描述 210 | desc String 211 | ///商品原价 212 | oldPrice Float 213 | ///商品现价 214 | newPrice Float 215 | ///商品照片url 216 | imgUrl String 217 | ///商品库存 218 | inventoryCount Int 219 | ///商品销量 220 | saleCount Int? 221 | ///商品收藏数 222 | favorCount Int? 223 | ///商品地址 224 | address String 225 | ///商品状态 226 | status Boolean? @default(true) 227 | ///是否删除 228 | isDelete Boolean? @default(false) 229 | ///商品创建时间 230 | createAt DateTime @default(now()) 231 | ///商品更新时间 232 | updateAt DateTime @updatedAt 233 | 234 | ///商品分类id 235 | categoryId Int? 236 | category GoodsCategory? @relation(fields:[categoryId],references:[id]) 237 | 238 | @@index([categoryId]) 239 | @@map("goods_info") 240 | } 241 | 242 | ///故事表 243 | model Story{ 244 | ///故事id 245 | id Int @id @default(autoincrement()) 246 | ///故事标题 247 | title String 248 | ///故事内容 249 | content String @db.Text 250 | 251 | ///是否删除 252 | isDelete Boolean? @default(false) 253 | 254 | ///是否启用 255 | enable Boolean? @default(true) 256 | 257 | ///故事创建时间 258 | createAt DateTime @default(now()) 259 | 260 | ///故事更新时间 261 | updateAt DateTime @updatedAt 262 | 263 | @@map("story") 264 | } 265 | -------------------------------------------------------------------------------- /scripts/generate-jwt-keys: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | echo " 5 | FROM alpine:3 6 | RUN apk add --no-cache openssh openssl && apk add --update coreutils 7 | " | docker build -t localhost/generate-jwt-keys - 8 | 9 | docker run -it --rm localhost/generate-jwt-keys /bin/sh -c ' 10 | ssh-keygen -t rsa -b 2048 -m PEM -P "" -f jwtRS256.key 11 | 12 | openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub 13 | 14 | echo -e "\e[32m\e[1mTo setup the JWT keys, please add the following values to your .env file:\e[0m" 15 | echo "JWT_PUBLIC_KEY_BASE64=\"$(base64 -i -w 0 jwtRS256.key.pub)\"" 16 | echo "JWT_PRIVATE_KEY_BASE64=\"$(base64 -i -w 0 jwtRS256.key)\"" 17 | ' 18 | -------------------------------------------------------------------------------- /src/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv extends ENV {} 3 | } 4 | 5 | interface ENV { 6 | NODE_ENV: "development" | "production" | "test"; 7 | APP_PORT: number; 8 | APP_ENV: string; 9 | DB_HOST: string; 10 | DB_PORT: number; 11 | 12 | DB_USERNAME: string; 13 | DB_DATABASE: string; 14 | DB_PASSWORD: string; 15 | 16 | DB_SYNC: boolean; 17 | 18 | LOG_LEVEL: string; 19 | TIMESTAMP: boolean; 20 | 21 | LOG_ON: boolean; 22 | 23 | JWT_PUBLIC_KEY: string; 24 | 25 | JWT_PRIVATE_KEY: string; 26 | 27 | JWT_ACCESS_TOKEN_EXPIRES_IN: string; 28 | JWT_REFRESH_TOKEN_EXPIRES_IN: string; 29 | UPLOAD_ADDRESS: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/@types/wrapper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper type used to circumvent ESM modules circular dependency issue 3 | * caused by reflection metadata saving the type of the property. 4 | */ 5 | export type WrapperType = T; 6 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 12:37:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-17 10:14:47 6 | * @FilePath: \cms\src\app.controller.ts 7 | * @Description: 8 | */ 9 | import { Controller } from "@nestjs/common"; 10 | 11 | @Controller() 12 | export class AppController {} 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 12:37:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:50:47 6 | * @FilePath: \cms\src\app.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | 11 | import { AppController } from "./app.controller"; 12 | import { AppService } from "./app.service"; 13 | 14 | import { AuthModule } from "./modules/auth/auth.module"; 15 | import { DepartmentModule } from "./modules/department/department.module"; 16 | import { GoodsCategoryModule } from "./modules/goods-category/goods-category.module"; 17 | import { GoodsInfoModule } from "./modules/goods-info/goods-info.module"; 18 | import { MenusModule } from "./modules/menus/menus.module"; 19 | import { QrcodeModule } from "./modules/qrcode/qrcode.module"; 20 | import { RolesModule } from "./modules/roles/roles.module"; 21 | import { StoryModule } from "./modules/story/story.module"; 22 | import { UsersModule } from "./modules/users/users.module"; 23 | import { SharedModule } from "./shared/shared.module"; 24 | 25 | @Module({ 26 | imports: [ 27 | SharedModule, 28 | AuthModule, 29 | UsersModule, 30 | RolesModule, 31 | MenusModule, 32 | DepartmentModule, 33 | GoodsInfoModule, 34 | GoodsCategoryModule, 35 | StoryModule, 36 | QrcodeModule, 37 | ], 38 | controllers: [AppController], 39 | providers: [AppService], 40 | }) 41 | export class AppModule {} 42 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 12:37:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:54:01 6 | * @FilePath: \cms\src\app.service.ts 7 | * @Description: 8 | */ 9 | import { Injectable } from "@nestjs/common"; 10 | 11 | @Injectable() 12 | export class AppService {} 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 12:37:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-13 15:34:06 6 | * @FilePath: \cms\src\main.ts 7 | * @Description: 8 | */ 9 | import { ValidationPipe } from "@nestjs/common"; 10 | import { ConfigService } from "@nestjs/config"; 11 | import { NestFactory } from "@nestjs/core"; 12 | import { NestExpressApplication } from "@nestjs/platform-express"; 13 | import helmet from "helmet"; 14 | import * as path from "path"; 15 | import { AppModule } from "./app.module"; 16 | import { EnvEnum } from "./shared/enums"; 17 | import { setGlobalApp } from "./shared/global"; 18 | import { setupLogger } from "./shared/logger"; 19 | import { setupSwagger } from "./swagger"; 20 | 21 | async function bootstrap() { 22 | const app = await NestFactory.create(AppModule, { 23 | logger: setupLogger(), 24 | }); 25 | app.useStaticAssets(path.resolve(__dirname, "../files"), { 26 | prefix: "/api/v1/static/", 27 | }); 28 | 29 | app.use(helmet()); 30 | 31 | //swagger 32 | setupSwagger(app); 33 | 34 | app.setGlobalPrefix("/api/v1"); 35 | //全局验证管道 36 | app.useGlobalPipes( 37 | new ValidationPipe({ 38 | whitelist: true, 39 | transform: true, 40 | }), 41 | ); 42 | const configService = app.get(ConfigService); 43 | await app.listen(configService.get(EnvEnum.APP_PORT)); 44 | 45 | setGlobalApp(app); 46 | } 47 | bootstrap(); 48 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-16 13:11:58 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 12:17:08 6 | * @FilePath: \cms\src\modules\auth\auth.controller.ts 7 | * @Description: 8 | */ 9 | 10 | import { GetCurrentUserID, Public } from "@/shared/decorators"; 11 | import { BaseApiErrorResponse, SwaggerBaseApiResponse } from "@/shared/dtos"; 12 | import { JwtAccessGuard, JwtRefreshGuard } from "@/shared/guards"; 13 | import { AppLoggerSevice } from "@/shared/logger"; 14 | import { 15 | Body, 16 | ClassSerializerInterceptor, 17 | Controller, 18 | Get, 19 | HttpCode, 20 | HttpStatus, 21 | Post, 22 | Req, 23 | UseGuards, 24 | UseInterceptors, 25 | } from "@nestjs/common"; 26 | import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; 27 | import type { Request } from "express"; 28 | import { AuthService } from "./auth.service"; 29 | import { ExportLoginDto } from "./dtos/export-login.dto"; 30 | import { LoginAccountDto } from "./dtos/login-account.dto"; 31 | 32 | @ApiTags("用户登录注册模块") 33 | @Controller() 34 | export class AuthController { 35 | constructor( 36 | private readonly authService: AuthService, 37 | private readonly logger: AppLoggerSevice, 38 | ) { 39 | this.logger.setContext(AuthController.name); 40 | } 41 | 42 | /** 43 | * 用户登录 44 | * @param loginAccountDto 登录信息 45 | * @returns 46 | */ 47 | @Post("/login") 48 | @ApiOperation({ 49 | summary: "用户登录", 50 | }) 51 | @ApiResponse({ 52 | status: HttpStatus.OK, 53 | description: "登录成功", 54 | type: SwaggerBaseApiResponse(ExportLoginDto), 55 | }) 56 | @ApiResponse({ 57 | status: HttpStatus.UNAUTHORIZED, 58 | description: "用户名或密码错误", 59 | type: BaseApiErrorResponse, 60 | }) 61 | @HttpCode(HttpStatus.OK) 62 | @Public() 63 | @UseInterceptors(ClassSerializerInterceptor) 64 | login(@Body() loginAccountDto: LoginAccountDto, @Req() req: Request) { 65 | return this.authService.login(loginAccountDto, req); 66 | } 67 | 68 | @Get("/test") 69 | @HttpCode(HttpStatus.OK) 70 | @UseGuards(JwtAccessGuard) 71 | testLogin() { 72 | return "test"; 73 | } 74 | 75 | @Post("/refresh-token") 76 | @UseGuards(JwtRefreshGuard) 77 | @HttpCode(HttpStatus.OK) 78 | refreshToken(@GetCurrentUserID() id: string) { 79 | return this.authService.refreshToken(+id); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-16 13:11:58 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 19:57:06 6 | * @FilePath: \cms\src\modules\auth\auth.module.ts 7 | * @Description: 8 | */ 9 | import { EnvEnum, StrategyEnum } from "@/shared/enums"; 10 | import { Module } from "@nestjs/common"; 11 | import { ConfigService } from "@nestjs/config"; 12 | import { JwtModule } from "@nestjs/jwt"; 13 | import { PassportModule } from "@nestjs/passport"; 14 | import { UsersService } from "../users/users.service"; 15 | import { AuthController } from "./auth.controller"; 16 | import { AuthService } from "./auth.service"; 17 | import { JwtAccessStrategy } from "./strategies/jwt-access.strategy"; 18 | import { JwtRefreshStrategy } from "./strategies/jwt-refresh.strategy"; 19 | import { LocalStrategy } from "./strategies/local.strategy"; 20 | 21 | @Module({ 22 | imports: [ 23 | PassportModule.register({ defaultStrategy: StrategyEnum.JWT_ACCESS }), 24 | JwtModule.registerAsync({ 25 | useFactory: async (configService: ConfigService) => ({ 26 | publicKey: Buffer.from( 27 | configService.get(EnvEnum.JWT_PUBLIC_KEY), 28 | "base64", 29 | ).toString("utf-8"), 30 | privateKey: Buffer.from( 31 | configService.get(EnvEnum.JWT_PRIVATE_KEY), 32 | "base64", 33 | ).toString("utf-8"), 34 | signOptions: { 35 | algorithm: "RS256", 36 | }, 37 | }), 38 | inject: [ConfigService], 39 | }), 40 | ], 41 | controllers: [AuthController], 42 | providers: [ 43 | AuthService, 44 | UsersService, 45 | ConfigService, 46 | LocalStrategy, 47 | JwtAccessStrategy, 48 | JwtRefreshStrategy, 49 | ], 50 | }) 51 | export class AuthModule {} 52 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-16 13:11:58 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-19 19:26:09 6 | * @FilePath: \cms\src\modules\auth\auth.service.ts 7 | * @Description: 8 | */ 9 | import { EnvEnum, RedisKeyEnum } from "@/shared/enums"; 10 | import { JwtPayloadInterface } from "@/shared/interfaces"; 11 | import { AppLoggerSevice } from "@/shared/logger"; 12 | import { RedisService } from "@/shared/redis"; 13 | import { BadRequestException, Injectable } from "@nestjs/common"; 14 | import { ConfigService } from "@nestjs/config"; 15 | import { JwtService } from "@nestjs/jwt"; 16 | import { plainToClass } from "class-transformer"; 17 | import type { Request } from "express"; 18 | import { UsersService } from "../users/users.service"; 19 | import { ExportLoginDto } from "./dtos/export-login.dto"; 20 | import { LoginAccountDto } from "./dtos/login-account.dto"; 21 | import { getClientIp } from "@/shared/utils"; 22 | 23 | @Injectable() 24 | export class AuthService { 25 | constructor( 26 | private readonly logger: AppLoggerSevice, 27 | private readonly userService: UsersService, 28 | private readonly jwtService: JwtService, 29 | private readonly configService: ConfigService, 30 | private readonly redisService: RedisService, 31 | ) { 32 | this.logger.setContext(AuthService.name); 33 | } 34 | 35 | /** 36 | * 登录 37 | * @param loginAccountDto 登录信息 38 | * @returns 39 | */ 40 | async login(loginAccountDto: LoginAccountDto, req: Request) { 41 | this.logger.log(`${this.login.name} was called`); 42 | 43 | //1.验证用户信息 44 | const user = await this.userService.validateUser( 45 | loginAccountDto.name, 46 | loginAccountDto.password, 47 | ); 48 | const ip = getClientIp(req); 49 | // 2.记录用户登录ip 50 | this.userService.recordUserIp(user.id, ip); 51 | 52 | const roleId = user.userRole[0]?.roleId; 53 | 54 | if (!roleId) { 55 | throw new BadRequestException("用户缺少部门"); 56 | } 57 | 58 | //3.生成token 59 | const { accessToken } = this.getAccessAndRefreshToken( 60 | user.id, 61 | user.name, 62 | roleId, 63 | ); 64 | 65 | this.redisService.setex( 66 | RedisKeyEnum.LoginKey + user.id, 67 | 60 * 60, 68 | accessToken, 69 | ); 70 | 71 | return { 72 | id: user.id, 73 | name: user.name, 74 | token: accessToken, 75 | }; 76 | } 77 | 78 | /** 79 | * 刷新token 80 | * @param id 用户id 81 | * @returns 82 | */ 83 | async refreshToken(id: number) { 84 | this.logger.log(`${this.refreshToken.name} was called`); 85 | const user = await this.userService.findUserById(id); 86 | const roleId = user.role.id; 87 | return this.getAccessAndRefreshToken(user.id, user.name, roleId); 88 | } 89 | 90 | /** 91 | * 获取access_token和refresh_token 92 | * @param id 用户id 93 | * @param name 用户名 94 | * @returns 95 | */ 96 | getAccessAndRefreshToken(id: number, name: string, roleId: number) { 97 | this.logger.log(`${this.getAccessAndRefreshToken.name} was called`); 98 | const payload = { id, name, roleId } as JwtPayloadInterface; 99 | return plainToClass(ExportLoginDto, { 100 | accessToken: this.jwtService.sign(payload, { 101 | expiresIn: this.configService.get(EnvEnum.JWT_ACCESS_TOKEN_EXPIRES_IN), 102 | }), 103 | refreshToken: this.jwtService.sign( 104 | { id }, 105 | { 106 | expiresIn: this.configService.get( 107 | EnvEnum.JWT_REFRESH_TOKEN_EXPIRES_IN, 108 | ), 109 | }, 110 | ), 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/export-login.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-18 18:00:08 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-18 20:09:38 6 | * @FilePath: \cms\src\modules\auth\dto\export-login.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | 11 | export class ExportLoginDto { 12 | @ApiProperty({ 13 | description: "accessToken", 14 | }) 15 | accessToken: string; 16 | 17 | @ApiProperty({ 18 | description: "refreshToken", 19 | }) 20 | refreshToken: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/login-account.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:12:04 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:30:59 6 | * @FilePath: \cms\src\modules\auth\dtos\login-account.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsNotEmpty, IsString, MaxLength } from "class-validator"; 11 | 12 | export class LoginAccountDto { 13 | @ApiProperty({ description: "用户名", type: String }) 14 | @IsNotEmpty({ message: "用户名不能为空" }) 15 | @IsString({ message: "用户名必须是字符串" }) 16 | @MaxLength(32, { message: "用户名长度不能超过32个字符" }) 17 | name: string; 18 | 19 | @ApiProperty({ description: "密码", type: String }) 20 | @IsNotEmpty({ message: "密码不能为空" }) 21 | @IsString({ message: "密码必须是字符串" }) 22 | password: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/auth/dtos/register-account.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:12:21 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:31:10 6 | * @FilePath: \cms\src\modules\auth\dtos\register-account.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsNotEmpty, IsString, MaxLength } from "class-validator"; 11 | 12 | export class RegisterAccountDto { 13 | @ApiProperty({ description: "用户名", type: String }) 14 | @IsNotEmpty({ message: "用户名不能为空" }) 15 | @IsString({ message: "用户名必须是字符串" }) 16 | @MaxLength(32, { message: "用户名长度不能超过32个字符" }) 17 | name: string; 18 | 19 | @ApiProperty({ description: "密码", type: String }) 20 | @IsNotEmpty({ message: "密码不能为空" }) 21 | @IsString({ message: "密码必须是字符串" }) 22 | password: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/jwt-access.strategy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 15:56:31 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 10:00:54 6 | * @FilePath: \cms\src\modules\auth\strategies\jwt-access.strategy.ts 7 | * @Description: 8 | */ 9 | 10 | import { EnvEnum, StrategyEnum } from "@/shared/enums"; 11 | import { JwtPayloadInterface } from "@/shared/interfaces"; 12 | import { Injectable } from "@nestjs/common"; 13 | import { ConfigService } from "@nestjs/config"; 14 | import { PassportStrategy } from "@nestjs/passport"; 15 | import { ExtractJwt, Strategy } from "passport-jwt"; 16 | 17 | @Injectable() 18 | export class JwtAccessStrategy extends PassportStrategy( 19 | Strategy, 20 | StrategyEnum.JWT_ACCESS, 21 | ) { 22 | constructor(private readonly configService: ConfigService) { 23 | super({ 24 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 25 | secretOrKey: Buffer.from( 26 | configService.get(EnvEnum.JWT_PUBLIC_KEY), 27 | "base64", 28 | ).toString("utf-8"), 29 | algorithms: ["RS256"], 30 | }); 31 | } 32 | 33 | async validate(payload): Promise { 34 | return { 35 | id: payload.id, 36 | name: payload.name, 37 | roleId: payload.roleId, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/jwt-refresh.strategy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 15:56:50 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-18 11:02:39 6 | * @FilePath: \cms\src\modules\auth\strategies\jwt-refresh.strategy.ts 7 | * @Description: 8 | */ 9 | import { EnvEnum, StrategyEnum } from "@/shared/enums"; 10 | import { Injectable } from "@nestjs/common"; 11 | import { ConfigService } from "@nestjs/config"; 12 | import { PassportStrategy } from "@nestjs/passport"; 13 | import { ExtractJwt, Strategy } from "passport-jwt"; 14 | 15 | @Injectable() 16 | export class JwtRefreshStrategy extends PassportStrategy( 17 | Strategy, 18 | StrategyEnum.JWT_REFRESH, 19 | ) { 20 | constructor(private readonly configService: ConfigService) { 21 | super({ 22 | jwtFromRequest: ExtractJwt.fromBodyField("refreshToken"), 23 | secretOrKey: Buffer.from( 24 | configService.get(EnvEnum.JWT_PUBLIC_KEY), 25 | "base64", 26 | ).toString("utf-8"), 27 | algorithms: ["RS256"], 28 | }); 29 | } 30 | 31 | async validate(payload) { 32 | return { 33 | id: payload.id, 34 | }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/auth/strategies/local.strategy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 15:57:05 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 12:15:37 6 | * @FilePath: \cms\src\modules\auth\strategies\local.strategy.ts 7 | * @Description: 8 | */ 9 | import { UsersService } from "@/modules/users/users.service"; 10 | import { StrategyEnum } from "@/shared/enums"; 11 | import { AppLoggerSevice } from "@/shared/logger"; 12 | import { Injectable } from "@nestjs/common"; 13 | import { PassportStrategy } from "@nestjs/passport"; 14 | import { Request } from "express"; 15 | import { Strategy } from "passport-local"; 16 | 17 | @Injectable() 18 | export class LocalStrategy extends PassportStrategy( 19 | //这里的Strategy是passport-local的Strategy 而不是passport-jwt的Strategy 20 | Strategy, 21 | StrategyEnum.LOCAL, 22 | ) { 23 | constructor( 24 | private readonly usersService: UsersService, 25 | private readonly logger: AppLoggerSevice, 26 | ) { 27 | super({ 28 | usernameField: "name", 29 | passwordField: "password", 30 | passReqToCallback: true, 31 | }); 32 | this.logger.setContext(LocalStrategy.name); 33 | } 34 | 35 | async validate(request: Request, name: string, password: string) { 36 | this.logger.log(`${this.validate.name} was called`); 37 | this.logger.warn(`name: ${name} password: ${password}`, this.validate.name); 38 | const user = await this.usersService.validateUser(name, password); 39 | //如果验证通过 自动会在request上添加user属性 40 | return user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/department/department.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:31:56 6 | * @FilePath: \cms\src\modules\department\department.controller.ts 7 | * @Description: 8 | */ 9 | import { RequirePermission } from "@/shared/decorators"; 10 | import { PermissionEnum } from "@/shared/enums"; 11 | import { 12 | Body, 13 | Controller, 14 | Delete, 15 | Get, 16 | HttpCode, 17 | HttpStatus, 18 | Param, 19 | Patch, 20 | Post, 21 | } from "@nestjs/common"; 22 | import { ApiTags } from "@nestjs/swagger"; 23 | import { DepartmentService } from "./department.service"; 24 | import { CreateDepartmentDto } from "./dto/create-department.dto"; 25 | import { QueryDepartmentDto } from "./dto/query-department.dto"; 26 | import { UpdateDepartmentDto } from "./dto/update-department.dto"; 27 | 28 | @Controller("department") 29 | @ApiTags("部门管理模块") 30 | export class DepartmentController { 31 | constructor(private readonly departmentService: DepartmentService) {} 32 | 33 | /** 34 | * 创建部门 35 | * @param createDepartmentDto 创建信息 36 | * @returns 37 | */ 38 | @Post() 39 | @HttpCode(HttpStatus.OK) 40 | @RequirePermission(PermissionEnum.SYSTEM_DEPARTMENT_CREATE) 41 | create(@Body() createDepartmentDto: CreateDepartmentDto) { 42 | return this.departmentService.create(createDepartmentDto); 43 | } 44 | 45 | /** 46 | * 查询部门列表 47 | * @param queryDepartmentDto 查询条件 48 | * @returns 49 | */ 50 | @Post("list") 51 | @HttpCode(HttpStatus.OK) 52 | findAll(@Body() queryDepartmentDto: QueryDepartmentDto) { 53 | return this.departmentService.findAll(queryDepartmentDto); 54 | } 55 | 56 | /** 57 | * 查询部门 58 | * @param id 部门id 59 | * @returns 60 | */ 61 | @Get(":id") 62 | @RequirePermission(PermissionEnum.SYSTEM_DEPARTMENT_QUERY) 63 | findOne(@Param("id") id: string) { 64 | return this.departmentService.findOne(+id); 65 | } 66 | 67 | /** 68 | * 更新部门 69 | * @param id 部门id 70 | * @param updateDepartmentDto 更新信息 71 | * @returns 72 | */ 73 | @Patch(":id") 74 | @RequirePermission(PermissionEnum.SYSTEM_DEPARTMENT_UPDATE) 75 | update( 76 | @Param("id") id: string, 77 | @Body() updateDepartmentDto: UpdateDepartmentDto, 78 | ) { 79 | return this.departmentService.update(+id, updateDepartmentDto); 80 | } 81 | 82 | /** 83 | * 删除部门 84 | * @param id 部门id 85 | * @returns 86 | */ 87 | @Delete(":id") 88 | @RequirePermission(PermissionEnum.SYSTEM_DEPARTMENT_DELETE) 89 | remove(@Param("id") id: string) { 90 | return this.departmentService.remove(+id); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/modules/department/department.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:31:52 6 | * @FilePath: \cms\src\modules\department\department.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | import { DepartmentController } from "./department.controller"; 11 | import { DepartmentService } from "./department.service"; 12 | 13 | @Module({ 14 | imports: [], 15 | controllers: [DepartmentController], 16 | providers: [DepartmentService], 17 | exports: [DepartmentService], 18 | }) 19 | export class DepartmentModule {} 20 | -------------------------------------------------------------------------------- /src/modules/department/dto/create-department.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:31:26 6 | * @FilePath: \cms\src\modules\department\dto\create-department.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; 11 | 12 | export class CreateDepartmentDto { 13 | @ApiProperty({ description: "部门名称", example: "研发部", type: String }) 14 | @IsString({ message: "部门名称必须为字符类型" }) 15 | @IsNotEmpty({ message: "部门名称不能为空" }) 16 | name: string; 17 | 18 | @ApiProperty({ description: "父级id", example: 1, type: Number }) 19 | @IsInt({ message: "父级id必须为数字类型" }) 20 | @IsOptional() 21 | parentId: number; 22 | 23 | @ApiProperty({ description: "部门领导", example: "张三", type: String }) 24 | @IsString({ message: "部门领导必须为字符类型" }) 25 | @IsOptional() 26 | leader: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/department/dto/export-department-list.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 12:05:11 6 | * @FilePath: \cms\src\modules\department\dto\export-department-list.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | import { ExportDepartmentDto } from "./export-department.dto"; 12 | 13 | export class ExportDepartmentListDto { 14 | @ApiProperty({ 15 | type: [ExportDepartmentDto], 16 | description: "部门列表", 17 | example: [], 18 | }) 19 | @Expose() 20 | @Type(() => ExportDepartmentDto) 21 | list: ExportDepartmentDto[]; 22 | 23 | @ApiProperty({ type: Number, description: "部门总数", example: 1 }) 24 | @Expose() 25 | totalCount: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/department/dto/export-department.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 10:56:11 6 | * @FilePath: \cms\src\modules\department\dto\export-department.dto.ts 7 | * @Description: 8 | */ 9 | import { formatTime } from "@/shared/utils"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { Expose, Transform, Type } from "class-transformer"; 12 | 13 | export class ExportDepartmentDto { 14 | @ApiProperty({ description: "部门id", example: 1, type: Number }) 15 | @Expose() 16 | id: number; 17 | 18 | @ApiProperty({ description: "部门名称", example: "研发部", type: String }) 19 | @Expose() 20 | name: string; 21 | 22 | @ApiProperty({ description: "父级id", example: 1, type: Number }) 23 | @Expose() 24 | parentId: number; 25 | 26 | @ApiProperty({ description: "部门领导", example: "张三", type: String }) 27 | @Expose() 28 | leader: string; 29 | 30 | @ApiProperty({ description: "是否启用", example: 10, type: Number }) 31 | @Expose() 32 | @Type(() => Number) 33 | enable: number; 34 | 35 | @ApiProperty({ 36 | description: "创建时间", 37 | example: "2021-07-01 00:00:00", 38 | type: Date, 39 | }) 40 | @Expose() 41 | @Transform(({ value }) => formatTime(value)) 42 | createAt: Date; 43 | 44 | @ApiProperty({ 45 | description: "更新时间", 46 | example: "2021-07-01 00:00:00", 47 | type: Date, 48 | }) 49 | @Expose() 50 | @Transform(({ value }) => formatTime(value)) 51 | updateAt: Date; 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/department/dto/query-department.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:31:41 6 | * @FilePath: \cms\src\modules\department\dto\query-department.dto.ts 7 | * @Description: 8 | */ 9 | import { BaseQueryDto } from "@/shared/dtos"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { Type } from "class-transformer"; 12 | import { IsNumber, IsOptional, IsString } from "class-validator"; 13 | 14 | export class QueryDepartmentDto extends BaseQueryDto { 15 | @ApiProperty({ name: "部门名称", example: "技术部", type: String }) 16 | @IsString({ message: "部门名称必须是字符串" }) 17 | @IsOptional() 18 | name: string; 19 | 20 | @ApiProperty({ name: "父级id", example: 1, type: Number }) 21 | @IsNumber({}, { message: "父级id必须是数字" }) 22 | @Type(() => Number) 23 | @IsOptional() 24 | parentId: number; 25 | 26 | @ApiProperty({ name: "部门领导", example: "张三", type: String }) 27 | @IsString({ message: "部门领导必须是字符串" }) 28 | @IsOptional() 29 | leader: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/department/dto/update-department.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:31:48 6 | * @FilePath: \cms\src\modules\department\dto\update-department.dto.ts 7 | * @Description: 8 | */ 9 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 10 | import { ApiProperty, PartialType } from "@nestjs/swagger"; 11 | import { IsOptional } from "class-validator"; 12 | import { CreateDepartmentDto } from "./create-department.dto"; 13 | 14 | export class UpdateDepartmentDto extends PartialType(CreateDepartmentDto) { 15 | @ApiProperty({ 16 | name: "是否启用 ", 17 | example: 0, 18 | type: Number, 19 | description: "0:禁用 1:启用", 20 | }) 21 | @TransformNumber2Boolean() 22 | @IsOptional() 23 | enable: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/goods-category/dto/create-goods-category.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:07 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 12:04:11 6 | * @FilePath: \cms\src\modules\goods-category\dto\create-goods-category.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsNotEmpty, IsString } from "class-validator"; 11 | 12 | export class CreateGoodsCategoryDto { 13 | @ApiProperty({ description: "商品分类名" }) 14 | @IsString({ message: "商品分类名必须是字符串" }) 15 | @IsNotEmpty({ message: "商品分类名不能为空" }) 16 | name: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/goods-category/dto/export-goods-category-list.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 22:35:09 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:32:17 6 | * @FilePath: \cms\src\modules\goods-category\dto\export-goods-category-list.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | import { ExportGoodsCategoryDto } from "./export-goods-category.dto"; 12 | 13 | export class ExportGoodsCategoryListDto { 14 | @ApiProperty({ 15 | description: "商品分类列表", 16 | type: ExportGoodsCategoryDto, 17 | isArray: true, 18 | }) 19 | @Expose() 20 | @Type(() => ExportGoodsCategoryDto) 21 | list: ExportGoodsCategoryDto[]; 22 | 23 | @ApiProperty({ 24 | description: "商品分类总数", 25 | type: Number, 26 | }) 27 | @Expose() 28 | totalCount: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/goods-category/dto/export-goods-category.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 22:29:18 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 11:03:05 6 | * @FilePath: \cms\src\modules\goods-category\dto\export-goods-category.dto.ts 7 | * @Description: 8 | */ 9 | import { formatTime } from "@/shared/utils"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { Expose, Transform, Type } from "class-transformer"; 12 | 13 | export class ExportGoodsCategoryDto { 14 | @ApiProperty({ 15 | description: "分类id", 16 | example: "分类id", 17 | type: Number, 18 | }) 19 | @Expose() 20 | id: number; 21 | 22 | @ApiProperty({ 23 | description: "分类名称", 24 | example: "分类名称", 25 | type: String, 26 | }) 27 | @Expose() 28 | name: string; 29 | 30 | @ApiProperty({ 31 | description: "是否启用", 32 | example: true, 33 | type: Number, 34 | }) 35 | @Type(() => Number) 36 | @Expose() 37 | enable: number; 38 | 39 | @ApiProperty({ 40 | description: "创建时间", 41 | example: "2021-07-01 00:00:00", 42 | type: Date, 43 | }) 44 | @Expose() 45 | @Transform(({ value }) => formatTime(value)) 46 | createAt: Date; 47 | 48 | @ApiProperty({ 49 | description: "更新时间", 50 | example: "2021-07-01 00:00:00", 51 | type: Date, 52 | }) 53 | @Expose() 54 | @Transform(({ value }) => formatTime(value)) 55 | updateAt: Date; 56 | } 57 | -------------------------------------------------------------------------------- /src/modules/goods-category/dto/query-goods-category.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:54:36 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:32:25 6 | * @FilePath: \cms\src\modules\goods-category\dto\query-goods-category.dto.ts 7 | * @Description: 8 | */ 9 | import { BaseQueryDto } from "@/shared/dtos"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { IsOptional, IsString } from "class-validator"; 12 | 13 | export class QueryGoodsCategoryDto extends BaseQueryDto { 14 | @ApiProperty({ name: "分类名称", description: "分类名称", type: String }) 15 | @IsString({ message: "分类名称必须是字符串" }) 16 | @IsOptional() 17 | name: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/goods-category/dto/update-goods-category.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:07 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:32:28 6 | * @FilePath: \cms\src\modules\goods-category\dto\update-goods-category.dto.ts 7 | * @Description: 8 | */ 9 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 10 | import { ApiProperty, PartialType } from "@nestjs/swagger"; 11 | import { IsOptional } from "class-validator"; 12 | import { CreateGoodsCategoryDto } from "./create-goods-category.dto"; 13 | 14 | export class UpdateGoodsCategoryDto extends PartialType( 15 | CreateGoodsCategoryDto, 16 | ) { 17 | @ApiProperty({ 18 | name: "是否启用 ", 19 | example: 0, 20 | type: Number, 21 | description: "0:禁用 1:启用", 22 | }) 23 | @TransformNumber2Boolean() 24 | @IsOptional() 25 | enable: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/goods-category/goods-category.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:07 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:52:30 6 | * @FilePath: \cms\src\modules\goods-category\goods-category.controller.ts 7 | * @Description: 8 | */ 9 | import { 10 | Body, 11 | Controller, 12 | Delete, 13 | Get, 14 | HttpCode, 15 | HttpStatus, 16 | Param, 17 | Patch, 18 | Post, 19 | } from "@nestjs/common"; 20 | import { ApiTags } from "@nestjs/swagger"; 21 | import { CreateGoodsCategoryDto } from "./dto/create-goods-category.dto"; 22 | import { QueryGoodsCategoryDto } from "./dto/query-goods-category.dto"; 23 | import { UpdateGoodsCategoryDto } from "./dto/update-goods-category.dto"; 24 | import { GoodsCategoryService } from "./goods-category.service"; 25 | import { PermissionEnum } from "@/shared/enums"; 26 | import { RequirePermission } from "@/shared/decorators"; 27 | 28 | @Controller("category") 29 | @ApiTags("商品分类模块") 30 | export class GoodsCategoryController { 31 | constructor(private readonly goodsCategoryService: GoodsCategoryService) {} 32 | 33 | @Post() 34 | @HttpCode(HttpStatus.OK) 35 | @RequirePermission(PermissionEnum.SYSTEM_CATEGORY_CREATE) 36 | create(@Body() createGoodsCategoryDto: CreateGoodsCategoryDto) { 37 | return this.goodsCategoryService.create(createGoodsCategoryDto); 38 | } 39 | 40 | @Post("list") 41 | @HttpCode(HttpStatus.OK) 42 | @RequirePermission(PermissionEnum.SYSTEM_CATEGORY_QUERY) 43 | findAll(@Body() queryGoodsCategoryDto: QueryGoodsCategoryDto) { 44 | return this.goodsCategoryService.findAll(queryGoodsCategoryDto); 45 | } 46 | 47 | @Get("all") 48 | findAllCategory() { 49 | return this.goodsCategoryService.findAllCategory(); 50 | } 51 | 52 | @Get(":id") 53 | @RequirePermission(PermissionEnum.SYSTEM_CATEGORY_QUERY) 54 | findOne(@Param("id") id: string) { 55 | return this.goodsCategoryService.findOne(+id); 56 | } 57 | 58 | @Patch(":id") 59 | @RequirePermission(PermissionEnum.SYSTEM_CATEGORY_UPDATE) 60 | update( 61 | @Param("id") id: string, 62 | @Body() updateGoodsCategoryDto: UpdateGoodsCategoryDto, 63 | ) { 64 | return this.goodsCategoryService.update(+id, updateGoodsCategoryDto); 65 | } 66 | 67 | @Delete(":id") 68 | @RequirePermission(PermissionEnum.SYSTEM_CATEGORY_DELETE) 69 | remove(@Param("id") id: string) { 70 | return this.goodsCategoryService.remove(+id); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/modules/goods-category/goods-category.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:07 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 20:05:19 6 | * @FilePath: \cms\src\modules\goods-category\goods-category.module.ts 7 | * @Description: 8 | */ 9 | import { Module, forwardRef } from "@nestjs/common"; 10 | import { GoodsInfoModule } from "../goods-info/goods-info.module"; 11 | import { GoodsCategoryController } from "./goods-category.controller"; 12 | import { GoodsCategoryService } from "./goods-category.service"; 13 | 14 | @Module({ 15 | imports: [forwardRef(() => GoodsInfoModule)], 16 | controllers: [GoodsCategoryController], 17 | providers: [GoodsCategoryService], 18 | exports: [GoodsCategoryService], 19 | }) 20 | export class GoodsCategoryModule {} 21 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/create-goods-info.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 20:59:52 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:32:53 6 | * @FilePath: \cms\src\modules\goods-info\dto\create-goods-info.dto.ts 7 | * @Description: 8 | */ 9 | import { ValidateStringNumber } from "@/shared/decorators"; 10 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 11 | import { ApiProperty } from "@nestjs/swagger"; 12 | import { Type } from "class-transformer"; 13 | import { IsNotEmpty, IsOptional, IsString } from "class-validator"; 14 | 15 | export class CreateGoodsInfoDto { 16 | @ApiProperty({ description: "商品分类ID", example: 8, type: Number }) 17 | @ValidateStringNumber({ message: "商品原价必须是数字或字符串" }) 18 | @IsNotEmpty({ message: "商品原价不能为空" }) 19 | @Type(() => Number) 20 | categoryId: number; 21 | 22 | @ApiProperty({ example: "iPhone 12", description: "商品名称", type: String }) 23 | @IsString({ message: "商品名称必须是字符串" }) 24 | @IsNotEmpty({ message: "商品名称不能为空" }) 25 | name: string; 26 | 27 | @ApiProperty({ example: 9999, description: "商品原价", type: Number }) 28 | @ValidateStringNumber({ message: "商品原价必须是数字或字符串" }) 29 | @IsNotEmpty({ message: "商品原价不能为空" }) 30 | @Type(() => Number) 31 | oldPrice: number; 32 | 33 | @ApiProperty({ example: 8888, description: "商品现价", type: Number }) 34 | @ValidateStringNumber({ message: "商品现价必须是数字或字符串" }) 35 | @IsNotEmpty({ message: "商品现价不能为空" }) 36 | @Type(() => Number) 37 | newPrice: number; 38 | 39 | @ApiProperty({ 40 | example: "The latest iPhone from Apple", 41 | description: "商品描述", 42 | type: String, 43 | }) 44 | @IsString({ message: "商品描述必须是字符串" }) 45 | @IsNotEmpty({ message: "商品描述不能为空" }) 46 | desc: string; 47 | 48 | @ApiProperty({ 49 | example: 1, 50 | description: "商品状态(1:在售,0:下架)", 51 | type: Number, 52 | }) 53 | @TransformNumber2Boolean() 54 | status: boolean; 55 | 56 | @ApiProperty({ 57 | example: "https://www.example.com/iphone12.png", 58 | description: "商品图片 URL", 59 | type: String, 60 | }) 61 | @IsString({ message: "商品图片 URL 必须是字符串" }) 62 | @IsNotEmpty({ message: "商品图片 URL 不能为空" }) 63 | imgUrl: string; 64 | 65 | @ApiProperty({ example: 100, description: "商品库存数量", type: Number }) 66 | @ValidateStringNumber({ message: "商品库存数量必须是数字或字符串" }) 67 | @Type(() => Number) 68 | inventoryCount: number; 69 | 70 | @ApiProperty({ example: 1000, description: "商品销售数量", type: Number }) 71 | @ValidateStringNumber({ message: "商品销售数量必须是数字或字符串" }) 72 | @IsOptional() 73 | @Type(() => Number) 74 | saleCount: number; 75 | 76 | @ApiProperty({ example: 999, description: "商品收藏数量", type: Number }) 77 | @ValidateStringNumber({ message: "商品收藏数量必须是数字或字符串" }) 78 | @Type(() => Number) 79 | @IsOptional() 80 | favorCount: number; 81 | 82 | @ApiProperty({ example: "北京", description: "商品所在地址", type: String }) 83 | @IsString({ message: "商品所在地址必须是字符串" }) 84 | @IsNotEmpty({ message: "商品所在地址不能为空" }) 85 | address: string; 86 | } 87 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-address-sale.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 21:38:27 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:43:11 6 | * @FilePath: \cms\src\modules\goods\dtos\export-address-sale.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | 12 | export class ExportAddressSaleDto { 13 | @ApiProperty({ description: "城市", example: "北京", type: String }) 14 | @Expose() 15 | address: string; 16 | 17 | @ApiProperty({ 18 | description: "城市所对应商品销售数量", 19 | example: 1, 20 | type: Number, 21 | }) 22 | @Type(() => Number) 23 | @Expose() 24 | count: number; 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-amout-list.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 21:38:37 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:47:48 6 | * @FilePath: \cms\src\modules\goods\dtos\export-amout-list.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | 12 | export class ExportAmoutListDto { 13 | @ApiProperty({ description: "统计信息名", example: "sale", type: String }) 14 | @Expose() 15 | amount: string; 16 | 17 | @ApiProperty({ 18 | description: "统计信息标题", 19 | example: "商品总销量", 20 | type: String, 21 | }) 22 | @Expose() 23 | title: string; 24 | 25 | @ApiProperty({ 26 | description: "统计信息提示", 27 | example: "所有商品的总销量", 28 | type: String, 29 | }) 30 | @Expose() 31 | tips: string; 32 | 33 | @ApiProperty({ 34 | description: "统计信息子标题", 35 | example: "商品总销量", 36 | type: String, 37 | }) 38 | @Expose() 39 | subtitle: string; 40 | 41 | @ApiProperty({ 42 | description: "统计数据1", 43 | example: 1, 44 | type: Number, 45 | }) 46 | @Type(() => Number) 47 | @Expose() 48 | number1: number; 49 | 50 | @ApiProperty({ 51 | description: "统计数据2", 52 | example: 1, 53 | type: Number, 54 | }) 55 | @Type(() => Number) 56 | @Expose() 57 | number2: number; 58 | } 59 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-category-count.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { Expose, Type } from "class-transformer"; 3 | 4 | export class ExportCategoryCountDto { 5 | @ApiProperty({ description: "商品分类ID", example: 1, type: Number }) 6 | @Expose() 7 | id: number; 8 | 9 | @ApiProperty({ description: "商品分类名", example: "手机", type: String }) 10 | @Expose() 11 | name: string; 12 | 13 | @ApiProperty({ 14 | description: "商品分类对应商品个数", 15 | example: 1, 16 | type: Number, 17 | }) 18 | @Type(() => Number) 19 | @Expose() 20 | goodsCount: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-category-favor.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | import { Expose, Type } from "class-transformer"; 3 | 4 | export class ExportCategoryFavorDto { 5 | @ApiProperty({ description: "商品分类ID", example: 1, type: Number }) 6 | @Expose() 7 | id: number; 8 | 9 | @ApiProperty({ description: "商品分类名", example: "手机", type: String }) 10 | @Expose() 11 | name: string; 12 | 13 | @ApiProperty({ 14 | description: "商品分类对应商品收藏量", 15 | example: 1, 16 | type: Number, 17 | }) 18 | @Type(() => Number) 19 | @Expose() 20 | goodsFavor: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-category-sale.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 21:37:18 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:42:12 6 | * @FilePath: \cms\src\modules\goods\dtos\export-category-sale.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | 12 | export class ExportCategorySaleDto { 13 | @ApiProperty({ description: "商品分类ID", example: 1, type: Number }) 14 | @Expose() 15 | id: number; 16 | 17 | @ApiProperty({ description: "商品分类名", example: "手机", type: String }) 18 | @Expose() 19 | name: string; 20 | 21 | @ApiProperty({ 22 | description: "商品分类对应商品销量", 23 | example: 1, 24 | type: Number, 25 | }) 26 | @Type(() => Number) 27 | @Expose() 28 | goodsCount: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-goods-info-list.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 13:21:20 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:32:56 6 | * @FilePath: \cms\src\modules\goods-info\dto\export-goods-info-list.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | import { ExportGoodsInfoDto } from "./export-goods-info.dto"; 12 | 13 | export class ExportGoodsInfoList { 14 | @ApiProperty({ 15 | name: "商品列表", 16 | type: ExportGoodsInfoDto, 17 | description: "商品列表", 18 | }) 19 | @Type(() => ExportGoodsInfoDto) 20 | @Expose() 21 | list: ExportGoodsInfoDto[]; 22 | 23 | @ApiProperty({ 24 | name: "商品总数", 25 | type: Number, 26 | description: "商品总数", 27 | }) 28 | @Expose() 29 | totalCount: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-goods-info.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 13:15:54 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:29:31 6 | * @FilePath: \cms\src\modules\goods-info\dto\export-goods-info.dto.ts 7 | * @Description: 8 | */ 9 | import { formatTime } from "@/shared/utils"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { Expose, Transform, Type } from "class-transformer"; 12 | 13 | export class ExportGoodsInfoDto { 14 | @ApiProperty({ description: "商品ID", example: 32, type: Number }) 15 | @Expose() 16 | id: number; 17 | 18 | @ApiProperty({ 19 | description: "商品名称", 20 | example: "2018新款时尚百搭黑色宽松机车皮夹克+网纱半身裙套装两件套", 21 | type: String, 22 | }) 23 | @Expose() 24 | name: string; 25 | 26 | @ApiProperty({ description: "原价", example: "226", type: String }) 27 | @Expose() 28 | oldPrice: string; 29 | 30 | @ApiProperty({ description: "现价", example: "158", type: String }) 31 | @Expose() 32 | newPrice: string; 33 | 34 | @ApiProperty({ 35 | description: "商品描述", 36 | example: "2018新款时尚百搭黑色宽松机车皮夹克+网纱半身裙套装两件套", 37 | type: String, 38 | }) 39 | @Expose() 40 | desc: string; 41 | 42 | @ApiProperty({ description: "商品状态", example: 1, type: Number }) 43 | @Expose() 44 | @Type(() => Number) 45 | status: number; 46 | 47 | @ApiProperty({ 48 | description: "商品图片URL", 49 | example: 50 | "http://s3.mogucdn.com/mlcdn/55cf19/180917_7e2fdc2d8131698jkg69c9586lkel_640x960.jpg_560x999.jpg", 51 | type: String, 52 | }) 53 | @Expose() 54 | imgUrl: string; 55 | 56 | @ApiProperty({ description: "库存数量", example: 1589, type: Number }) 57 | @Expose() 58 | inventoryCount: number; 59 | 60 | @ApiProperty({ description: "销售数量", example: 16985, type: Number }) 61 | @Expose() 62 | saleCount: number; 63 | 64 | @ApiProperty({ description: "收藏数量", example: 28, type: Number }) 65 | @Expose() 66 | favorCount: number; 67 | 68 | @ApiProperty({ description: "商品所在地址", example: "西安", type: String }) 69 | @Expose() 70 | address: string; 71 | 72 | @ApiProperty({ description: "商品分类ID", example: 8, type: Number }) 73 | @Expose() 74 | categoryId: number; 75 | 76 | @ApiProperty({ 77 | description: "创建时间", 78 | example: "2021-04-30 13:40:30", 79 | type: Date, 80 | }) 81 | @Expose() 82 | @Transform(({ value }) => formatTime(value)) 83 | createAt: Date; 84 | 85 | @ApiProperty({ 86 | description: "更新时间", 87 | example: "2021-04-30 13:40:30", 88 | type: Date, 89 | }) 90 | @Expose() 91 | @Transform(({ value }) => formatTime(value)) 92 | updateAt: Date; 93 | } 94 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/export-sale-top-10.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 21:38:07 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:41:32 6 | * @FilePath: \cms\src\modules\goods\dtos\export-sale-top-10.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | 12 | export class ExportSaleTop10Dto { 13 | @ApiProperty({ description: "商品分类ID", example: 1, type: Number }) 14 | @Expose() 15 | id: number; 16 | 17 | @ApiProperty({ description: "商品分类名", example: "手机", type: String }) 18 | @Expose() 19 | name: string; 20 | 21 | @ApiProperty({ 22 | description: "商品分类对应商品销售数量", 23 | example: 1, 24 | type: Number, 25 | }) 26 | @Type(() => Number) 27 | @Expose() 28 | saleCount: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/query-goods-info.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 10:20:00 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:33:04 6 | * @FilePath: \cms\src\modules\goods-info\dto\query-goods-info.dto.ts 7 | * @Description: 8 | */ 9 | import { ValidateArrary, ValidateStringNumber } from "@/shared/decorators"; 10 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 11 | import { BaseQueryDto } from "@/shared/dtos"; 12 | import { ApiProperty } from "@nestjs/swagger"; 13 | import { Type } from "class-transformer"; 14 | import { IsOptional, IsString } from "class-validator"; 15 | 16 | export class QueryGoodsInfoDto extends BaseQueryDto { 17 | @ApiProperty({ name: "商品分类id", example: 1, type: Number }) 18 | @ValidateStringNumber({ message: "商品分类id必须是字符串或者数字" }) 19 | @Type(() => Number) 20 | @IsOptional() 21 | categoryId: number; 22 | 23 | @ApiProperty({ 24 | name: "商品名称", 25 | example: "格姬2018秋装4", 26 | type: String, 27 | }) 28 | @IsString({ message: "商品名称必须是字符串" }) 29 | @IsOptional() 30 | name: string; 31 | 32 | @ApiProperty({ name: "原价", example: [1, 100], type: [Number, Number] }) 33 | @ValidateArrary("原价") 34 | @IsOptional() 35 | oldPrice: number[]; 36 | 37 | @ApiProperty({ name: "现价", example: [1, 100], type: [Number, Number] }) 38 | @ValidateArrary("现价") 39 | @IsOptional() 40 | newPrice: number[]; 41 | 42 | @ApiProperty({ 43 | name: "商品描述", 44 | example: "格姬2018", 45 | type: String, 46 | }) 47 | @IsString({ message: "商品描述必须是字符串" }) 48 | @IsOptional() 49 | desc: string; 50 | 51 | @ApiProperty({ name: "商品状态", example: 1, type: Number }) 52 | @TransformNumber2Boolean() 53 | @IsOptional() 54 | status: boolean; 55 | 56 | @ApiProperty({ name: "库存数量", example: [1, 100], type: [Number, Number] }) 57 | @ValidateArrary("库存数量") 58 | @IsOptional() 59 | inventoryCount: number[]; 60 | 61 | @ApiProperty({ name: "销售数量", example: [1, 100], type: [Number, Number] }) 62 | @ValidateArrary("销售数量") 63 | @IsOptional() 64 | saleCount: number[]; 65 | 66 | @ApiProperty({ name: "收藏数量", example: [1, 100], type: [Number, Number] }) 67 | @ValidateArrary("收藏数量") 68 | @IsOptional() 69 | favorCount: number[]; 70 | 71 | @ApiProperty({ name: "商品地址", example: "南京", type: String }) 72 | @IsString({ message: "商品地址必须是字符串" }) 73 | @IsOptional() 74 | address: string; 75 | } 76 | -------------------------------------------------------------------------------- /src/modules/goods-info/dto/update-goods-info.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 20:59:52 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:33:07 6 | * @FilePath: \cms\src\modules\goods-info\dto\update-goods-info.dto.ts 7 | * @Description: 8 | */ 9 | import { PartialType } from "@nestjs/swagger"; 10 | import { CreateGoodsInfoDto } from "./create-goods-info.dto"; 11 | 12 | export class UpdateGoodsInfoDto extends PartialType(CreateGoodsInfoDto) {} 13 | -------------------------------------------------------------------------------- /src/modules/goods-info/goods-info.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 20:59:52 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:46:46 6 | * @FilePath: \cms\src\modules\goods-info\goods-info.controller.ts 7 | * @Description: 8 | */ 9 | import { RequirePermission } from "@/shared/decorators"; 10 | import { PermissionEnum } from "@/shared/enums"; 11 | import { 12 | Body, 13 | Controller, 14 | Delete, 15 | Get, 16 | HttpCode, 17 | HttpStatus, 18 | Param, 19 | Patch, 20 | Post, 21 | } from "@nestjs/common"; 22 | import { ApiTags } from "@nestjs/swagger"; 23 | import { CreateGoodsInfoDto } from "./dto/create-goods-info.dto"; 24 | import { QueryGoodsInfoDto } from "./dto/query-goods-info.dto"; 25 | import { UpdateGoodsInfoDto } from "./dto/update-goods-info.dto"; 26 | import { GoodsInfoService } from "./goods-info.service"; 27 | 28 | @Controller("goods") 29 | @ApiTags("商品信息模块") 30 | export class GoodsInfoController { 31 | constructor(private readonly goodsInfoService: GoodsInfoService) {} 32 | 33 | @Post() 34 | @HttpCode(HttpStatus.OK) 35 | @RequirePermission(PermissionEnum.SYSTEM_GOODS_CREATE) 36 | create(@Body() createGoodsInfoDto: CreateGoodsInfoDto) { 37 | return this.goodsInfoService.create(createGoodsInfoDto); 38 | } 39 | 40 | @Post("list") 41 | @HttpCode(HttpStatus.OK) 42 | @RequirePermission(PermissionEnum.SYSTEM_GOODS_QUERY) 43 | findAll(@Body() queryGoodsInfoDto: QueryGoodsInfoDto) { 44 | return this.goodsInfoService.findAll(queryGoodsInfoDto); 45 | } 46 | 47 | @Get(":id") 48 | @RequirePermission(PermissionEnum.SYSTEM_GOODS_QUERY) 49 | findOne(@Param("id") id: string) { 50 | return this.goodsInfoService.findOne(+id); 51 | } 52 | 53 | @Patch(":id") 54 | @RequirePermission(PermissionEnum.SYSTEM_GOODS_UPDATE) 55 | update( 56 | @Param("id") id: string, 57 | @Body() updateGoodsInfoDto: UpdateGoodsInfoDto, 58 | ) { 59 | return this.goodsInfoService.update(+id, updateGoodsInfoDto); 60 | } 61 | 62 | @Delete(":id") 63 | @RequirePermission(PermissionEnum.SYSTEM_GOODS_DELETE) 64 | remove(@Param("id") id: string) { 65 | return this.goodsInfoService.remove(+id); 66 | } 67 | 68 | @Get("category/count") 69 | getCategoryCount() { 70 | return this.goodsInfoService.getCategoryCount(); 71 | } 72 | 73 | @Get("category/sale") 74 | getCategorySale() { 75 | return this.goodsInfoService.getCategorySale(); 76 | } 77 | @Get("category/favor") 78 | getCategoryFavor() { 79 | return this.goodsInfoService.getCategoryFavor(); 80 | } 81 | 82 | @Get("sale/top10") 83 | getSaleTop10() { 84 | return this.goodsInfoService.getSaleTop10(); 85 | } 86 | @Get("address/sale") 87 | getAddressSale() { 88 | return this.goodsInfoService.getAddressSale(); 89 | } 90 | 91 | @Get("amount/list") 92 | getAmountList() { 93 | return this.goodsInfoService.getAmountCounts(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/goods-info/goods-info.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 20:59:52 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:33:11 6 | * @FilePath: \cms\src\modules\goods-info\goods-info.module.ts 7 | * @Description: 8 | */ 9 | import { Module, forwardRef } from "@nestjs/common"; 10 | import { GoodsCategoryModule } from "../goods-category/goods-category.module"; 11 | import { GoodsInfoController } from "./goods-info.controller"; 12 | import { GoodsInfoService } from "./goods-info.service"; 13 | 14 | @Module({ 15 | imports: [forwardRef(() => GoodsCategoryModule)], 16 | controllers: [GoodsInfoController], 17 | providers: [GoodsInfoService], 18 | exports: [GoodsInfoService], 19 | }) 20 | export class GoodsInfoModule {} 21 | -------------------------------------------------------------------------------- /src/modules/menus/dto/create-menu.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:33:36 6 | * @FilePath: \cms\src\modules\menus\dto\create-menu.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Type } from "class-transformer"; 11 | import { 12 | IsInt, 13 | IsNotEmpty, 14 | IsOptional, 15 | IsString, 16 | MaxLength, 17 | } from "class-validator"; 18 | 19 | export class CreateMenuDto { 20 | @ApiProperty({ 21 | description: "菜单名", 22 | maxLength: 20, 23 | example: "菜单名", 24 | type: String, 25 | }) 26 | @IsString({ message: "菜单名必须是字符串" }) 27 | @IsNotEmpty({ message: "菜单名不能为空" }) 28 | @MaxLength(20, { message: "菜单名不能超过20个字符" }) 29 | name: string; 30 | 31 | @ApiProperty({ description: "菜单层级", example: 0, type: Number }) 32 | @IsInt({ message: "菜单层级必须是数字" }) 33 | @IsNotEmpty({ message: "菜单层级不能为空" }) 34 | @Type(() => Number) 35 | type: number; 36 | 37 | @ApiProperty({ description: "菜单url", example: "/menu", type: String }) 38 | @IsString({ message: "菜单url必须是字符串" }) 39 | @IsOptional() 40 | url: string; 41 | 42 | @ApiProperty({ description: "菜单排序", example: 0, type: Number }) 43 | @IsInt({ message: "菜单排序必须是数字" }) 44 | @IsOptional() 45 | @Type(() => Number) 46 | sort: number; 47 | 48 | @ApiProperty({ 49 | description: "菜单图标", 50 | example: "el-icon-s-home", 51 | type: String, 52 | }) 53 | @IsString({ message: "菜单图标必须是字符串" }) 54 | @IsOptional() 55 | icon: string; 56 | 57 | @ApiProperty({ description: "菜单父级id", example: 0, type: Number }) 58 | @IsInt({ message: "菜单父级id必须是数字" }) 59 | @IsOptional() 60 | @Type(() => Number) 61 | parentId: number; 62 | 63 | @ApiProperty({ description: "菜单权限", example: "admin", type: String }) 64 | @IsString({ message: "菜单权限必须是字符串" }) 65 | @IsOptional() 66 | permission: string; 67 | } 68 | -------------------------------------------------------------------------------- /src/modules/menus/dto/export-menu.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 11:03:45 6 | * @FilePath: \cms\src\modules\menus\dto\export-menu.dto.ts 7 | * @Description: 8 | */ 9 | import { ExposeNotNull } from "@/shared/decorators"; 10 | import { formatTime } from "@/shared/utils"; 11 | import { ApiProperty } from "@nestjs/swagger"; 12 | import { Expose, Transform, Type } from "class-transformer"; 13 | 14 | @Expose() 15 | export class ExportMenuDto { 16 | @ApiProperty({ description: "菜单 ID", example: "1", type: String }) 17 | @Expose() 18 | id: string; 19 | 20 | @ApiProperty({ description: "菜单名称", example: "用户管理", type: String }) 21 | @Expose() 22 | name: string; 23 | 24 | @ApiProperty({ description: "菜单层级", example: 1, type: Number }) 25 | @Expose() 26 | type: number; 27 | 28 | @ApiProperty({ description: "菜单地址", example: "/user", type: String }) 29 | @Expose() 30 | url: string; 31 | 32 | @ApiProperty({ description: "菜单图标", example: "user", type: String }) 33 | @ExposeNotNull() 34 | icon: string; 35 | 36 | @ApiProperty({ description: "菜单排序", example: 1, type: Number }) 37 | @Expose() 38 | sort: number; 39 | 40 | @ApiProperty({ 41 | description: "是否启用", 42 | example: true, 43 | type: Number, 44 | }) 45 | @Type(() => Number) 46 | @Expose() 47 | enable: number; 48 | 49 | @ApiProperty({ description: "菜单父级id", example: "1", type: String }) 50 | @Expose() 51 | parentId: string; 52 | 53 | @ApiProperty({ description: "菜单权限", example: "admin", type: String }) 54 | @ExposeNotNull() 55 | permission: string; 56 | 57 | @ApiProperty({ 58 | description: "创建时间", 59 | example: "2022-01-01 00:00:00", 60 | type: Date, 61 | }) 62 | @Expose() 63 | @Transform(({ value }) => formatTime(value)) 64 | createAt: Date; 65 | 66 | @ApiProperty({ 67 | description: "更新时间", 68 | example: "2022-01-01 00:00:00", 69 | type: Date, 70 | }) 71 | @Expose() 72 | @Transform(({ value }) => formatTime(value)) 73 | updateAt: Date; 74 | 75 | @ApiProperty({ description: "子菜单", example: [], type: [ExportMenuDto] }) 76 | @Type(() => ExportMenuDto) 77 | @ExposeNotNull() 78 | children: ExportMenuDto[]; 79 | } 80 | -------------------------------------------------------------------------------- /src/modules/menus/dto/update-menu.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:33:49 6 | * @FilePath: \cms\src\modules\menus\dto\update-menu.dto.ts 7 | * @Description: 8 | */ 9 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 10 | import { ApiProperty, PartialType } from "@nestjs/swagger"; 11 | import { IsOptional } from "class-validator"; 12 | import { CreateMenuDto } from "./create-menu.dto"; 13 | 14 | export class UpdateMenuDto extends PartialType(CreateMenuDto) { 15 | @ApiProperty({ 16 | name: "是否启用 ", 17 | example: 0, 18 | type: Number, 19 | description: "0:禁用 1:启用", 20 | }) 21 | @TransformNumber2Boolean() 22 | @IsOptional() 23 | enable: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/menus/menus.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:32:57 6 | * @FilePath: \cms\src\modules\menus\menus.controller.ts 7 | * @Description: 8 | */ 9 | import { RequirePermission } from "@/shared/decorators"; 10 | import { PermissionEnum } from "@/shared/enums"; 11 | import { 12 | Body, 13 | Controller, 14 | Delete, 15 | Get, 16 | HttpCode, 17 | HttpStatus, 18 | Param, 19 | Patch, 20 | Post, 21 | } from "@nestjs/common"; 22 | import { ApiTags } from "@nestjs/swagger"; 23 | import { CreateMenuDto } from "./dto/create-menu.dto"; 24 | import { UpdateMenuDto } from "./dto/update-menu.dto"; 25 | import { MenusService } from "./menus.service"; 26 | 27 | @Controller("menu") 28 | @ApiTags("菜单管理模块") 29 | export class MenusController { 30 | constructor(private readonly menusService: MenusService) {} 31 | 32 | /** 33 | * 创建菜单 34 | * @param createMenuDto 创建信息 35 | * @returns 36 | */ 37 | @Post() 38 | @HttpCode(HttpStatus.OK) 39 | @RequirePermission(PermissionEnum.SYSTEM_MENU_CREATE) 40 | create(@Body() createMenuDto: CreateMenuDto) { 41 | return this.menusService.create(createMenuDto); 42 | } 43 | 44 | /** 45 | * 查询菜单列表 46 | * @returns 47 | */ 48 | @Post("list") 49 | @HttpCode(HttpStatus.OK) 50 | async findAll() { 51 | return { 52 | list: await this.menusService.findAll(), 53 | }; 54 | } 55 | 56 | /** 57 | * 查询菜单 58 | * @param id 菜单id 59 | * @returns 60 | */ 61 | @Get(":id") 62 | @RequirePermission(PermissionEnum.SYSTEM_MENU_QUERY) 63 | findOne(@Param("id") id: string) { 64 | return this.menusService.findOne(+id); 65 | } 66 | 67 | /** 68 | * 更新菜单 69 | * @param id 菜单id 70 | * @param updateMenuDto 更新信息 71 | * @returns 72 | */ 73 | @Patch(":id") 74 | @RequirePermission(PermissionEnum.SYSTEM_MENU_UPDATE) 75 | update(@Param("id") id: string, @Body() updateMenuDto: UpdateMenuDto) { 76 | return this.menusService.update(+id, updateMenuDto); 77 | } 78 | 79 | /** 80 | * 删除菜单 81 | * @param id 菜单id 82 | * @returns 83 | */ 84 | @Delete(":id") 85 | @RequirePermission(PermissionEnum.SYSTEM_MENU_DELETE) 86 | remove(@Param("id") id: string) { 87 | return this.menusService.remove(+id); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/modules/menus/menus.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:33:53 6 | * @FilePath: \cms\src\modules\menus\menus.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | import { MenusController } from "./menus.controller"; 11 | import { MenusService } from "./menus.service"; 12 | 13 | @Module({ 14 | imports: [], 15 | controllers: [MenusController], 16 | providers: [MenusService], 17 | exports: [MenusService], 18 | }) 19 | export class MenusModule {} 20 | -------------------------------------------------------------------------------- /src/modules/menus/menus.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:19:51 6 | * @FilePath: \cms\src\modules\menus\menus.service.ts 7 | * @Description: 8 | */ 9 | import { CacheEvict, Cacheable } from "@/shared/decorators"; 10 | import { RedisKeyEnum } from "@/shared/enums"; 11 | import { AppLoggerSevice } from "@/shared/logger"; 12 | import { PrismaService } from "@/shared/prisma"; 13 | import { generateTree, getRandomId, handleError } from "@/shared/utils"; 14 | import { 15 | BadRequestException, 16 | ForbiddenException, 17 | Injectable, 18 | } from "@nestjs/common"; 19 | import { plainToInstance } from "class-transformer"; 20 | import { CreateMenuDto } from "./dto/create-menu.dto"; 21 | import { ExportMenuDto } from "./dto/export-menu.dto"; 22 | import { UpdateMenuDto } from "./dto/update-menu.dto"; 23 | 24 | @Injectable() 25 | export class MenusService { 26 | constructor( 27 | private readonly logger: AppLoggerSevice, 28 | private readonly prismaService: PrismaService, 29 | ) { 30 | this.logger.setContext(MenusService.name); 31 | } 32 | 33 | /** 34 | * 创建菜单 35 | * @param createMenuDto 36 | * @returns 37 | */ 38 | @CacheEvict(RedisKeyEnum.MenuKey) 39 | async create(createMenuDto: CreateMenuDto) { 40 | this.logger.log(`${this.create.name} was called`); 41 | const { parentId, ...rest } = createMenuDto; 42 | try { 43 | // 如果有父级id,就创建子菜单 44 | if (parentId) { 45 | // 保存子菜单 46 | await this.prismaService.menu.create({ 47 | data: { 48 | ...rest, 49 | parentId, 50 | }, 51 | }); 52 | return "创建菜单成功"; 53 | } 54 | // 如果没有父级id,就保存一级菜单 55 | await this.prismaService.menu.create({ 56 | data: rest, 57 | }); 58 | return "创建菜单成功"; 59 | } catch (error) { 60 | handleError(this.logger, error, { 61 | common: "创建菜单失败", 62 | unique: "菜单名已存在", 63 | }); 64 | } 65 | } 66 | 67 | /** 68 | * 查询所有菜单 69 | * @returns 70 | */ 71 | @Cacheable(RedisKeyEnum.MenuKey) 72 | async findAll() { 73 | this.logger.log(`${this.findAll.name} was called`); 74 | try { 75 | const menuListTrees = await this.prismaService.menu.findMany({ 76 | where: { 77 | isDelete: false, 78 | }, 79 | }); 80 | return plainToInstance(ExportMenuDto, generateTree(menuListTrees), { 81 | excludeExtraneousValues: true, 82 | }); 83 | } catch (error) { 84 | handleError(this.logger, error, { 85 | common: "查找菜单失败", 86 | }); 87 | } 88 | } 89 | 90 | /** 91 | * 查询所有菜单id 92 | * @returns 93 | */ 94 | @Cacheable(RedisKeyEnum.MenuKey) 95 | async findAllIds() { 96 | this.logger.log(`${this.findAllIds.name} was called`); 97 | const menuList = await this.prismaService.menu.findMany({ 98 | select: { 99 | id: true, 100 | }, 101 | where: { 102 | isDelete: false, 103 | }, 104 | }); 105 | return menuList.map((item) => item.id); 106 | } 107 | 108 | /** 109 | * 根据id查找菜单 110 | * @param id 111 | * @returns 112 | */ 113 | async findOne(id: number) { 114 | this.logger.error(`${this.findOne.name} was called`); 115 | try { 116 | if (!id) throw new BadRequestException("菜单不存在"); 117 | const menu = await this.prismaService.menu.findUnique({ 118 | where: { 119 | id, 120 | isDelete: false, 121 | }, 122 | }); 123 | if (!menu) throw new BadRequestException("菜单不存在"); 124 | return plainToInstance(ExportMenuDto, menu, { 125 | excludeExtraneousValues: true, 126 | }); 127 | } catch (error) { 128 | handleError(this.logger, error, { 129 | common: "查找菜单失败", 130 | }); 131 | } 132 | } 133 | 134 | /** 135 | * 更新菜单 136 | * @param id 137 | * @param updateMenuDto 138 | * @returns 139 | */ 140 | @CacheEvict(RedisKeyEnum.MenuKey, RedisKeyEnum.RoleKey) 141 | async update(id: number, updateMenuDto: UpdateMenuDto) { 142 | this.logger.log(`${this.update.name} was called`); 143 | this.judgeCanDo(id); 144 | try { 145 | await this.findOne(id); 146 | await this.prismaService.menu.update({ 147 | where: { 148 | id, 149 | isDelete: false, 150 | }, 151 | data: updateMenuDto, 152 | }); 153 | return "更新菜单成功"; 154 | } catch (error) { 155 | handleError(this.logger, error, { 156 | common: "更新菜单失败", 157 | unique: "菜单名已存在", 158 | }); 159 | } 160 | } 161 | 162 | /** 163 | * 删除菜单 164 | * @param id 165 | * @returns 166 | */ 167 | @CacheEvict(RedisKeyEnum.MenuKey, RedisKeyEnum.RoleKey) 168 | async remove(id: number) { 169 | this.logger.log(`${this.remove.name} was called`); 170 | this.judgeCanDo(id); 171 | try { 172 | const menu = await this.findOne(id); 173 | await this.prismaService.menu.update({ 174 | where: { 175 | id, 176 | isDelete: false, 177 | }, 178 | data: { 179 | isDelete: true, 180 | name: "已删除" + "_" + menu.name + "_" + getRandomId(), 181 | roleMenu: { 182 | deleteMany: {}, 183 | }, 184 | }, 185 | }); 186 | return "删除菜单成功"; 187 | } catch (error) { 188 | handleError(this.logger, error, { 189 | common: "删除菜单失败", 190 | }); 191 | } 192 | } 193 | 194 | /** 195 | * 根据id数组查找菜单 196 | * @param ids 197 | * @returns 198 | */ 199 | @Cacheable(RedisKeyEnum.MenuKey) 200 | async findListByIds(ids: number[]) { 201 | try { 202 | return await this.prismaService.menu.findMany({ 203 | where: { 204 | id: { 205 | in: ids, 206 | }, 207 | isDelete: false, 208 | }, 209 | }); 210 | } catch (error) { 211 | handleError(this.logger, error, { 212 | common: "查找菜单失败", 213 | }); 214 | } 215 | } 216 | 217 | /** 218 | * 判断是否可以操作 219 | * @param id 220 | * @returns 221 | */ 222 | judgeCanDo(id: number) { 223 | if (id <= 44) { 224 | throw new ForbiddenException("系统菜单不能操作"); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/modules/qrcode/qrcode.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from "@nestjs/common"; 2 | import { QrcodeService } from "./qrcode.service"; 3 | import { Public } from "@/shared/decorators"; 4 | 5 | @Public() 6 | @Controller("qrcode") 7 | export class QrcodeController { 8 | constructor(private readonly qrcodeService: QrcodeService) {} 9 | 10 | @Get("generate") 11 | async generateQRCode() { 12 | return this.qrcodeService.generateQRCode(); 13 | } 14 | 15 | @Get("check/:id") 16 | async check(@Param("id") id: string) { 17 | return this.qrcodeService.checkStatus(id); 18 | } 19 | 20 | @Get("scan/:id") 21 | async scanQRCode(@Param("id") id: string) { 22 | return this.qrcodeService.scanQRCode(id); 23 | } 24 | 25 | @Get("confirm/:id") 26 | async confirmQRCode(@Param("id") id: string) { 27 | return this.qrcodeService.confirmQRCode(id); 28 | } 29 | 30 | @Get("cancel/:id") 31 | async cancelQRCode(@Param("id") id: string) { 32 | return this.qrcodeService.cancelQRCode(id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/modules/qrcode/qrcode.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { QrcodeController } from "./qrcode.controller"; 3 | import { QrcodeService } from "./qrcode.service"; 4 | 5 | @Module({ 6 | controllers: [QrcodeController], 7 | providers: [QrcodeService], 8 | }) 9 | export class QrcodeModule {} 10 | -------------------------------------------------------------------------------- /src/modules/qrcode/qrcode.service.ts: -------------------------------------------------------------------------------- 1 | import { RedisKeyEnum, ScanStatusEnum } from "@/shared/enums"; 2 | import { AppLoggerSevice } from "@/shared/logger"; 3 | import { RedisService } from "@/shared/redis"; 4 | import { handleError } from "@/shared/utils"; 5 | import { BadRequestException, Injectable } from "@nestjs/common"; 6 | import * as QRCode from "qrcode"; 7 | import { v4 as UUID } from "uuid"; 8 | 9 | @Injectable() 10 | export class QrcodeService { 11 | constructor( 12 | private readonly logger: AppLoggerSevice, 13 | private readonly redisService: RedisService, 14 | ) { 15 | this.logger.log(QrcodeService.name); 16 | } 17 | 18 | async generateQRCode() { 19 | this.logger.log(`${this.generateQRCode.name} was called`); 20 | try { 21 | const uuid = UUID(); 22 | const dataUrl = await QRCode.toDataURL( 23 | "https://scan.hqk10.xyz?id=" + uuid, 24 | ); 25 | // 生成二维码并存入redis 有效期30分钟 26 | this.redisService.setex( 27 | RedisKeyEnum.QrcodeKey + uuid, 28 | 60 * 30, 29 | ScanStatusEnum.NotScan, 30 | ); 31 | return { 32 | id: uuid, 33 | url: dataUrl, 34 | }; 35 | } catch (error) { 36 | handleError(this.logger, error, { 37 | common: "生成二维码失败", 38 | }); 39 | } 40 | } 41 | 42 | async checkStatus(id: string) { 43 | try { 44 | const status = await this.redisService._get(RedisKeyEnum.QrcodeKey + id); 45 | if (status == null) { 46 | return { 47 | status: ScanStatusEnum.Expired, 48 | }; 49 | } 50 | return { status }; 51 | } catch (error) { 52 | handleError(this.logger, error, { 53 | common: "获取二维码状态失败", 54 | }); 55 | } 56 | } 57 | 58 | async scanQRCode(id: string) { 59 | this.logger.log(`${this.scanQRCode.name} was called`); 60 | try { 61 | const status = await this.redisService._get(RedisKeyEnum.QrcodeKey + id); 62 | if (status == null) { 63 | throw new BadRequestException("二维码已过期"); 64 | } 65 | this.redisService._set( 66 | RedisKeyEnum.QrcodeKey + id, 67 | ScanStatusEnum.Scanned, 68 | ); 69 | return "扫码成功"; 70 | } catch (error) { 71 | handleError(this.logger, error, { 72 | common: "扫码失败", 73 | }); 74 | } 75 | } 76 | 77 | async confirmQRCode(id: string) { 78 | this.logger.log(`${this.scanQRCode.name} was called`); 79 | try { 80 | const status = await this.redisService._get(RedisKeyEnum.QrcodeKey + id); 81 | if (status == null) { 82 | throw new BadRequestException("二维码已过期"); 83 | } 84 | this.redisService._set( 85 | RedisKeyEnum.QrcodeKey + id, 86 | ScanStatusEnum.Confirmed, 87 | ); 88 | return "确认登录成功"; 89 | } catch (error) { 90 | handleError(this.logger, error, { 91 | common: "确认登录失败", 92 | }); 93 | } 94 | } 95 | 96 | async cancelQRCode(id: string) { 97 | this.logger.log(`${this.cancelQRCode.name} was called`); 98 | try { 99 | const status = await this.redisService._get(RedisKeyEnum.QrcodeKey + id); 100 | if (status == null) { 101 | throw new BadRequestException("二维码已过期"); 102 | } 103 | this.redisService._set( 104 | RedisKeyEnum.QrcodeKey + id, 105 | ScanStatusEnum.Canceled, 106 | ); 107 | return "取消成功"; 108 | } catch (error) { 109 | handleError(this.logger, error, { 110 | common: "取消登录失败", 111 | }); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/modules/roles/dto/assign-role.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 17:50:42 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:28:29 6 | * @FilePath: \cms\src\modules\roles\dto\assign-role.dto.ts 7 | * @Description: 8 | */ 9 | import { ValidateStringNumber } from "@/shared/decorators"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { Type } from "class-transformer"; 12 | import { IsArray, IsNotEmpty } from "class-validator"; 13 | 14 | export class AssignRoleDto { 15 | @ApiProperty({ 16 | name: "角色id", 17 | type: String, 18 | example: "1", 19 | }) 20 | @ValidateStringNumber() 21 | @IsNotEmpty({ message: "角色id不能为空" }) 22 | @Type(() => Number) 23 | roleId: number; 24 | 25 | @ApiProperty({ 26 | name: "菜单列表ids", 27 | type: [Number], 28 | example: [1, 2, 3], 29 | }) 30 | @IsArray({ message: "菜单列表ids必须是数组" }) 31 | @IsNotEmpty({ message: "菜单列表ids不能为空" }) 32 | menuList: number[]; 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/roles/dto/create-role.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:08 6 | * @FilePath: \cms\src\modules\roles\dto\create-role.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsArray, IsNotEmpty, IsString, MaxLength } from "class-validator"; 11 | 12 | export class CreateRoleDto { 13 | @ApiProperty({ description: "角色名称", type: String, example: "管理员" }) 14 | @IsString({ message: "角色名称必须是字符串" }) 15 | @IsNotEmpty({ message: "角色名称不能为空" }) 16 | @MaxLength(20, { message: "角色名称最大长度为20" }) 17 | name: string; 18 | 19 | @ApiProperty({ description: "角色介绍", type: String, example: "管理员" }) 20 | @IsString({ message: "角色介绍必须是字符串" }) 21 | @IsNotEmpty({ message: "角色介绍不能为空" }) 22 | intro: string; 23 | 24 | @ApiProperty({ description: "菜单列表", type: [Number], example: [1, 2, 3] }) 25 | @IsNotEmpty({ message: "菜单列表不能为空" }) 26 | @IsArray({ message: "菜单列表必须是数组" }) 27 | menuList: number[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/roles/dto/export-role-list.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:12 6 | * @FilePath: \cms\src\modules\roles\dto\export-role-list.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | import { ExportRoleDto } from "./export-role.dto"; 12 | 13 | export class ExportRoleListDto { 14 | @ApiProperty({ description: "角色列表", example: [], type: [ExportRoleDto] }) 15 | @Expose() 16 | @Type(() => ExportRoleDto) 17 | list: ExportRoleDto[]; 18 | 19 | @ApiProperty({ description: "总数", example: 10, type: Number }) 20 | @Expose() 21 | totalCount: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/roles/dto/export-role.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 11:04:04 6 | * @FilePath: \cms\src\modules\roles\dto\export-role.dto.ts 7 | * @Description: 8 | */ 9 | import { ExportMenuDto } from "@/modules/menus/dto/export-menu.dto"; 10 | import { formatTime } from "@/shared/utils"; 11 | import { ApiProperty } from "@nestjs/swagger"; 12 | import { Expose, Transform, Type } from "class-transformer"; 13 | 14 | export class ExportRoleDto { 15 | @ApiProperty({ description: "角色 ID", example: 1, type: Number }) 16 | @Expose() 17 | id: number; 18 | 19 | @ApiProperty({ description: "角色名称", example: "管理员", type: String }) 20 | @Expose() 21 | name: string; 22 | 23 | @ApiProperty({ 24 | description: "是否启用", 25 | example: true, 26 | type: Number, 27 | }) 28 | @Type(() => Number) 29 | @Expose() 30 | enable: number; 31 | 32 | @ApiProperty({ 33 | description: "角色简介", 34 | example: "拥有所有权限", 35 | type: String, 36 | }) 37 | @Expose() 38 | intro: string; 39 | 40 | @ApiProperty({ 41 | description: "创建时间", 42 | example: "2022-01-01 00:00:00", 43 | type: Date, 44 | }) 45 | @Expose() 46 | @Transform(({ value }) => formatTime(value)) 47 | createAt: Date; 48 | 49 | @ApiProperty({ 50 | description: "更新时间", 51 | example: "2022-01-01 00:00:00", 52 | type: Date, 53 | }) 54 | @Expose() 55 | @Transform(({ value }) => formatTime(value)) 56 | updateAt: Date; 57 | 58 | @Expose() 59 | @Type(() => ExportMenuDto) 60 | menuList: ExportMenuDto[]; 61 | } 62 | -------------------------------------------------------------------------------- /src/modules/roles/dto/query-role.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:15 6 | * @FilePath: \cms\src\modules\roles\dto\query-role.dto.ts 7 | * @Description: 8 | */ 9 | import { BaseQueryDto } from "@/shared/dtos"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { IsArray, IsOptional, IsString } from "class-validator"; 12 | 13 | export class QueryRoleDto extends BaseQueryDto { 14 | @ApiProperty({ name: "角色名称", example: "管理员", type: String }) 15 | @IsString() 16 | @IsOptional() 17 | name: string; 18 | 19 | @ApiProperty({ name: "角色描述", example: "管理员", type: String }) 20 | @IsString() 21 | @IsOptional() 22 | intro: string; 23 | 24 | @ApiProperty({ name: "菜单列表", example: [1, 2, 3], type: [Number] }) 25 | @IsArray({ message: "菜单列表必须是数组" }) 26 | @IsOptional() 27 | menuList: number[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/roles/dto/update-role.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:19 6 | * @FilePath: \cms\src\modules\roles\dto\update-role.dto.ts 7 | * @Description: 8 | */ 9 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 10 | import { ApiProperty, PartialType } from "@nestjs/swagger"; 11 | import { IsOptional } from "class-validator"; 12 | import { CreateRoleDto } from "./create-role.dto"; 13 | 14 | export class UpdateRoleDto extends PartialType(CreateRoleDto) { 15 | @ApiProperty({ 16 | name: "是否启用 ", 17 | example: 0, 18 | type: Number, 19 | description: "0:禁用 1:启用", 20 | }) 21 | @TransformNumber2Boolean() 22 | @IsOptional() 23 | enable: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/roles/roles.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:53:30 6 | * @FilePath: \cms\src\modules\roles\roles.controller.ts 7 | * @Description: 8 | */ 9 | import { 10 | Body, 11 | Controller, 12 | Delete, 13 | Get, 14 | HttpCode, 15 | HttpStatus, 16 | Param, 17 | Patch, 18 | Post, 19 | } from "@nestjs/common"; 20 | import { ApiTags } from "@nestjs/swagger"; 21 | import { AssignRoleDto } from "./dto/assign-role.dto"; 22 | import { CreateRoleDto } from "./dto/create-role.dto"; 23 | import { QueryRoleDto } from "./dto/query-role.dto"; 24 | import { UpdateRoleDto } from "./dto/update-role.dto"; 25 | import { RolesService } from "./roles.service"; 26 | import { RequirePermission } from "@/shared/decorators"; 27 | import { PermissionEnum } from "@/shared/enums"; 28 | 29 | @Controller("role") 30 | @ApiTags("角色管理模块") 31 | export class RolesController { 32 | constructor(private readonly rolesService: RolesService) {} 33 | 34 | /** 35 | * 创建角色 36 | * @param createRoleDto 创建信息 37 | * @returns 38 | */ 39 | @Post() 40 | @HttpCode(HttpStatus.OK) 41 | @RequirePermission(PermissionEnum.SYSTEM_ROLE_CREATE) 42 | create(@Body() createRoleDto: CreateRoleDto) { 43 | return this.rolesService.create(createRoleDto); 44 | } 45 | 46 | /** 47 | * 查询角色列表 48 | * @param queryRoleDto 查询条件 49 | * @returns 50 | */ 51 | @Post("list") 52 | @HttpCode(HttpStatus.OK) 53 | findAll(@Body() queryRoleDto: QueryRoleDto) { 54 | return this.rolesService.findAll(queryRoleDto); 55 | } 56 | 57 | /** 58 | * 查询角色 59 | * @param id 角色id 60 | * @returns 61 | */ 62 | @Get(":id") 63 | @RequirePermission(PermissionEnum.SYSTEM_ROLE_QUERY) 64 | findOne(@Param("id") id: string) { 65 | return this.rolesService.findOne(+id); 66 | } 67 | 68 | /** 69 | * 更新角色 70 | * @param id 角色id 71 | * @param updateRoleDto 更新信息 72 | * @returns 73 | */ 74 | @Patch(":id") 75 | @RequirePermission(PermissionEnum.SYSTEM_ROLE_UPDATE) 76 | update(@Param("id") id: string, @Body() updateRoleDto: UpdateRoleDto) { 77 | return this.rolesService.update(+id, updateRoleDto); 78 | } 79 | 80 | /** 81 | * 删除角色 82 | * @param id 角色id 83 | * @returns 84 | */ 85 | @Delete(":id") 86 | @RequirePermission(PermissionEnum.SYSTEM_ROLE_DELETE) 87 | remove(@Param("id") id: string) { 88 | return this.rolesService.remove(+id); 89 | } 90 | 91 | @Get(":id/menu") 92 | findRoleMenu(@Param("id") id: string) { 93 | return this.rolesService.findRoleMenuById(+id); 94 | } 95 | 96 | @Get(":id/menuIds") 97 | @RequirePermission(PermissionEnum.SYSTEM_ROLE_QUERY) 98 | findRoleMenuIds(@Param("id") id: string) { 99 | return this.rolesService.findRoleMenuIdsById(+id); 100 | } 101 | 102 | @Post("assign") 103 | @HttpCode(HttpStatus.OK) 104 | @RequirePermission(PermissionEnum.SYSTEM_ROLE_UPDATE) 105 | assignRole(@Body() assignRoleDto: AssignRoleDto) { 106 | return this.rolesService.assignRole(assignRoleDto); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/modules/roles/roles.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 22:22:28 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:22 6 | * @FilePath: \cms\src\modules\roles\roles.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | import { MenusModule } from "../menus/menus.module"; 11 | import { RolesController } from "./roles.controller"; 12 | import { RolesService } from "./roles.service"; 13 | 14 | @Module({ 15 | imports: [MenusModule], 16 | controllers: [RolesController], 17 | providers: [RolesService], 18 | exports: [RolesService], 19 | }) 20 | export class RolesModule {} 21 | -------------------------------------------------------------------------------- /src/modules/story/dto/create-story.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:32 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:32 6 | * @FilePath: \cms\src\modules\story\dto\create-story.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsNotEmpty, IsString } from "class-validator"; 11 | 12 | export class CreateStoryDto { 13 | @ApiProperty({ 14 | description: "故事标题", 15 | example: "故事标题", 16 | type: String, 17 | }) 18 | @IsString({ message: "标题必须是字符串" }) 19 | @IsNotEmpty({ message: "标题不能为空" }) 20 | title: string; 21 | 22 | @ApiProperty({ 23 | description: "故事内容", 24 | example: "故事内容", 25 | type: String, 26 | }) 27 | @IsString({ message: "内容必须是字符串" }) 28 | @IsNotEmpty({ message: "内容不能为空" }) 29 | content: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/story/dto/export-story-list.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 11:45:48 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:36 6 | * @FilePath: \cms\src\modules\story\dto\export-story-list.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Expose, Type } from "class-transformer"; 11 | import { ExportExportDto } from "./export-story.dto"; 12 | 13 | export class ExportStoryListDto { 14 | @ApiProperty({ 15 | description: "故事列表", 16 | example: "故事列表", 17 | type: [ExportExportDto], 18 | }) 19 | @Expose() 20 | @Type(() => ExportExportDto) 21 | list: ExportExportDto[]; 22 | 23 | @ApiProperty({ 24 | description: "故事总数", 25 | example: 1, 26 | type: Number, 27 | }) 28 | @Expose() 29 | totalCount: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/story/dto/export-story.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 11:40:24 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 10:55:38 6 | * @FilePath: \cms\src\modules\story\dto\export-story.dto.ts 7 | * @Description: 8 | */ 9 | import { formatTime } from "@/shared/utils"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { Expose, Transform, Type } from "class-transformer"; 12 | 13 | export class ExportExportDto { 14 | @ApiProperty({ 15 | description: "故事id", 16 | example: "故事id", 17 | type: Number, 18 | }) 19 | @Expose() 20 | id: number; 21 | 22 | @ApiProperty({ 23 | description: "故事名称", 24 | example: "故事名称", 25 | type: String, 26 | }) 27 | @Expose() 28 | title: string; 29 | 30 | @ApiProperty({ 31 | description: "故事内容", 32 | example: "故事内容", 33 | type: String, 34 | }) 35 | @Expose() 36 | content: string; 37 | 38 | @ApiProperty({ 39 | description: "是否启用", 40 | example: true, 41 | type: Boolean, 42 | }) 43 | @Expose() 44 | @Type(() => Number) 45 | enable: number; 46 | 47 | @ApiProperty({ 48 | description: "创建时间", 49 | example: "2021-07-01 00:00:00", 50 | type: Date, 51 | }) 52 | @Expose() 53 | @Transform(({ value }) => formatTime(value)) 54 | createAt: Date; 55 | 56 | @ApiProperty({ 57 | description: "更新时间", 58 | example: "2021-07-01 00:00:00", 59 | type: Date, 60 | }) 61 | @Expose() 62 | @Transform(({ value }) => formatTime(value)) 63 | updateAt: Date; 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/story/dto/query-story.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 11:27:15 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:42 6 | * @FilePath: \cms\src\modules\story\dto\query-story.dto.ts 7 | * @Description: 8 | */ 9 | import { BaseQueryDto } from "@/shared/dtos"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { IsOptional, IsString } from "class-validator"; 12 | 13 | export class QueryStoryDto extends BaseQueryDto { 14 | @ApiProperty({ 15 | description: "故事标题", 16 | example: "故事标题", 17 | type: String, 18 | }) 19 | @IsString() 20 | @IsOptional() 21 | title: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/story/dto/update-story.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:32 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:45 6 | * @FilePath: \cms\src\modules\story\dto\update-story.dto.ts 7 | * @Description: 8 | */ 9 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 10 | import { ApiProperty, PartialType } from "@nestjs/swagger"; 11 | import { IsOptional } from "class-validator"; 12 | import { CreateStoryDto } from "./create-story.dto"; 13 | 14 | export class UpdateStoryDto extends PartialType(CreateStoryDto) { 15 | @ApiProperty({ 16 | name: "是否启用 ", 17 | example: 0, 18 | type: Number, 19 | description: "0:禁用 1:启用", 20 | }) 21 | @TransformNumber2Boolean() 22 | @IsOptional() 23 | enable: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/story/story.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:32 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 21:54:27 6 | * @FilePath: \cms\src\modules\story\story.controller.ts 7 | * @Description: 8 | */ 9 | import { 10 | Body, 11 | Controller, 12 | Delete, 13 | Get, 14 | HttpCode, 15 | HttpStatus, 16 | Param, 17 | Patch, 18 | Post, 19 | } from "@nestjs/common"; 20 | import { ApiTags } from "@nestjs/swagger"; 21 | import { CreateStoryDto } from "./dto/create-story.dto"; 22 | import { QueryStoryDto } from "./dto/query-story.dto"; 23 | import { UpdateStoryDto } from "./dto/update-story.dto"; 24 | import { StoryService } from "./story.service"; 25 | import { RequirePermission } from "@/shared/decorators"; 26 | import { PermissionEnum } from "@/shared/enums"; 27 | 28 | @Controller("story") 29 | @ApiTags("故事管理模块") 30 | export class StoryController { 31 | constructor(private readonly storyService: StoryService) {} 32 | 33 | @Post() 34 | @HttpCode(HttpStatus.OK) 35 | @RequirePermission(PermissionEnum.SYSTEM_STORY_CREATE) 36 | create(@Body() createStoryDto: CreateStoryDto) { 37 | return this.storyService.create(createStoryDto); 38 | } 39 | 40 | @Post("list") 41 | @HttpCode(HttpStatus.OK) 42 | @RequirePermission(PermissionEnum.SYSTEM_STORY_QUERY) 43 | findAll(@Body() queryStoryDto: QueryStoryDto) { 44 | return this.storyService.findAll(queryStoryDto); 45 | } 46 | 47 | @Get(":id") 48 | @RequirePermission(PermissionEnum.SYSTEM_STORY_QUERY) 49 | findOne(@Param("id") id: string) { 50 | return this.storyService.findOne(+id); 51 | } 52 | 53 | @Patch(":id") 54 | @RequirePermission(PermissionEnum.SYSTEM_STORY_UPDATE) 55 | update(@Param("id") id: string, @Body() updateStoryDto: UpdateStoryDto) { 56 | return this.storyService.update(+id, updateStoryDto); 57 | } 58 | 59 | @Delete(":id") 60 | @RequirePermission(PermissionEnum.SYSTEM_STORY_DELETE) 61 | remove(@Param("id") id: string) { 62 | return this.storyService.remove(+id); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/story/story.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:32 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:50 6 | * @FilePath: \cms\src\modules\story\story.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | import { StoryController } from "./story.controller"; 11 | import { StoryService } from "./story.service"; 12 | 13 | @Module({ 14 | imports: [], 15 | controllers: [StoryController], 16 | providers: [StoryService], 17 | }) 18 | export class StoryModule {} 19 | -------------------------------------------------------------------------------- /src/modules/story/story.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 21:00:32 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:34:57 6 | * @FilePath: \cms\src\modules\story\story.service.ts 7 | * @Description: 8 | */ 9 | import { CacheEvict, Cacheable } from "@/shared/decorators"; 10 | import { RedisKeyEnum } from "@/shared/enums"; 11 | import { AppLoggerSevice } from "@/shared/logger"; 12 | import { PrismaService } from "@/shared/prisma"; 13 | import { handleError } from "@/shared/utils"; 14 | import { 15 | BadRequestException, 16 | ForbiddenException, 17 | Injectable, 18 | } from "@nestjs/common"; 19 | import { Prisma } from "@prisma/client"; 20 | import { plainToInstance } from "class-transformer"; 21 | import DOMPurify from "dompurify"; 22 | import { JSDOM } from "jsdom"; 23 | import { CreateStoryDto } from "./dto/create-story.dto"; 24 | import { ExportStoryListDto } from "./dto/export-story-list.dto"; 25 | import { ExportExportDto } from "./dto/export-story.dto"; 26 | import { QueryStoryDto } from "./dto/query-story.dto"; 27 | import { UpdateStoryDto } from "./dto/update-story.dto"; 28 | @Injectable() 29 | export class StoryService { 30 | constructor( 31 | private readonly logger: AppLoggerSevice, 32 | private readonly prismaService: PrismaService, 33 | ) { 34 | this.logger.setContext(StoryService.name); 35 | } 36 | 37 | /** 38 | * 创建故事 39 | * @param createStoryDto 40 | * @returns 41 | */ 42 | @CacheEvict(RedisKeyEnum.StoryKey) 43 | async create(createStoryDto: CreateStoryDto) { 44 | this.logger.log(`${this.create.name} was called`); 45 | try { 46 | const { content, title } = createStoryDto; 47 | // 进行xss过滤 48 | const window = new JSDOM("").window; 49 | const purify = DOMPurify(window); 50 | const cleanContent = purify.sanitize(content); 51 | await this.prismaService.story.create({ 52 | data: { 53 | title, 54 | content: cleanContent, 55 | }, 56 | }); 57 | return "创建故事成功~"; 58 | } catch (error) { 59 | handleError(this.logger, error, { 60 | common: "创建故事失败", 61 | }); 62 | } 63 | } 64 | 65 | /** 66 | * 查询故事列表 67 | * @param queryStoryDto 68 | * @returns 69 | */ 70 | @Cacheable(RedisKeyEnum.StoryKey) 71 | async findAll(queryStoryDto: QueryStoryDto) { 72 | this.logger.log(`${this.findAll.name} was called`); 73 | 74 | try { 75 | const { createAt, enable, id, offset, size, title, updateAt } = 76 | queryStoryDto; 77 | 78 | const where: Prisma.StoryWhereInput = { 79 | id, 80 | title: { 81 | contains: title, 82 | }, 83 | enable, 84 | createAt: { 85 | gte: createAt?.[0], 86 | lte: createAt?.[1], 87 | }, 88 | updateAt: { 89 | gte: updateAt?.[0], 90 | lte: updateAt?.[1], 91 | }, 92 | isDelete: false, 93 | }; 94 | 95 | const [list, totalCount] = await this.prismaService.$transaction([ 96 | this.prismaService.story.findMany({ 97 | where, 98 | skip: offset, 99 | take: size, 100 | orderBy: { 101 | id: "desc", 102 | }, 103 | }), 104 | this.prismaService.story.count({ where }), 105 | ]); 106 | 107 | return plainToInstance( 108 | ExportStoryListDto, 109 | { 110 | list, 111 | totalCount, 112 | }, 113 | { excludeExtraneousValues: true }, 114 | ); 115 | } catch (error) { 116 | handleError(this.logger, error, { 117 | common: "查询故事列表失败", 118 | }); 119 | } 120 | } 121 | 122 | /** 123 | * 查询故事详情 124 | * @param id 125 | * @returns 126 | */ 127 | async findOne(id: number) { 128 | this.logger.log(`${this.findOne.name} was called`); 129 | try { 130 | const story = await this.prismaService.story.findUnique({ 131 | where: { 132 | id, 133 | isDelete: false, 134 | }, 135 | }); 136 | if (!story) { 137 | throw new BadRequestException("故事不存在"); 138 | } 139 | return plainToInstance(ExportExportDto, story, { 140 | excludeExtraneousValues: true, 141 | }); 142 | } catch (error) { 143 | handleError(this.logger, error, { 144 | common: "查询故事失败", 145 | }); 146 | } 147 | } 148 | 149 | /** 150 | * 更新故事 151 | * @param id 152 | * @param updateStoryDto 153 | * @returns 154 | */ 155 | @CacheEvict(RedisKeyEnum.StoryKey) 156 | async update(id: number, updateStoryDto: UpdateStoryDto) { 157 | this.judgeCanDo(id); 158 | 159 | try { 160 | await this.findOne(id); 161 | const { content, title } = updateStoryDto; 162 | let cleanContent = undefined; 163 | // 如果有内容,就进行xss过滤 164 | if (content) { 165 | const window = new JSDOM("").window; 166 | const purify = DOMPurify(window); 167 | cleanContent = purify.sanitize(content); 168 | } 169 | await this.prismaService.story.update({ 170 | where: { 171 | id, 172 | isDelete: false, 173 | }, 174 | data: { 175 | content: cleanContent, 176 | title, 177 | }, 178 | }); 179 | return "更新故事成功~"; 180 | } catch (error) { 181 | handleError(this.logger, error, { 182 | common: "更新故事失败", 183 | }); 184 | } 185 | } 186 | 187 | /** 188 | * 删除故事 189 | * @param id 190 | * @returns 191 | */ 192 | @CacheEvict(RedisKeyEnum.StoryKey) 193 | async remove(id: number) { 194 | this.logger.log(`${this.remove.name} was called`); 195 | this.judgeCanDo(id); 196 | try { 197 | await this.findOne(id); 198 | await this.prismaService.story.update({ 199 | where: { 200 | id, 201 | isDelete: false, 202 | }, 203 | data: { 204 | isDelete: true, 205 | }, 206 | }); 207 | return "删除故事成功~"; 208 | } catch (error) { 209 | handleError(this.logger, error, { 210 | common: "删除故事失败", 211 | }); 212 | } 213 | } 214 | 215 | /** 216 | * 判断是否能操作 217 | * @param id 218 | */ 219 | judgeCanDo(id: number) { 220 | if (id <= 2) { 221 | throw new ForbiddenException("系统故事不能操作"); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/modules/users/dtos/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:12:21 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:02 6 | * @FilePath: \cms\src\modules\users\dtos\create-user.dto.ts 7 | * @Description: 8 | */ 9 | import { ValidateStringNumber } from "@/shared/decorators"; 10 | import { ApiProperty } from "@nestjs/swagger"; 11 | import { 12 | IsInt, 13 | IsNotEmpty, 14 | IsOptional, 15 | IsString, 16 | MaxLength, 17 | } from "class-validator"; 18 | 19 | export class CreateUserDto { 20 | @ApiProperty({ description: "用户名", type: String }) 21 | @IsNotEmpty({ message: "用户名不能为空" }) 22 | @IsString({ message: "用户名必须是字符串" }) 23 | @MaxLength(32, { message: "用户名长度不能超过32个字符" }) 24 | name: string; 25 | 26 | @ApiProperty({ description: "密码", type: String }) 27 | @IsNotEmpty({ message: "密码不能为空" }) 28 | @IsString({ message: "密码必须是字符串" }) 29 | password: string; 30 | 31 | @ApiProperty({ name: "真实姓名", example: "管理员" }) 32 | @IsString({ message: "真实姓名必须是字符串" }) 33 | @IsNotEmpty({ message: "真实姓名不能为空" }) 34 | realname: string; 35 | 36 | @ApiProperty({ name: "头像" }) 37 | @IsString({ message: "头像必须是字符串" }) 38 | @IsOptional() 39 | avatar: string; 40 | 41 | @ApiProperty({ name: "手机号码", example: 12345678901 }) 42 | @ValidateStringNumber({ message: "手机号码必须是字符串或者数字" }) 43 | @IsOptional() 44 | cellphone: string; 45 | 46 | @ApiProperty({ name: "部门id", example: 1 }) 47 | @IsInt({ message: "部门id必须是整数" }) 48 | @IsNotEmpty({ message: "部门id不能为空" }) 49 | departmentId: number; 50 | 51 | @ApiProperty({ name: "角色id", example: 1 }) 52 | @IsInt({ message: "角色id必须是整数" }) 53 | @IsNotEmpty({ message: "角色id不能为空" }) 54 | roleId: number; 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/users/dtos/export-user-list.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:30:20 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 11:00:50 6 | * @FilePath: \cms\src\modules\users\dtos\export-user-list.dto.ts 7 | * @Description: 8 | */ 9 | import { ExportDepartmentDto } from "@/modules/department/dto/export-department.dto"; 10 | import { ExportRoleDto } from "@/modules/roles/dto/export-role.dto"; 11 | import { formatTime } from "@/shared/utils"; 12 | import { ApiProperty } from "@nestjs/swagger"; 13 | import { Expose, Transform, Type } from "class-transformer"; 14 | import { ExportUserDto } from "./export-user.dto"; 15 | 16 | export class ExportUserListDto { 17 | @ApiProperty({ description: "用户列表", example: [], type: [ExportUserDto] }) 18 | @Expose() 19 | @Type(() => ExportUserListItem) 20 | list: ExportUserListItem[]; 21 | 22 | @ApiProperty({ description: "用户总数", example: 1, type: Number }) 23 | @Expose() 24 | totalCount: number; 25 | } 26 | 27 | class ExportUserListItem { 28 | @ApiProperty({ 29 | description: "用户ID", 30 | example: 1, 31 | type: Number, 32 | }) 33 | @Expose() 34 | id: number; 35 | 36 | @ApiProperty({ 37 | description: "用户名", 38 | example: "John Doe", 39 | type: String, 40 | }) 41 | @Expose() 42 | name: string; 43 | 44 | @ApiProperty({ 45 | description: "真实姓名", 46 | example: "John Smith", 47 | type: String, 48 | }) 49 | @Expose() 50 | realname: string; 51 | 52 | @ApiProperty({ 53 | description: "用户头像", 54 | type: String, 55 | }) 56 | @Expose() 57 | avatar: string; 58 | 59 | @ApiProperty({ 60 | description: "手机号码", 61 | example: "1234567890", 62 | type: Number, 63 | }) 64 | @Expose() 65 | @Transform(({ value }) => value / 1) 66 | cellphone: number; 67 | 68 | @ApiProperty({ 69 | description: "用户是否启用", 70 | example: "1", 71 | type: Number, 72 | }) 73 | @Type(() => Number) 74 | @Expose() 75 | enable: number; 76 | 77 | @ApiProperty({ 78 | description: "用户创建时间", 79 | example: "2022-01-01 00:00:00", 80 | type: Date, 81 | }) 82 | @Expose() 83 | @Transform(({ value }) => formatTime(value)) 84 | createAt: Date; 85 | 86 | @ApiProperty({ 87 | description: "用户最后更新时间", 88 | example: "2022-01-01T00:00:00", 89 | type: Date, 90 | }) 91 | @Expose() 92 | @Transform(({ value }) => formatTime(value)) 93 | updateAt: Date; 94 | 95 | @ApiProperty({ 96 | description: "用户角色Id", 97 | example: 1, 98 | type: Number, 99 | }) 100 | @Expose() 101 | @Type(() => ExportRoleDto) 102 | @Transform(({ obj }) => { 103 | return obj?.userRole?.[0]?.role?.id; 104 | }) 105 | roleId: ExportRoleDto; 106 | 107 | @ApiProperty({ 108 | description: "用户部门Id", 109 | example: 1, 110 | type: Number, 111 | }) 112 | @Expose() 113 | @Type(() => ExportDepartmentDto) 114 | @Transform(({ obj }) => { 115 | return obj?.department?.id; 116 | }) 117 | departmentId: ExportDepartmentDto; 118 | } 119 | -------------------------------------------------------------------------------- /src/modules/users/dtos/export-user.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:12:21 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 10:58:12 6 | * @FilePath: \cms\src\modules\users\dtos\export-user.dto.ts 7 | * @Description: 8 | */ 9 | import { ExportDepartmentDto } from "@/modules/department/dto/export-department.dto"; 10 | import { ExportRoleDto } from "@/modules/roles/dto/export-role.dto"; 11 | import { formatTime } from "@/shared/utils"; 12 | import { ApiProperty } from "@nestjs/swagger"; 13 | import { Expose, Transform, Type } from "class-transformer"; 14 | export class ExportUserDto { 15 | @ApiProperty({ 16 | description: "用户ID", 17 | example: 1, 18 | type: Number, 19 | }) 20 | @Expose() 21 | id: number; 22 | 23 | @ApiProperty({ 24 | description: "用户名", 25 | example: "John Doe", 26 | type: String, 27 | }) 28 | @Expose() 29 | name: string; 30 | 31 | @ApiProperty({ 32 | description: "真实姓名", 33 | example: "John Smith", 34 | type: String, 35 | }) 36 | @Expose() 37 | realname: string; 38 | 39 | @ApiProperty({ 40 | description: "用户头像", 41 | type: String, 42 | }) 43 | @Expose() 44 | avatar: string; 45 | 46 | @ApiProperty({ 47 | description: "手机号码", 48 | example: "1234567890", 49 | type: Number, 50 | }) 51 | @Expose() 52 | @Transform(({ value }) => value / 1) 53 | cellphone: number; 54 | 55 | @ApiProperty({ 56 | description: "用户是否启用", 57 | example: 1, 58 | type: Number, 59 | }) 60 | @Expose() 61 | @Type(() => Number) 62 | enable: number; 63 | 64 | @ApiProperty({ 65 | description: "用户创建时间", 66 | example: "2022-01-01 00:00:00", 67 | type: Date, 68 | }) 69 | @Expose() 70 | @Transform(({ value }) => formatTime(value)) 71 | createAt: Date; 72 | 73 | @ApiProperty({ 74 | description: "用户最后更新时间", 75 | example: "2022-01-01 00:00:00", 76 | type: Date, 77 | }) 78 | @Expose() 79 | @Transform(({ value }) => formatTime(value)) 80 | updateAt: Date; 81 | 82 | @ApiProperty({ 83 | description: "用户角色", 84 | example: ExportRoleDto, 85 | type: ExportRoleDto, 86 | }) 87 | @Expose() 88 | @Type(() => ExportRoleDto) 89 | role: ExportRoleDto; 90 | 91 | @ApiProperty({ 92 | description: "用户部门", 93 | example: ExportDepartmentDto, 94 | type: ExportDepartmentDto, 95 | }) 96 | @Expose() 97 | @Type(() => ExportDepartmentDto) 98 | department: ExportDepartmentDto; 99 | } 100 | -------------------------------------------------------------------------------- /src/modules/users/dtos/query-user.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:30:06 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:13 6 | * @FilePath: \cms\src\modules\users\dtos\query-user.dto.ts 7 | * @Description: 8 | */ 9 | import { ValidateStringNumber } from "@/shared/decorators"; 10 | import { BaseQueryDto } from "@/shared/dtos"; 11 | import { ApiProperty } from "@nestjs/swagger"; 12 | import { Type } from "class-transformer"; 13 | import { IsInt, IsOptional, IsString } from "class-validator"; 14 | 15 | export class QueryUserDto extends BaseQueryDto { 16 | @ApiProperty({ 17 | name: "手机号", 18 | example: 12345678901, 19 | type: Number, 20 | description: "手机号", 21 | }) 22 | @ValidateStringNumber({ message: "手机号必须是字符串或者数字" }) 23 | @Type(() => String) 24 | @IsOptional() 25 | cellphone: string; 26 | 27 | @ApiProperty({ name: "用户名", example: "张三", type: String }) 28 | @IsString({ message: "用户名必须是字符串" }) 29 | @IsOptional() 30 | name: string; 31 | 32 | @ApiProperty({ name: "用户真实姓名", example: "张三", type: String }) 33 | @IsString({ message: "用户真实姓名必须是字符串" }) 34 | @IsOptional() 35 | realname: string; 36 | 37 | @ApiProperty({ name: "角色id", example: 1, type: Number }) 38 | @IsInt({ message: "角色id必须是数字" }) 39 | @IsOptional() 40 | roleId: number; 41 | 42 | @ApiProperty({ name: "部门id", example: 1, type: Number }) 43 | @IsInt({ message: "部门id必须是数字" }) 44 | @IsOptional() 45 | departmentId: number; 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/users/dtos/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 19:12:21 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:16 6 | * @FilePath: \cms\src\modules\users\dtos\update-user.dto.ts 7 | * @Description: 8 | */ 9 | import { TransformNumber2Boolean } from "@/shared/decorators/transform-number-to-boolean"; 10 | import { ApiProperty, PartialType } from "@nestjs/swagger"; 11 | import { IsOptional } from "class-validator"; 12 | import { CreateUserDto } from "./create-user.dto"; 13 | 14 | export class UpdateUserDto extends PartialType(CreateUserDto) { 15 | @ApiProperty({ 16 | name: "是否启用 ", 17 | example: 0, 18 | type: Number, 19 | description: "0:禁用 1:启用", 20 | }) 21 | @TransformNumber2Boolean() 22 | @IsOptional() 23 | enable: boolean; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 17:08:57 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-18 12:00:06 6 | * @FilePath: \cms\src\modules\users\users.controller.ts 7 | * @Description: 8 | */ 9 | import { RequirePermission } from "@/shared/decorators"; 10 | import { PermissionEnum } from "@/shared/enums"; 11 | import { 12 | Body, 13 | Controller, 14 | Delete, 15 | Get, 16 | HttpCode, 17 | HttpStatus, 18 | Param, 19 | Patch, 20 | Post, 21 | } from "@nestjs/common"; 22 | import { ApiTags } from "@nestjs/swagger"; 23 | import { CreateUserDto } from "./dtos/create-user.dto"; 24 | import { QueryUserDto } from "./dtos/query-user.dto"; 25 | import { UpdateUserDto } from "./dtos/update-user.dto"; 26 | import { UsersService } from "./users.service"; 27 | 28 | @Controller("users") 29 | @ApiTags("用户管理模块") 30 | export class UsersController { 31 | constructor(private readonly usersService: UsersService) {} 32 | 33 | /** 34 | * 用户注册 35 | * @param registerAccountDto 注册信息 36 | * @returns 37 | */ 38 | @Post() 39 | @HttpCode(HttpStatus.OK) 40 | @RequirePermission(PermissionEnum.SYSTEM_USERS_CREATE) 41 | register(@Body() createUserDto: CreateUserDto) { 42 | return this.usersService.createUser(createUserDto); 43 | } 44 | 45 | /** 46 | * 查询用户 47 | * @param id 用户id 48 | * @returns 49 | */ 50 | @Get(":id") 51 | findOne(@Param("id") id: string) { 52 | return this.usersService.findUserById(+id); 53 | } 54 | 55 | /** 56 | * 查询用户列表 57 | * @param queryUserDto 查询条件 58 | * @returns 59 | */ 60 | @Post("list") 61 | @HttpCode(HttpStatus.OK) 62 | @RequirePermission(PermissionEnum.SYSTEM_USERS_QUERY) 63 | findAll(@Body() queryUserDto: QueryUserDto) { 64 | return this.usersService.findAll(queryUserDto); 65 | } 66 | 67 | /** 68 | * 更新用户 69 | * @param id 用户id 70 | * @param updateUserDto 更新信息 71 | * @returns 72 | */ 73 | @Patch(":id") 74 | @RequirePermission(PermissionEnum.SYSTEM_USERS_UPDATE) 75 | update(@Param("id") id: string, @Body() updateUserDto: UpdateUserDto) { 76 | return this.usersService.updateUser(+id, updateUserDto); 77 | } 78 | 79 | /** 80 | * 删除用户 81 | * @param id 用户id 82 | * @returns 83 | */ 84 | @Delete(":id") 85 | @HttpCode(HttpStatus.OK) 86 | @RequirePermission(PermissionEnum.SYSTEM_USERS_DELETE) 87 | remove(@Param("id") id: string) { 88 | return this.usersService.remove(+id); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/modules/users/users.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 17:08:57 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-18 10:15:43 6 | * @FilePath: \cms\src\modules\users\users.module.ts 7 | * @Description: 8 | */ 9 | import { Global, Module, forwardRef } from "@nestjs/common"; 10 | import { DepartmentModule } from "../department/department.module"; 11 | import { RolesModule } from "../roles/roles.module"; 12 | import { UsersController } from "./users.controller"; 13 | import { UsersService } from "./users.service"; 14 | 15 | @Global() 16 | @Module({ 17 | imports: [forwardRef(() => RolesModule), forwardRef(() => DepartmentModule)], 18 | controllers: [UsersController], 19 | providers: [UsersService], 20 | exports: [UsersService, RolesModule, DepartmentModule], 21 | }) 22 | export class UsersModule {} 23 | -------------------------------------------------------------------------------- /src/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | async function main() { 8 | const sqls = fs 9 | .readFileSync(path.join(__dirname, "../sql/all.sql"), "utf-8") 10 | .split(";") 11 | .filter((item) => item.trim() != ""); 12 | for await (const sql of sqls) { 13 | await prisma.$executeRawUnsafe(sql); 14 | } 15 | } 16 | 17 | main() 18 | .then(async () => { 19 | await prisma.$disconnect(); 20 | }) 21 | .catch(async (e) => { 22 | console.error(e); 23 | await prisma.$disconnect(); 24 | process.exit(1); 25 | }); 26 | -------------------------------------------------------------------------------- /src/shared/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./loadEnv.config"; 2 | export * from "./validateEnv.config"; 3 | -------------------------------------------------------------------------------- /src/shared/config/loadEnv.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-18 19:38:46 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:40 6 | * @FilePath: \cms\src\shared\config\loadEnv.config.ts 7 | * @Description: 8 | */ 9 | import * as dotebnv from "dotenv"; 10 | import { EnvEnum } from "../enums/env.enum"; 11 | 12 | dotebnv.config({ path: `.env.${process.env.NODE_ENV}` }); 13 | 14 | export const loadEnvConfig = (): Record => { 15 | return { 16 | APP_ENV: process.env.APP_ENV, 17 | APP_PORT: process.env.APP_PORT, 18 | DATABASE_URL: process.env.DATABASE_URL, 19 | JWT_ACCESS_TOKEN_EXPIRES_IN: process.env.JWT_ACCESS_TOKEN_EXPIRES_IN, 20 | JWT_REFRESH_TOKEN_EXPIRES_IN: process.env.JWT_REFRESH_TOKEN_EXPIRES_IN, 21 | JWT_PRIVATE_KEY: process.env.JWT_PRIVATE_KEY, 22 | JWT_PUBLIC_KEY: process.env.JWT_PUBLIC_KEY, 23 | LOG_LEVEL: process.env.LOG_LEVEL, 24 | TIMESTAMP: process.env.TIMESTAMP, 25 | LOG_ON: process.env.LOG_ON, 26 | REDIS_HOST: process.env.REDIS_HOST, 27 | REDIS_PASSWORD: process.env.REDIS_PASSWORD, 28 | REDIS_PORT: process.env.REDIS_PORT, 29 | UPLOAD_ADDRESS: process.env.UPLOAD_ADDRESS, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /src/shared/config/validateEnv.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-18 19:40:09 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:44 6 | * @FilePath: \cms\src\shared\config\validateEnv.config.ts 7 | * @Description: 8 | */ 9 | import * as Joi from "joi"; 10 | import { EnvEnum } from "../enums/env.enum"; 11 | export const validationSchema = Joi.object({ 12 | DATABASE_URL: Joi.string().required(), 13 | APP_ENV: Joi.string() 14 | .valid("development", "production") 15 | .default("development"), 16 | APP_PORT: Joi.number().default(3000), 17 | JWT_ACCESS_TOKEN_EXPIRES_IN: Joi.string().default("1h"), 18 | JWT_REFRESH_TOKEN_EXPIRES_IN: Joi.string().default("7d"), 19 | JWT_PRIVATE_KEY: Joi.string().required(), 20 | JWT_PUBLIC_KEY: Joi.string().required(), 21 | UPLOAD_ADDRESS: Joi.string().required(), 22 | } as Record); 23 | -------------------------------------------------------------------------------- /src/shared/decorators/cache.decorator.ts: -------------------------------------------------------------------------------- 1 | import { RedisKeyEnum } from "../enums"; 2 | import { getGlobalApp } from "../global"; 3 | import { RedisService } from "../redis"; 4 | import { filterEmpty } from "../utils"; 5 | 6 | //我目前没有可以优化的想法,你可以自己实现 7 | 8 | export const Cacheable = (...keys: RedisKeyEnum[]) => { 9 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 10 | const originalMethod = descriptor.value; 11 | descriptor.value = async function (...args: any[]) { 12 | let cacheKey = ""; 13 | const redisService = getGlobalApp().get(RedisService); 14 | 15 | //如果没有参数,那么就是key:propertyKey 16 | if (args.length === 0) { 17 | //如果只有一个key,那么就是key:propertyKey 18 | if (keys.length === 1) { 19 | cacheKey = `${keys[0]}:${propertyKey}`; 20 | } else if (keys.length > 1) { 21 | //如果有多个key,那么就是key1:key2:propertyKey 22 | keys.forEach((key) => { 23 | cacheKey += `${key}:`; 24 | }); 25 | cacheKey = cacheKey + propertyKey; 26 | } 27 | } else { 28 | //如果有参数,那么就是key:propertyKey:args 29 | if (keys.length === 1) { 30 | //如果只有一个key,那么就是key:propertyKey:args 31 | cacheKey = `${keys[0]}:${propertyKey}:${JSON.stringify( 32 | args.map((arg) => filterEmpty(arg)), 33 | )}`; 34 | } else if (keys.length > 1) { 35 | //如果有多个key,那么就是key1:key2:propertyKey:args 36 | keys.forEach((key) => { 37 | cacheKey += `${key}:`; 38 | }); 39 | cacheKey = 40 | cacheKey + 41 | propertyKey + 42 | `:${JSON.stringify(args.map((arg) => filterEmpty(arg)))}`; 43 | } 44 | } 45 | const cacheValue = await redisService._get(cacheKey); 46 | if (cacheValue) { 47 | return cacheValue; 48 | } 49 | const result = await originalMethod.apply(this, args); 50 | redisService._set(cacheKey, result); 51 | return result; 52 | }; 53 | }; 54 | }; 55 | 56 | export const CachePut = (key: RedisKeyEnum) => { 57 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 58 | const originalMethod = descriptor.value; 59 | descriptor.value = async function (...args: any[]) { 60 | const redisService = getGlobalApp().get(RedisService); 61 | const result = await originalMethod.apply(this, args); 62 | redisService._set(key, result); 63 | return result; 64 | }; 65 | }; 66 | }; 67 | 68 | //会删除所有包含key的缓存 69 | export const CacheEvict = (...keys: RedisKeyEnum[]) => { 70 | return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { 71 | const originalMethod = descriptor.value; 72 | descriptor.value = async function (...args: any[]) { 73 | const redisService = getGlobalApp().get(RedisService); 74 | const result = await originalMethod.apply(this, args); 75 | keys.map(async (key) => { 76 | await redisService._delKeysContainStr(key); 77 | }); 78 | return result; 79 | }; 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /src/shared/decorators/expose-not-null.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-05 19:37:54 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:48 6 | * @FilePath: \cms\src\shared\decorators\expose-not-null.decorator.ts 7 | * @Description: 8 | */ 9 | import { applyDecorators } from "@nestjs/common"; 10 | import { Expose, Transform } from "class-transformer"; 11 | 12 | /** 13 | * @description: 暴露并且排除null值 14 | * @param {*} 15 | * @return {*} 16 | */ 17 | export function ExposeNotNull() { 18 | return applyDecorators( 19 | Expose(), 20 | Transform(({ value }) => { 21 | if (value instanceof Array) { 22 | //判断是否为数组 23 | return value[0] && value; 24 | } else if (value instanceof Object) { 25 | //判断是否为对象 26 | return Object.keys(value).length > 0 && value; 27 | } else { 28 | if (!value) return; 29 | return value; 30 | } 31 | }), 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/shared/decorators/get-current-user-id.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-22 10:59:55 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 11:39:58 6 | * @FilePath: \cms\src\shared\decorators\get-current-user-id.decorator.ts 7 | * @Description: 8 | */ 9 | import { ExecutionContext, createParamDecorator } from "@nestjs/common"; 10 | import { Request } from "express"; 11 | 12 | export const GetCurrentUserID = createParamDecorator( 13 | (_: undefined, context: ExecutionContext) => { 14 | const ctx = context.switchToHttp(); 15 | const req = ctx.getRequest(); 16 | return req.user?.["id"]; 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/shared/decorators/get-current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-22 11:00:15 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:50:36 6 | * @FilePath: \cms\src\shared\decorators\get-current-user.decorator.ts 7 | * @Description: 8 | */ 9 | import { ExecutionContext, createParamDecorator } from "@nestjs/common"; 10 | import { Request } from "express"; 11 | 12 | export const GetCurrentUser = createParamDecorator( 13 | (param: any, context: ExecutionContext) => { 14 | const ctx = context.switchToHttp(); 15 | const req = ctx.getRequest(); 16 | return req.user[param]; 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/shared/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./cache.decorator"; 2 | export * from "./expose-not-null.decorator"; 3 | export * from "./get-current-user-id.decorator"; 4 | export * from "./get-current-user.decorator"; 5 | export * from "./public.decorator"; 6 | export * from "./require-permission.decorator"; 7 | export * from "./validate-array.decorator"; 8 | export * from "./validate-date.decorator"; 9 | export * from "./validate-string-number.decorator"; 10 | -------------------------------------------------------------------------------- /src/shared/decorators/public.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 20:44:12 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:50:40 6 | * @FilePath: \cms\src\shared\decorators\public.decorator.ts 7 | * @Description: 8 | */ 9 | import { SetMetadata } from "@nestjs/common"; 10 | import { DecoratorEnum } from "../enums/decorator.enum"; 11 | 12 | export const Public = () => SetMetadata(DecoratorEnum.IS_PUBLIC, true); 13 | -------------------------------------------------------------------------------- /src/shared/decorators/record-time.decorator.ts: -------------------------------------------------------------------------------- 1 | export function RecordTime() { 2 | return function ( 3 | target: any, 4 | propertyKey: string, 5 | descriptor: PropertyDescriptor, 6 | ) { 7 | const method = descriptor.value; 8 | descriptor.value = async function (...args: any[]) { 9 | const startTime = Date.now(); 10 | const result = await method.apply(this, args); 11 | const endTime = Date.now(); 12 | this.logger.log( 13 | `${propertyKey} was called , args: ${args} time: 🚀 ${ 14 | endTime - startTime 15 | }ms`, 16 | ); 17 | return result; 18 | }; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/decorators/require-permission.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-03 15:55:23 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:50:54 6 | * @FilePath: \cms\src\shared\decorators\require-permission.decorator.ts 7 | * @Description: 8 | */ 9 | import { SetMetadata } from "@nestjs/common"; 10 | import { DecoratorEnum } from "../enums/decorator.enum"; 11 | import { PermissionEnum } from "../enums/permission.enum"; 12 | 13 | export interface RequirePermissionOptions { 14 | permission: PermissionEnum[]; 15 | logical: "or" | "and"; 16 | } 17 | 18 | export const RequirePermission = ( 19 | permission: PermissionEnum | PermissionEnum[], 20 | logical?: "or" | "and", 21 | ) => { 22 | return SetMetadata( 23 | DecoratorEnum.REQUIRE_PERMISSION, 24 | { 25 | permission: Array.isArray(permission) ? permission : [permission], 26 | logical: logical || "or", 27 | }, 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/shared/decorators/transform-number-to-boolean.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-05 19:37:54 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:35:48 6 | * @FilePath: \cms\src\shared\decorators\expose-not-null.decorator.ts 7 | * @Description: 8 | */ 9 | import { BadRequestException, applyDecorators } from "@nestjs/common"; 10 | import { Transform, Type } from "class-transformer"; 11 | 12 | /** 13 | * @description: 将字符串/数字转换为布尔值除了undefined和null 14 | * @param {*} 15 | * @return {*} 16 | */ 17 | export function TransformNumber2Boolean() { 18 | return applyDecorators( 19 | Type(() => Number), 20 | Transform(({ value, key }) => { 21 | if (value === undefined || value === null) return undefined; 22 | if (typeof value !== "number" && typeof value != "string") { 23 | throw new BadRequestException(`${key}必须是数字或者字符串`); 24 | } 25 | return !!value; 26 | }), 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/decorators/validate-array.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-14 14:13:44 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:51:02 6 | * @FilePath: \cms\src\shared\decorators\validate-number-array.decorator.ts 7 | * @Description: 8 | */ 9 | import { BadRequestException } from "@nestjs/common"; 10 | import { Transform } from "class-transformer"; 11 | 12 | /** 13 | * 验证数组是否为数字数组 14 | * @param name 字段名 15 | * @param length 数组长度 16 | */ 17 | export function ValidateArrary(name: string, length = 2) { 18 | return Transform(({ value, key }) => { 19 | //1.判断是否是数组 20 | if (!Array.isArray(value)) { 21 | throw new BadRequestException(`${name}必须是数组`); 22 | } 23 | //2.判断数组长度是否为length 24 | if (value.length === 0) return undefined; 25 | if (value.length > length) { 26 | throw new BadRequestException(`${name}数组长度必须小于${length + 1}`); 27 | } 28 | //3.判断数组元素是否为数字 29 | if ( 30 | value.some((item) => { 31 | return typeof item !== "number" && typeof item !== "string"; 32 | }) 33 | ) { 34 | throw new BadRequestException(`${name}数组元素必须为数字或字符串`); 35 | } 36 | return value; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/shared/decorators/validate-date.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-10 18:31:40 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 10:44:17 6 | * @FilePath: \cms\src\shared\decorators\validate-date.decorator.ts 7 | * @Description: 8 | */ 9 | import { BadRequestException } from "@nestjs/common"; 10 | import { Transform } from "class-transformer"; 11 | 12 | /** 13 | * 验证日期 14 | * @returns 15 | */ 16 | export function ValidateDate() { 17 | return Transform(({ value, key }) => { 18 | if (!value) return undefined; 19 | //1.判断是否是数组 20 | if (!Array.isArray(value)) { 21 | throw new BadRequestException(`${key} 必须是数组`); 22 | } 23 | //2.判断数组长度是否为二 24 | if (value.length === 0) return undefined; 25 | if (value.length != 2) { 26 | throw new BadRequestException(`${key} 数组长度必须为二`); 27 | } 28 | //3.判断数组元素是否为日期 29 | if ( 30 | value.some((item) => { 31 | return new Date(item).toString() === "Invalid Date"; 32 | }) 33 | ) { 34 | throw new BadRequestException(`${key} 数组元素必须为日期`); 35 | } 36 | //4.返回日期 37 | //由于prisma的时间是UTC时间,所以需要减去8小时 38 | return [ 39 | new Date(new Date(value[0]).getTime() - 8 * 60 * 60 * 1000), 40 | new Date(new Date(value[1]).getTime() - 8 * 60 * 60 * 1000), 41 | ]; 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/decorators/validate-string-number.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-11 16:02:53 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:51:06 6 | * @FilePath: \cms\src\shared\decorators\validate-string-number.decorator.ts 7 | * @Description: 8 | */ 9 | import { ValidationOptions, registerDecorator } from "class-validator"; 10 | 11 | /** 12 | * @description: 验证是否是字符串或者数字 13 | * @param {string} property 14 | * @param {ValidationOptions} validationOptions 15 | * @return {*} 16 | */ 17 | export function ValidateStringNumber(validationOptions?: ValidationOptions) { 18 | return function (object: object, propertyName: string) { 19 | registerDecorator({ 20 | name: "ValidateStringNumber", 21 | target: object.constructor, 22 | propertyName: propertyName, 23 | options: validationOptions, 24 | validator: { 25 | validate(value: any) { 26 | if (typeof value !== "string" && typeof value !== "number") 27 | return false; 28 | return true; 29 | }, 30 | }, 31 | }); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/shared/dtos/base-api-response.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-18 20:15:14 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-19 12:51:51 6 | * @FilePath: \cms\src\shared\dtos\base-api-response.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | 11 | export class BaseApiResponse { 12 | data: T; // Swagger Decorator is added in the extended class below, since that will override this one. 13 | code: number; 14 | message: string; 15 | } 16 | 17 | export function SwaggerBaseApiResponse(type: T): typeof BaseApiResponse { 18 | class ExtendedBaseApiResponse extends BaseApiResponse { 19 | @ApiProperty({ type }) 20 | public data: T; 21 | @ApiProperty({ type: Number }) 22 | public code: number; 23 | @ApiProperty({ type: String }) 24 | public message: string; 25 | } 26 | 27 | const isAnArray = Array.isArray(type) ? " [ ] " : ""; 28 | Object.defineProperty(ExtendedBaseApiResponse, "name", { 29 | value: `${isAnArray}`, 30 | }); 31 | 32 | return ExtendedBaseApiResponse; 33 | } 34 | 35 | export class BaseApiErrorResponse { 36 | @ApiProperty({ type: Number }) 37 | public code: number; 38 | 39 | @ApiProperty({ type: String }) 40 | public message: string; 41 | 42 | @ApiProperty({ type: String }) 43 | public path: string; 44 | @ApiProperty({ type: String }) 45 | public timestamp: string; 46 | } 47 | -------------------------------------------------------------------------------- /src/shared/dtos/base-pagination.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-08 19:56:10 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:51:12 6 | * @FilePath: \cms\src\shared\dtos\base-pagination.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { IsInt, IsNotEmpty } from "class-validator"; 11 | 12 | export class BasePaginationDto { 13 | @ApiProperty({ type: Number, example: 10 }) 14 | @IsInt({ message: "size必须是整数" }) 15 | @IsNotEmpty({ message: "size不能为空" }) 16 | size: number; 17 | 18 | @ApiProperty({ type: Number, example: 0 }) 19 | @IsInt({ message: "offset必须是整数" }) 20 | @IsNotEmpty({ message: "offset不能为空" }) 21 | offset: number; 22 | } 23 | -------------------------------------------------------------------------------- /src/shared/dtos/base-query.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 22:01:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-15 11:20:49 6 | * @FilePath: \cms\src\shared\dtos\base-query.dto.ts 7 | * @Description: 8 | */ 9 | import { ApiProperty } from "@nestjs/swagger"; 10 | import { Type } from "class-transformer"; 11 | import { IsOptional } from "class-validator"; 12 | import { TransformNumber2Boolean } from "../decorators/transform-number-to-boolean"; 13 | import { ValidateDate } from "../decorators/validate-date.decorator"; 14 | import { ValidateStringNumber } from "../decorators/validate-string-number.decorator"; 15 | import { BasePaginationDto } from "./base-pagination.dto"; 16 | 17 | export class BaseQueryDto extends BasePaginationDto { 18 | @ApiProperty({ name: "id", example: 1, type: Number }) 19 | @ValidateStringNumber({ message: "id必须是字符串或者数字" }) 20 | @Type(() => Number) 21 | @IsOptional() 22 | id: number; 23 | 24 | @ApiProperty({ 25 | name: "是否启用 ", 26 | example: 0, 27 | type: Boolean, 28 | description: "0:禁用 1:启用", 29 | }) 30 | @TransformNumber2Boolean() 31 | @IsOptional() 32 | enable: boolean; 33 | 34 | @ApiProperty({ name: "创建时间", example: "2021-10-10", type: [Date, Date] }) 35 | @IsOptional() 36 | @ValidateDate() 37 | createAt: Array; 38 | 39 | @ApiProperty({ name: "更新时间", example: "2021-10-10", type: [Date, Date] }) 40 | @ValidateDate() 41 | @IsOptional() 42 | updateAt: Array; 43 | } 44 | -------------------------------------------------------------------------------- /src/shared/dtos/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base-api-response.dto"; 2 | export * from "./base-pagination.dto"; 3 | export * from "./base-query.dto"; 4 | -------------------------------------------------------------------------------- /src/shared/enums/decorator.enum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-03 15:56:54 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:52:12 6 | * @FilePath: \cms\src\shared\enums\decorator.enum.ts 7 | * @Description: 8 | */ 9 | export enum DecoratorEnum { 10 | REQUIRE_PERMISSION = "require-permission", 11 | IS_PUBLIC = "is-public", 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/enums/env.enum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 19:32:39 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-21 20:27:34 6 | * @FilePath: \cms\src\shared\enums\env.enum.ts 7 | * @Description: 8 | */ 9 | export enum EnvEnum { 10 | APP_ENV = "APP_ENV", 11 | APP_PORT = "APP_PORT", 12 | 13 | DATABASE_URL = "DATABASE_URL", 14 | 15 | JWT_PUBLIC_KEY = "JWT_PUBLIC_KEY", 16 | JWT_PRIVATE_KEY = "JWT_PRIVATE_KEY", 17 | JWT_ACCESS_TOKEN_EXPIRES_IN = "JWT_ACCESS_TOKEN_EXPIRES_IN", 18 | JWT_REFRESH_TOKEN_EXPIRES_IN = "JWT_REFRESH_TOKEN_EXPIRES_IN", 19 | 20 | LOG_LEVEL = "LOG_LEVEL", 21 | TIMESTAMP = "TIMESTAMP", 22 | LOG_ON = "LOG_ON", 23 | 24 | REDIS_PORT = "REDIS_PORT", 25 | REDIS_HOST = "REDIS_HOST", 26 | REDIS_PASSWORD = "REDIS_PASSWORD", 27 | 28 | UPLOAD_ADDRESS = "UPLOAD_ADDRESS", 29 | } 30 | -------------------------------------------------------------------------------- /src/shared/enums/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./decorator.enum"; 2 | export * from "./env.enum"; 3 | export * from "./permission.enum"; 4 | export * from "./prisma-error-code.enum"; 5 | export * from "./redis-key.enum"; 6 | export * from "./scan-status.enum"; 7 | export * from "./strategy.enum"; 8 | -------------------------------------------------------------------------------- /src/shared/enums/permission.enum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-03 16:08:16 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:52:20 6 | * @FilePath: \cms\src\shared\enums\permission.enum.ts 7 | * @Description: 8 | */ 9 | export enum PermissionEnum { 10 | ALL = "*:*:*", 11 | SYSTEM_USERS_CREATE = "system:users:create", 12 | SYSTEM_USERS_UPDATE = "system:users:update", 13 | SYSTEM_USERS_DELETE = "system:users:delete", 14 | SYSTEM_USERS_QUERY = "system:users:query", 15 | 16 | SYSTEM_DEPARTMENT_CREATE = "system:department:create", 17 | SYSTEM_DEPARTMENT_UPDATE = "system:department:update", 18 | SYSTEM_DEPARTMENT_DELETE = "system:department:delete", 19 | SYSTEM_DEPARTMENT_QUERY = "system:department:query", 20 | 21 | SYSTEM_MENU_CREATE = "system:menu:create", 22 | SYSTEM_MENU_UPDATE = "system:menu:update", 23 | SYSTEM_MENU_DELETE = "system:menu:delete", 24 | SYSTEM_MENU_QUERY = "system:menu:query", 25 | 26 | SYSTEM_ROLE_CREATE = "system:role:create", 27 | SYSTEM_ROLE_UPDATE = "system:role:update", 28 | SYSTEM_ROLE_DELETE = "system:role:delete", 29 | SYSTEM_ROLE_QUERY = "system:role:query", 30 | 31 | SYSTEM_CATEGORY_CREATE = "system:category:create", 32 | SYSTEM_CATEGORY_UPDATE = "system:category:update", 33 | SYSTEM_CATEGORY_DELETE = "system:category:delete", 34 | SYSTEM_CATEGORY_QUERY = "system:category:query", 35 | 36 | SYSTEM_GOODS_CREATE = "system:goods:create", 37 | SYSTEM_GOODS_UPDATE = "system:goods:update", 38 | SYSTEM_GOODS_DELETE = "system:goods:delete", 39 | SYSTEM_GOODS_QUERY = "system:goods:query", 40 | 41 | SYSTEM_STORY_CREATE = "system:story:create", 42 | SYSTEM_STORY_UPDATE = "system:story:update", 43 | SYSTEM_STORY_DELETE = "system:story:delete", 44 | SYSTEM_STORY_QUERY = "system:story:query", 45 | } 46 | -------------------------------------------------------------------------------- /src/shared/enums/prisma-error-code.enum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 18:10:54 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-15 18:11:08 6 | * @FilePath: \cms\src\enums\http-status.enum.ts 7 | * @Description: 8 | */ 9 | export enum PrismaErrorCode { 10 | UniqueConstraintViolation = "P2002", 11 | ForeignKeyConstraintViolation = "P2003", 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/enums/redis-key.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RedisKeyEnum { 2 | LoginKey = "login-->", 3 | MenuKey = "menu", 4 | RoleKey = "role-->", 5 | StoryKey = "story-->", 6 | DepartmentKey = "department-->", 7 | GoodsInfoKey = "goodsInfo-->", 8 | GoodsCategoryKey = "goodsCategory-->", 9 | UserKey = "user-->", 10 | QrcodeKey = "qrcode-->", 11 | } 12 | -------------------------------------------------------------------------------- /src/shared/enums/scan-status.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ScanStatusEnum { 2 | // 未扫码 3 | NotScan = 0, 4 | // 已扫码 5 | Scanned = 1, 6 | // 已确认 7 | Confirmed = 2, 8 | // 已取消 9 | Canceled = 3, 10 | // 已过期 11 | Expired = 4, 12 | } 13 | -------------------------------------------------------------------------------- /src/shared/enums/strategy.enum.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 16:34:06 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:52:23 6 | * @FilePath: \cms\src\shared\enums\strategy.enum.ts 7 | * @Description: 8 | */ 9 | export enum StrategyEnum { 10 | LOCAL = "local", 11 | JWT_REFRESH = "jwt-refresh", 12 | JWT_ACCESS = "jwt-access", 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/filters/all-exceptions.filter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 18:56:47 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:52:10 6 | * @FilePath: \cms\src\shared\filters\all-exceptions.filter.ts 7 | * @Description: 8 | */ 9 | import { 10 | ArgumentsHost, 11 | Catch, 12 | ExceptionFilter, 13 | HttpException, 14 | HttpStatus, 15 | } from "@nestjs/common"; 16 | import { Request, Response } from "express"; 17 | import { AppLoggerSevice } from "../logger"; 18 | import { formatTime } from "../utils"; 19 | import { getClientIp } from "../utils"; 20 | 21 | @Catch() 22 | export class AllExceptionsFilter implements ExceptionFilter { 23 | constructor(private readonly logger: AppLoggerSevice) {} 24 | catch(exception: any, host: ArgumentsHost): void { 25 | const ctx = host.switchToHttp(); 26 | const request = ctx.getRequest(); 27 | const response = ctx.getResponse(); 28 | 29 | const httpStatus = 30 | exception instanceof HttpException 31 | ? exception.getStatus() 32 | : HttpStatus.INTERNAL_SERVER_ERROR; 33 | 34 | const message = 35 | exception instanceof HttpException 36 | ? (exception.getResponse() as any).message instanceof Array 37 | ? (exception.getResponse() as any).message.join(",") 38 | : (exception.getResponse() as any).message 39 | : "服务器错误,请稍后重试。"; 40 | 41 | const responseBody = { 42 | code: httpStatus, 43 | message, 44 | path: request.url, 45 | timestamp: formatTime(new Date()), 46 | }; 47 | 48 | if ( 49 | responseBody.message == "TokenExpiredError: jwt expired" || 50 | responseBody.message == "JsonWebTokenError: invalid signature" 51 | ) { 52 | responseBody.message = "登录已过期,请重新登录"; 53 | } else if (responseBody.message == "Error: No auth token") { 54 | responseBody.message = "请先登录"; 55 | } else if (httpStatus == 429) { 56 | responseBody.message = "请求过于频繁,请稍后再试"; 57 | } 58 | 59 | this.logger.error( 60 | { 61 | ...responseBody, 62 | ip: getClientIp(request), 63 | }, 64 | null, 65 | "AllExceptions", 66 | ); 67 | 68 | response.status(httpStatus).json(responseBody); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/shared/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./all-exceptions.filter"; 2 | -------------------------------------------------------------------------------- /src/shared/global/index.ts: -------------------------------------------------------------------------------- 1 | let globalApp: any; 2 | 3 | export const setGlobalApp = (app: any) => { 4 | globalApp = app; 5 | }; 6 | 7 | export const getGlobalApp = () => { 8 | if (!globalApp) { 9 | throw new Error("获取全局实例失败"); 10 | } 11 | return globalApp; 12 | }; 13 | -------------------------------------------------------------------------------- /src/shared/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./jwt-access.guard"; 2 | export * from "./jwt-local.guard"; 3 | export * from "./jwt-refresh.guard"; 4 | export * from "./permission-auth.guard"; 5 | -------------------------------------------------------------------------------- /src/shared/guards/jwt-access.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-21 17:03:40 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 09:49:29 6 | * @FilePath: \cms\src\shared\guards\jwt-access.guard.ts 7 | * @Description: 8 | */ 9 | import { 10 | ExecutionContext, 11 | Injectable, 12 | UnauthorizedException, 13 | } from "@nestjs/common"; 14 | import { Reflector } from "@nestjs/core"; 15 | import { AuthGuard } from "@nestjs/passport"; 16 | import { Observable } from "rxjs"; 17 | import { StrategyEnum } from "../enums"; 18 | import { DecoratorEnum } from "../enums/decorator.enum"; 19 | 20 | @Injectable() 21 | export class JwtAccessGuard extends AuthGuard(StrategyEnum.JWT_ACCESS) { 22 | constructor(private readonly reflector: Reflector) { 23 | super(); 24 | } 25 | 26 | canActivate( 27 | context: ExecutionContext, 28 | ): boolean | Promise | Observable { 29 | const isPublic = this.reflector.getAllAndOverride(DecoratorEnum.IS_PUBLIC, [ 30 | context.getHandler(), 31 | context.getClass(), 32 | ]); 33 | //如果是公共路由,直接放行 不进行jwt验证 34 | if (isPublic) return true; 35 | return super.canActivate(context); 36 | } 37 | 38 | handleRequest(err, user, info) { 39 | if (err || !user) { 40 | throw err || new UnauthorizedException(`${info}`); 41 | } 42 | return user; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/guards/jwt-local.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-21 17:03:40 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:52:43 6 | * @FilePath: \cms\src\shared\guards\jwt-local.guard.ts 7 | * @Description: 8 | */ 9 | import { Injectable } from "@nestjs/common"; 10 | import { AuthGuard } from "@nestjs/passport"; 11 | import { StrategyEnum } from "../enums"; 12 | 13 | @Injectable() 14 | export class JwtLocalGuard extends AuthGuard(StrategyEnum.LOCAL) {} 15 | -------------------------------------------------------------------------------- /src/shared/guards/jwt-refresh.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-21 17:03:40 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 09:50:00 6 | * @FilePath: \cms\src\shared\guards\jwt-refresh.guard.ts 7 | * @Description: 8 | */ 9 | import { 10 | ExecutionContext, 11 | Injectable, 12 | UnauthorizedException, 13 | } from "@nestjs/common"; 14 | import { AuthGuard } from "@nestjs/passport"; 15 | import { Observable } from "rxjs"; 16 | import { StrategyEnum } from "../enums"; 17 | 18 | @Injectable() 19 | export class JwtRefreshGuard extends AuthGuard(StrategyEnum.JWT_REFRESH) { 20 | canActivate( 21 | context: ExecutionContext, 22 | ): boolean | Promise | Observable { 23 | return super.canActivate(context); 24 | } 25 | 26 | handleRequest(err, user, info) { 27 | if (err || !user) { 28 | throw err || new UnauthorizedException(`${info}`); 29 | } 30 | return user; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/guards/permission-auth.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-03 16:28:10 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:52:48 6 | * @FilePath: \cms\src\shared\guards\permission-auth.guard.ts 7 | * @Description: 8 | */ 9 | import { RolesService } from "@/modules/roles/roles.service"; 10 | import { 11 | CanActivate, 12 | ExecutionContext, 13 | ForbiddenException, 14 | Injectable, 15 | UnauthorizedException, 16 | } from "@nestjs/common"; 17 | import { Reflector } from "@nestjs/core"; 18 | import { RedisService } from "../redis/redis.service"; 19 | import { RequirePermissionOptions } from "../decorators"; 20 | import { DecoratorEnum, PermissionEnum, RedisKeyEnum } from "../enums"; 21 | import { JwtPayloadInterface } from "../interfaces"; 22 | 23 | @Injectable() 24 | export class PermissionAuthGuard implements CanActivate { 25 | constructor( 26 | private readonly reflector: Reflector, 27 | private readonly roleService: RolesService, 28 | private readonly redisService: RedisService, 29 | ) {} 30 | 31 | async canActivate(context: ExecutionContext): Promise { 32 | //1.获取当前请求的路由上的装饰器 33 | const permissionOptions = 34 | this.reflector?.getAllAndOverride( 35 | DecoratorEnum.REQUIRE_PERMISSION, 36 | [context.getHandler(), context.getClass()], 37 | ); 38 | 39 | //2.如果没有装饰器,直接返回true->表示可以访问 40 | if (!permissionOptions || !permissionOptions.permission.length) { 41 | return true; 42 | } 43 | 44 | const request = context.switchToHttp().getRequest(); 45 | 46 | const user = request.user as JwtPayloadInterface; 47 | 48 | const accessToken = await this.redisService.get( 49 | RedisKeyEnum.LoginKey + user.id, 50 | ); 51 | 52 | // 3.如果没有accessToken,直接返回false->表示不可以访问 53 | if (!accessToken) throw new UnauthorizedException("请先登录~"); 54 | 55 | // 4.如果没有用户信息,直接返回false->表示不可以访问 56 | if (!user) throw new UnauthorizedException("请先登录~"); 57 | 58 | // 5.如果是超级管理员,直接返回true->表示可以访问 59 | if (user.roleId === 1) return true; 60 | 61 | // 6.获取用户角色 62 | const role = await this.roleService.findRoleWithMenuList(user.roleId); 63 | // 7.获取用户权限 64 | const userPermissions = role.menuList 65 | .map((item) => { 66 | if (item.permission !== "null" && item.permission) { 67 | return item.permission; 68 | } 69 | }) 70 | .filter((item) => item); 71 | 72 | const { permission: requirePermissions, logical } = permissionOptions; 73 | 74 | // 8.如果用PermissionEnum.All 则直接放行 75 | if (userPermissions.includes(PermissionEnum.ALL)) return true; 76 | 77 | // 9.判断是否有权限 78 | const hasPermission = 79 | logical == "or" 80 | ? requirePermissions.some((item) => userPermissions.includes(item)) 81 | : requirePermissions.every((item) => userPermissions.includes(item)); 82 | 83 | if (!hasPermission) { 84 | throw new ForbiddenException("抱歉,没有权限~"); 85 | } 86 | 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/shared/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./transform.interceptor"; 2 | -------------------------------------------------------------------------------- /src/shared/interceptors/transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-15 18:09:41 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 22:51:58 6 | * @FilePath: \cms\src\shared\interceptors\transform.interceptor.ts 7 | * @Description:. 8 | */ 9 | import { 10 | CallHandler, 11 | ExecutionContext, 12 | Injectable, 13 | NestInterceptor, 14 | } from "@nestjs/common"; 15 | import { Observable, map } from "rxjs"; 16 | import { AppLoggerSevice } from "../logger"; 17 | 18 | @Injectable() 19 | export class TransformResultInterceptor implements NestInterceptor { 20 | constructor(private readonly logger: AppLoggerSevice) {} 21 | intercept(context: ExecutionContext, next: CallHandler): Observable { 22 | const request = context.switchToHttp().getRequest(); 23 | // 在请求进入控制器之前,对请求数据进行处理 24 | if (request.body) { 25 | this.trimStrings(request.body); 26 | } 27 | 28 | return next.handle().pipe( 29 | map((data) => { 30 | // 在请求返回之前,对返回数据进行处理 31 | const result = { 32 | data, 33 | code: 200, 34 | message: "操作成功", 35 | }; 36 | // 记录日志 37 | this.logger.setContext(context.getClass().name); 38 | this.logger.log(JSON.stringify(result)); 39 | return result; 40 | }), 41 | ); 42 | } 43 | 44 | private trimStrings(data: any): any { 45 | if (data instanceof Object) { 46 | // 递归遍历对象,去掉字符串两端的空格 47 | Object.keys(data).forEach((key) => { 48 | if (typeof data[key] === "string") { 49 | if (data[key].trim() == "") { 50 | data[key] = undefined; 51 | } 52 | } else if (data[key] instanceof Object) { 53 | this.trimStrings(data[key]); 54 | } 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/shared/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./jwt-payload.interface"; 2 | -------------------------------------------------------------------------------- /src/shared/interfaces/jwt-payload.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-12 20:44:12 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:52:55 6 | * @FilePath: \cms\src\shared\interfaces\jwt-payload.interface.ts 7 | * @Description: 8 | */ 9 | export interface JwtPayloadInterface { 10 | id: number; 11 | name: string; 12 | roleId: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/logger/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./log"; 2 | export * from "./logger.module"; 3 | export * from "./logger.service"; 4 | -------------------------------------------------------------------------------- /src/shared/logger/log.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-16 20:33:45 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-18 14:49:47 6 | * @FilePath: \cms\src\log.ts 7 | * @Description: 8 | */ 9 | import { WinstonModule, utilities } from "nest-winston"; 10 | import * as winston from "winston"; 11 | import "winston-daily-rotate-file"; 12 | import { Console } from "winston/lib/winston/transports"; 13 | import { loadEnvConfig } from "../config"; 14 | import { EnvEnum } from "../enums/env.enum"; 15 | 16 | function createDailyRotateTrasnport(level: string, filename: string) { 17 | return new winston.transports.DailyRotateFile({ 18 | level, 19 | dirname: "logs", //日志文件夹 20 | filename: `${filename}-%DATE%.log`, //日志名称,占位符 %DATE% 取值为 datePattern 值 21 | datePattern: "YYYY-MM-DD", //日志轮换的频率,此处表示每天。其他值还有:YYYY-MM、YYYY-MM-DD-HH、YYYY-MM-DD-HH-mm 22 | zippedArchive: true, //是否通过压缩的方式归档被轮换的日志文件 23 | maxSize: "20m", // 设置日志文件的最大大小,m 表示 mb 。 24 | maxFiles: "14d", // 保留日志文件的最大天数,此处表示自动删除超过 14 天的日志文件 25 | format: winston.format.combine( 26 | winston.format.timestamp({ 27 | format: "YYYY-MM-DD HH:mm:ss", 28 | }), 29 | winston.format.simple(), 30 | ), 31 | }); 32 | } 33 | 34 | export function setupLogger() { 35 | const config = loadEnvConfig(); 36 | const timestamp = config[EnvEnum.TIMESTAMP] === "true"; 37 | const conbine = []; 38 | if (timestamp) { 39 | conbine.push(winston.format.timestamp()); 40 | } 41 | conbine.push(utilities.format.nestLike()); 42 | const consoleTransports = new Console({ 43 | level: (config[EnvEnum.LOG_LEVEL] as string) || "info", 44 | format: winston.format.combine(...conbine), 45 | }); 46 | 47 | return WinstonModule.createLogger({ 48 | transports: [ 49 | consoleTransports, 50 | ...(config[EnvEnum.LOG_ON] 51 | ? [ 52 | createDailyRotateTrasnport("info", "application"), 53 | createDailyRotateTrasnport("warn", "error"), 54 | ] 55 | : []), 56 | ], 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/shared/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 09:40:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:53:00 6 | * @FilePath: \cms\src\shared\logger\logger.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | import { AppLoggerSevice } from "./logger.service"; 11 | 12 | @Module({ 13 | controllers: [], 14 | providers: [AppLoggerSevice], 15 | exports: [AppLoggerSevice], 16 | }) 17 | export class LoggerModule {} 18 | -------------------------------------------------------------------------------- /src/shared/logger/logger.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 09:40:59 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-10-22 09:31:33 6 | * @FilePath: \cms\src\shared\logger\logger.service.ts 7 | * @Description: 8 | */ 9 | import { Injectable, Logger, Scope } from "@nestjs/common"; 10 | 11 | @Injectable({ scope: Scope.TRANSIENT }) 12 | export class AppLoggerSevice { 13 | private context?: string; 14 | 15 | constructor(private readonly logger: Logger) {} 16 | 17 | public setContext(context: string) { 18 | this.context = context; 19 | } 20 | 21 | log(message: any, context?: string) { 22 | return this.logger.log(message, context || this.context); 23 | } 24 | 25 | error(message: any, trace?: string, context?: string): any { 26 | return this.logger.error(message, trace, context || this.context); 27 | } 28 | 29 | warn(message: any, context?: string): any { 30 | return this.logger.warn(message, context || this.context); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./log.middleware"; 2 | -------------------------------------------------------------------------------- /src/shared/middleware/log.middleware.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-16 10:27:12 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:53:05 6 | * @FilePath: \cms\src\shared\middleware\log.middleware.ts 7 | * @Description: 8 | */ 9 | import { Injectable, NestMiddleware } from "@nestjs/common"; 10 | import { Request, Response } from "express"; 11 | import { AppLoggerSevice } from "../logger/logger.service"; 12 | import { getReqMainInfo } from "../utils"; 13 | @Injectable() 14 | export class LogMiddleware implements NestMiddleware { 15 | constructor(private readonly logger: AppLoggerSevice) {} 16 | use(req: Request, res: Response, next: () => void) { 17 | // 记录日志 18 | this.logger.log(getReqMainInfo(req), req.url); 19 | next(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/prisma/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./prisma.module"; 2 | export * from "./prisma.service"; 3 | -------------------------------------------------------------------------------- /src/shared/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "@nestjs/common"; 2 | import { PrismaService } from "./prisma.service"; 3 | 4 | @Module({ 5 | controllers: [], 6 | providers: [PrismaService], 7 | exports: [PrismaService], 8 | }) 9 | export class PrismaModule {} 10 | -------------------------------------------------------------------------------- /src/shared/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { Prisma, PrismaClient } from "@prisma/client"; 4 | import { EnvEnum } from "../enums/env.enum"; 5 | import { AppLoggerSevice } from "../logger"; 6 | 7 | type EventType = "query" | "info" | "warn" | "error"; 8 | 9 | @Injectable() 10 | export class PrismaService 11 | extends PrismaClient 12 | implements OnModuleInit, OnModuleDestroy 13 | { 14 | constructor( 15 | private readonly configService: ConfigService, 16 | private readonly logger: AppLoggerSevice, 17 | ) { 18 | super({ 19 | datasourceUrl: configService.get(EnvEnum.DATABASE_URL), 20 | errorFormat: "pretty", 21 | log: [ 22 | { emit: "event", level: "query" }, 23 | { emit: "event", level: "info" }, 24 | { emit: "event", level: "warn" }, 25 | { emit: "event", level: "error" }, 26 | ], 27 | }); 28 | this.logger.setContext(PrismaService.name); 29 | } 30 | 31 | async onModuleInit() { 32 | await this.$connect(); 33 | this.logger.log(`prisma connect success ✅`); 34 | this.log(); 35 | } 36 | 37 | async onModuleDestroy() { 38 | await this.$disconnect(); 39 | this.logger.log(`prisma disconnect success ✅`); 40 | } 41 | 42 | private log() { 43 | this.$on("query", (e) => { 44 | this.logger.log( 45 | `sql: 📝 ${e.query} - params: 💬 ${e.params} - duration: 🚀 ${e.duration}ms`, 46 | ); 47 | }); 48 | this.$on("error", (e) => { 49 | this.logger.error( 50 | `🔖 errorMessage: ${e.message} - target: ${e.target} - timestamp: ${e.timestamp}`, 51 | ); 52 | }); 53 | this.$on("warn", (e) => { 54 | this.logger.warn( 55 | `warnMessage: ${e.message} - target: ${e.target} - timestamp: ${e.timestamp}`, 56 | ); 57 | }); 58 | this.$on("info", (e) => { 59 | this.logger.log( 60 | `infoMessage: ${e.message} - target: ${e.target} - timestamp: ${e.timestamp}`, 61 | ); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/shared/redis/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./redis.module"; 2 | export * from "./redis.service"; 3 | -------------------------------------------------------------------------------- /src/shared/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 21:26:30 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:53:16 6 | * @FilePath: \cms\src\shared\redis\redis.module.ts 7 | * @Description: 8 | */ 9 | import { Module } from "@nestjs/common"; 10 | import { RedisService } from "./redis.service"; 11 | 12 | @Module({ 13 | providers: [RedisService], 14 | exports: [RedisService], 15 | }) 16 | export class RedisModule {} 17 | -------------------------------------------------------------------------------- /src/shared/redis/redis.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 21:26:38 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:53:11 6 | * @FilePath: \cms\src\shared\redis\redis.service.ts 7 | * @Description: 8 | */ 9 | 10 | import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; 11 | import { ConfigService } from "@nestjs/config"; 12 | import Redis from "ioredis"; 13 | import { RecordTime } from "../decorators/record-time.decorator"; 14 | import { EnvEnum } from "../enums/env.enum"; 15 | import { AppLoggerSevice } from "../logger/logger.service"; 16 | 17 | @Injectable() 18 | export class RedisService 19 | extends Redis 20 | implements OnModuleInit, OnModuleDestroy 21 | { 22 | constructor( 23 | private readonly configService: ConfigService, 24 | private readonly logger: AppLoggerSevice, 25 | ) { 26 | super({ 27 | port: configService.get(EnvEnum.REDIS_PORT), 28 | host: configService.get(EnvEnum.REDIS_HOST), 29 | password: configService.get(EnvEnum.REDIS_PASSWORD), 30 | lazyConnect: true, 31 | }); 32 | 33 | this.logger.setContext(RedisService.name); 34 | } 35 | 36 | @RecordTime() 37 | async _get(key: string) { 38 | return JSON.parse(await this.get(key)); 39 | } 40 | 41 | @RecordTime() 42 | async _setex(key: string, seconds: number, value: any) {} 43 | 44 | @RecordTime() 45 | async _set(key: string, value: any) { 46 | return await this.set(key, JSON.stringify(value)); 47 | } 48 | 49 | @RecordTime() 50 | async _delKeysWithPrefix(prefix: string) { 51 | const keys = await this.keys(`${prefix}*`); 52 | if (keys.length === 0) { 53 | return 0; 54 | } 55 | return await this.del(...keys); 56 | } 57 | 58 | @RecordTime() 59 | async _delKeysContainStr(str: string) { 60 | const keys = await this.keys(`*${str}*`); 61 | if (keys.length === 0) { 62 | return 0; 63 | } 64 | return await this.del(...keys); 65 | } 66 | 67 | async onModuleInit() { 68 | await this.connect(); 69 | //删除所有key 70 | this.flushall(); 71 | this.logger.log("redis connect success ✅"); 72 | } 73 | 74 | onModuleDestroy() { 75 | this.disconnect(false); 76 | this.logger.log("redis disconnect success ✅"); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-29 09:25:52 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-13 22:01:36 6 | * @FilePath: \cms\src\shared\shared.module.ts 7 | * @Description: 8 | */ 9 | import { 10 | Global, 11 | Logger, 12 | MiddlewareConsumer, 13 | Module, 14 | RequestMethod, 15 | } from "@nestjs/common"; 16 | import { ConfigModule } from "@nestjs/config"; 17 | import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core"; 18 | import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; 19 | 20 | import { loadEnvConfig, validationSchema } from "./config"; 21 | import { AllExceptionsFilter } from "./filters"; 22 | import { JwtAccessGuard, PermissionAuthGuard } from "./guards"; 23 | import { TransformResultInterceptor } from "./interceptors"; 24 | import { LoggerModule } from "./logger"; 25 | import { LogMiddleware } from "./middleware"; 26 | import { PrismaModule } from "./prisma"; 27 | import { RedisModule } from "./redis"; 28 | import { SharedService } from "./shared.service"; 29 | import { UploadModule } from "./upload"; 30 | const envFilePath = `.env.${process.env.NODE_ENV || `development`}`; 31 | 32 | @Global() 33 | @Module({ 34 | imports: [ 35 | ConfigModule.forRoot({ 36 | envFilePath, 37 | validationSchema, 38 | isGlobal: true, 39 | load: [loadEnvConfig], 40 | }), 41 | UploadModule, 42 | RedisModule, 43 | PrismaModule, 44 | LoggerModule, 45 | 46 | ThrottlerModule.forRoot({ 47 | throttlers: [ 48 | { 49 | //每秒最多请求10次 50 | ttl: 1000, 51 | limit: 10, 52 | }, 53 | ], 54 | }), 55 | ], 56 | providers: [ 57 | SharedService, 58 | Logger, 59 | //全局异常拦截器 60 | { 61 | provide: APP_FILTER, 62 | useClass: AllExceptionsFilter, 63 | }, 64 | //全局响应结果过滤 65 | { 66 | provide: APP_INTERCEPTOR, 67 | useClass: TransformResultInterceptor, 68 | }, 69 | { 70 | provide: APP_GUARD, 71 | useClass: JwtAccessGuard, 72 | }, 73 | //全部权限守卫 74 | { 75 | provide: APP_GUARD, 76 | useClass: PermissionAuthGuard, 77 | }, 78 | { 79 | provide: APP_GUARD, 80 | useClass: ThrottlerGuard, 81 | }, 82 | ], 83 | exports: [SharedService, Logger, LoggerModule, RedisModule, PrismaModule], 84 | }) 85 | 86 | //配置请求日志中间件 87 | export class SharedModule { 88 | configure(consumer: MiddlewareConsumer) { 89 | consumer 90 | .apply(LogMiddleware) 91 | .forRoutes( 92 | { path: "*", method: RequestMethod.ALL }, 93 | { path: "/", method: RequestMethod.ALL }, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/shared/shared.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | 3 | @Injectable() 4 | export class SharedService {} 5 | -------------------------------------------------------------------------------- /src/shared/upload/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./upload.controller"; 2 | export * from "./upload.module"; 3 | export * from "./upload.service"; 4 | -------------------------------------------------------------------------------- /src/shared/upload/interfaces/file.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 14:12:54 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 18:53:22 6 | * @FilePath: \cms\src\shared\upload\interfaces\file.interface.ts 7 | * @Description: 8 | */ 9 | export interface UploadFileInterface { 10 | fieldname: string; 11 | originalname: string; 12 | encoding: string; 13 | mimetype: string; 14 | destination: string; 15 | filename: string; 16 | path: string; 17 | size: number; 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 13:32:03 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 12:10:15 6 | * @FilePath: \cms\src\shared\upload\upload.controller.ts 7 | * @Description: 8 | */ 9 | import { 10 | Controller, 11 | HttpCode, 12 | HttpStatus, 13 | MaxFileSizeValidator, 14 | ParseFilePipe, 15 | Post, 16 | UploadedFile, 17 | UploadedFiles, 18 | UseInterceptors, 19 | } from "@nestjs/common"; 20 | import { FileInterceptor, FilesInterceptor } from "@nestjs/platform-express"; 21 | import { ApiTags } from "@nestjs/swagger"; 22 | import { UploadFileInterface } from "./interfaces/file.interface"; 23 | import { UploadService } from "./upload.service"; 24 | 25 | @Controller() 26 | @ApiTags("文件上传模块") 27 | export class UploadController { 28 | constructor(private readonly uploadService: UploadService) {} 29 | 30 | @Post("upload/single") 31 | @HttpCode(HttpStatus.OK) 32 | @UseInterceptors(FileInterceptor("file")) 33 | uploadSingle( 34 | @UploadedFile( 35 | new ParseFilePipe({ 36 | validators: [ 37 | new MaxFileSizeValidator({ 38 | maxSize: 1024 * 1024 * 10, 39 | message: "文件大小不能超过10M", 40 | }), 41 | ], 42 | }), 43 | ) 44 | file: UploadFileInterface, 45 | ) { 46 | return this.uploadService.uploadSingle(file); 47 | } 48 | 49 | @Post("upload/multiple") 50 | @HttpCode(HttpStatus.OK) 51 | @UseInterceptors(FilesInterceptor("file")) 52 | uploadMultiple( 53 | @UploadedFiles( 54 | new ParseFilePipe({ 55 | validators: [ 56 | new MaxFileSizeValidator({ 57 | maxSize: 1024 * 1024 * 10, 58 | message: "文件大小不能超过10M", 59 | }), 60 | ], 61 | }), 62 | ) 63 | files: UploadFileInterface[], 64 | ) { 65 | return this.uploadService.uploadMultiple(files); 66 | } 67 | 68 | // todo 大文件上传 69 | } 70 | -------------------------------------------------------------------------------- /src/shared/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 13:32:03 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 11:34:43 6 | * @FilePath: \cms\src\shared\upload\upload.module.ts 7 | * @Description: 8 | */ 9 | import { ForbiddenException, Module } from "@nestjs/common"; 10 | import { MulterModule } from "@nestjs/platform-express"; 11 | import * as fs from "fs"; 12 | import { diskStorage } from "multer"; 13 | import * as path from "path"; 14 | import { RedisService } from "../redis/redis.service"; 15 | import { UploadController } from "./upload.controller"; 16 | import { UploadService } from "./upload.service"; 17 | 18 | /** 19 | * @description: 生成文件夹路径 20 | * @return {*} 21 | */ 22 | function generateDirPath() { 23 | const rootPath = path.resolve(__dirname, `../../../files`); 24 | if (!fs.existsSync(rootPath)) { 25 | fs.mkdirSync(rootPath); 26 | } 27 | const date = new Date(); 28 | const year = date.getFullYear(); 29 | const month = date.getMonth() + 1; 30 | const day = date.getDate(); 31 | const folder = `${year}-${month}-${day}`; 32 | const dirPath = path.resolve(__dirname, `../../../files/${folder}`); 33 | if (!fs.existsSync(dirPath)) { 34 | fs.mkdirSync(dirPath); 35 | } 36 | return dirPath; 37 | } 38 | 39 | /** 40 | * @description: 判断是否可以上传 41 | * @param {RedisService} redisService 42 | * @return {*} 43 | */ 44 | async function judgeCanUpload(redisService: RedisService) { 45 | const today = new Date().toISOString().slice(0, 10); 46 | const key = `uploads:${today}`; 47 | let count = parseInt(await redisService.get(key)); 48 | if (!count) { 49 | count = 0; 50 | } 51 | count++; 52 | if (count > 1001) return false; 53 | await redisService.set(key, count); 54 | return count <= 1000; 55 | } 56 | 57 | @Module({ 58 | imports: [ 59 | MulterModule.registerAsync({ 60 | useFactory: (redisService: RedisService) => ({ 61 | storage: diskStorage({ 62 | destination: (req, file, cb) => { 63 | cb(null, generateDirPath()); 64 | }, 65 | filename: (req, file, cb) => { 66 | cb(null, new Date().getTime() + path.extname(file.originalname)); 67 | }, 68 | }), 69 | fileFilter: async (req, file, cb) => { 70 | const canUpload = await judgeCanUpload(redisService); 71 | if (canUpload) { 72 | cb(null, true); 73 | } else { 74 | cb(new ForbiddenException("今日上传次数已达上限"), false); 75 | } 76 | }, 77 | }), 78 | inject: [RedisService], 79 | }), 80 | ], 81 | controllers: [UploadController], 82 | providers: [UploadService], 83 | }) 84 | export class UploadModule {} 85 | -------------------------------------------------------------------------------- /src/shared/upload/upload.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-11-13 13:32:03 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-13 22:25:18 6 | * @FilePath: \cms\src\shared\upload\upload.service.ts 7 | * @Description: 8 | */ 9 | import { Injectable } from "@nestjs/common"; 10 | import { ConfigService } from "@nestjs/config"; 11 | import { EnvEnum } from "../enums/env.enum"; 12 | import { AppLoggerSevice } from "../logger/logger.service"; 13 | import { UploadFileInterface } from "./interfaces/file.interface"; 14 | 15 | @Injectable() 16 | export class UploadService { 17 | constructor( 18 | private readonly configService: ConfigService, 19 | private readonly logger: AppLoggerSevice, 20 | ) { 21 | this.logger.setContext(UploadService.name); 22 | } 23 | 24 | async uploadSingle(file: UploadFileInterface) { 25 | this.logger.log(`${this.uploadSingle.name} was called`); 26 | return await this.getFileURL(file); 27 | } 28 | 29 | async uploadMultiple(files: UploadFileInterface[]) { 30 | this.logger.log(`${this.uploadMultiple.name} was called`); 31 | return await Promise.all(files.map((file) => this.getFileURL(file))); 32 | } 33 | 34 | async getFileURL(file: UploadFileInterface) { 35 | //提取出file.destination日期文件夹路径 36 | const dateFolder = file.destination.split("files")[1].slice(1); 37 | return { 38 | url: `${this.configService.get( 39 | EnvEnum.UPLOAD_ADDRESS, 40 | )}/api/v1/static/${dateFolder}/${file.filename}`, 41 | name: file.originalname, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import "dayjs/locale/zh-cn"; 3 | import timezone from "dayjs/plugin/timezone"; 4 | import utc from "dayjs/plugin/utc"; 5 | dayjs.extend(utc); 6 | dayjs.extend(timezone); 7 | 8 | export const formatTime = ( 9 | time: Date, 10 | formatString = "YYYY-MM-DD HH:mm:ss", 11 | ) => { 12 | return dayjs(time).tz("Asia/Shanghai").format(formatString); 13 | }; 14 | -------------------------------------------------------------------------------- /src/shared/utils/filer-empty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 去除对象中所有underfined、null、''的属性 3 | * @param obj 4 | * @returns 5 | */ 6 | export const filterEmpty = (obj: any) => { 7 | Object.keys(obj).forEach((key) => { 8 | if ( 9 | obj[key] && 10 | typeof obj[key] === "object" && 11 | obj[key].constructor === Object 12 | ) { 13 | filterEmpty(obj[key]); 14 | } else if (obj[key] === undefined || obj[key] === null || obj[key] === "") { 15 | delete obj[key]; 16 | } 17 | }); 18 | return obj; 19 | }; 20 | -------------------------------------------------------------------------------- /src/shared/utils/generate-tree.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 3 | * @param {any[]} data 4 | * @param {string} [id] 5 | * @param {string} [parentId] 6 | * @param {string} [children] 7 | * @return {*} {any[]} 8 | */ 9 | export const generateTree = ( 10 | data: any[], 11 | id?: string, 12 | parentId?: string, 13 | children?: string, 14 | ) => { 15 | const config = { 16 | id: id || "id", 17 | parentId: parentId || "parentId", 18 | childrenList: children || "children", 19 | }; 20 | 21 | const childrenListMap = {}; 22 | const nodeIds = {}; 23 | const tree = []; 24 | 25 | for (const d of data) { 26 | const parentId = d[config.parentId]; 27 | if (childrenListMap[parentId] == null) { 28 | childrenListMap[parentId] = []; 29 | } 30 | nodeIds[d[config.id]] = d; 31 | childrenListMap[parentId].push(d); 32 | } 33 | 34 | for (const d of data) { 35 | const parentId = d[config.parentId]; 36 | if (nodeIds[parentId] == null) { 37 | tree.push(d); 38 | } 39 | } 40 | 41 | for (const t of tree) { 42 | adaptToChildrenList(t); 43 | } 44 | 45 | function adaptToChildrenList(o) { 46 | if (childrenListMap[o[config.id]] !== null) { 47 | o[config.childrenList] = childrenListMap[o[config.id]]; 48 | } 49 | if (o[config.childrenList]) { 50 | for (const c of o[config.childrenList]) { 51 | adaptToChildrenList(c); 52 | } 53 | } 54 | } 55 | return tree; 56 | }; 57 | -------------------------------------------------------------------------------- /src/shared/utils/handle-error.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, HttpException } from "@nestjs/common"; 2 | import { Prisma } from "@prisma/client"; 3 | import { PrismaErrorCode } from "../enums"; 4 | import { AppLoggerSevice } from "../logger"; 5 | 6 | interface ErrorMessage { 7 | common?: string; 8 | unique?: string; 9 | foreign?: string; 10 | } 11 | 12 | export const handleError = ( 13 | logger: AppLoggerSevice, 14 | error: any, 15 | messages: ErrorMessage, 16 | ) => { 17 | logger.error(error); 18 | if ( 19 | error instanceof Prisma.PrismaClientKnownRequestError && 20 | error.code == PrismaErrorCode.UniqueConstraintViolation && 21 | messages.unique 22 | ) { 23 | throw new BadRequestException(`${messages.unique}`); 24 | } else if ( 25 | error instanceof Prisma.PrismaClientKnownRequestError && 26 | error.code == PrismaErrorCode.ForeignKeyConstraintViolation && 27 | messages.foreign 28 | ) { 29 | throw new BadRequestException(`${messages.foreign}`); 30 | } else if (error.message && error instanceof HttpException) { 31 | throw new BadRequestException(error.message); 32 | } 33 | throw new BadRequestException(`${messages.common}`); 34 | }; 35 | -------------------------------------------------------------------------------- /src/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./date"; 2 | export * from "./filer-empty"; 3 | export * from "./generate-tree"; 4 | export * from "./random"; 5 | export * from "./request-info"; 6 | export * from "./handle-error"; 7 | -------------------------------------------------------------------------------- /src/shared/utils/random.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | 3 | export const getRandomId = () => { 4 | return uuid().replace(/-/g, ""); 5 | }; 6 | -------------------------------------------------------------------------------- /src/shared/utils/request-info.ts: -------------------------------------------------------------------------------- 1 | import { Request } from "express"; 2 | export const getReqMainInfo = (req: Request) => { 3 | const { query, headers, url, method, body } = req; 4 | const ip = getClientIp(req); 5 | return JSON.stringify({ 6 | url, 7 | host: headers.host, 8 | ip, 9 | method, 10 | query, 11 | body, 12 | }); 13 | }; 14 | 15 | export function getClientIp(req: Request) { 16 | const realIP = req.headers["x-real-ip"]; 17 | const ip = realIP 18 | ? Array.isArray(realIP) 19 | ? realIP[0] 20 | : realIP 21 | : req.ip.slice(7); 22 | return ip; 23 | } 24 | -------------------------------------------------------------------------------- /src/swagger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Leo l024983409@qq.com 3 | * @Date: 2023-10-17 12:53:01 4 | * @LastEditors: Leo l024983409@qq.com 5 | * @LastEditTime: 2023-11-14 12:31:25 6 | * @FilePath: \cms\src\swagger.ts 7 | * @Description: 8 | */ 9 | import { INestApplication } from "@nestjs/common"; 10 | import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; 11 | 12 | export function setupSwagger(app: INestApplication) { 13 | const options = new DocumentBuilder() 14 | .addBearerAuth() 15 | .setTitle("CMS_BACKEND") 16 | .setDescription("The CMS_BACKEND API description") 17 | .setVersion("1.0") 18 | .addServer("/api/v1") 19 | .build(); 20 | const document = SwaggerModule.createDocument(app, options); 21 | SwaggerModule.setup("swagger", app, document, { 22 | jsonDocumentUrl: "/api/v1/swagger-json", 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from "@nestjs/common"; 2 | import { Test, TestingModule } from "@nestjs/testing"; 3 | import * as request from "supertest"; 4 | import { AppModule } from "./../src/app.module"; 5 | 6 | describe("AppController (e2e)", () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it("/ (GET)", () => { 19 | return request(app.getHttpServer()) 20 | .get("/") 21 | .expect(200) 22 | .expect("Hello World!"); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "ES2021", 11 | "sourceMap": true, 12 | "outDir": "./dist", 13 | "baseUrl": "./", 14 | "incremental": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | }, 24 | "types": ["./src/@types/index.d.ts", "node"] 25 | } 26 | } 27 | --------------------------------------------------------------------------------