├── .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 |
--------------------------------------------------------------------------------