diff --git a/DETAILS.md b/DETAILS.md new file mode 100644 index 0000000..2926680 --- /dev/null +++ b/DETAILS.md @@ -0,0 +1,725 @@ + + ๐Ÿ“‹ ้กน็›ฎๆฆ‚่งˆ + + ่ฟ™ๆ˜ฏไธ€ไธชไผไธš็บง Spring Boot 3.x ๆจกๆฟ้กน็›ฎ๏ผŒๆไพ›ไบ†ๅฎŒๆ•ด็š„็”จๆˆท่ฎค่ฏไธŽๆƒ้™็ฎก็†็ณป็ปŸใ€‚ + + ๆ ธๅฟƒๅŠŸ่ƒฝ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๆ ธๅฟƒๅŠŸ่ƒฝๆจกๅ— โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ 1. ็”จๆˆท่ฎค่ฏ JWT + Refresh Token ๅŒTokenๆœบๅˆถ โ”‚ + โ”‚ 2. ๆƒ้™็ฎก็† RBAC๏ผˆ่ง’่‰ฒ-ๆƒ้™๏ผ‰ๆจกๅž‹ โ”‚ + โ”‚ 3. ่ดฆๆˆทๅฎ‰ๅ…จ ็™ปๅฝ•ๅคฑ่ดฅ้”ๅฎšใ€ๅฏ†็ ้‡็ฝฎใ€้™ๆต โ”‚ + โ”‚ 4. ๅฎก่ฎกๆ—ฅๅฟ— ่ฎฐๅฝ•ๆ‰€ๆœ‰ๆ•ๆ„Ÿๆ“ไฝœ โ”‚ + โ”‚ 5. ็ผ“ๅญ˜ๆ”ฏๆŒ Redis ็ผ“ๅญ˜ใ€ๅˆ†ๅธƒๅผ้” โ”‚ + โ”‚ 6. ๆถˆๆฏ้˜Ÿๅˆ— RocketMQ ๅผ‚ๆญฅๅค„็† โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + ๆŠ€ๆœฏๆ ˆ + + Searched for 1 pattern, read 1 file (ctrl+o to expand) + +โ— ็Žฐๅœจ่ฎฉๆˆ‘ไธบไฝ ่ฏฆ็ป†ๅ‰–ๆžๆ•ดไธช้กน็›ฎ๏ผš + + --- + ไธ€ใ€้กน็›ฎๆŠ€ๆœฏๆ ˆ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๆŠ€ๆœฏ โ”‚ ็‰ˆๆœฌ โ”‚ ็”จ้€” โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ Spring Boot โ”‚ 3.5.6 โ”‚ ๅŸบ็ก€ๆก†ๆžถ โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ Spring Security โ”‚ 6.x โ”‚ ่ฎค่ฏๆŽˆๆƒ โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ Spring Data JPA โ”‚ - โ”‚ ORM ๆ•ฐๆฎ่ฎฟ้—ฎ โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ MySQL โ”‚ - โ”‚ ๅ…ณ็ณปๆ•ฐๆฎๅบ“ โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ Redis โ”‚ - โ”‚ ็ผ“ๅญ˜/ๅˆ†ๅธƒๅผ้” โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ RocketMQ โ”‚ 2.3.1 โ”‚ ๆถˆๆฏ้˜Ÿๅˆ— โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ JJWT โ”‚ 0.11.5 โ”‚ JWT Token โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ Flyway โ”‚ - โ”‚ ๆ•ฐๆฎๅบ“็‰ˆๆœฌ็ฎก็† โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ Lombok โ”‚ - โ”‚ ็ฎ€ๅŒ–ไปฃ็  โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + --- + ไบŒใ€้กน็›ฎ็›ฎๅฝ•็ป“ๆž„ + +โ— Bash(tree -L 4 -I 'target|.git' D:\tmp\springboot-template\src\main\java\com\aisi\template 2>/dev/null || find + D:\tmp\springboot-template\src\main\java\com\aisi\tempโ€ฆ) + โŽฟ ย find: โ€˜D:tmpspringboot-templatesrcmainjavacomaisitemplateโ€™: No such file or directory + sed: -e expression #1, char 7: unterminated `s' command + + Searched for 1 pattern (ctrl+o to expand) + +โ— ้กน็›ฎๅˆ†ๅฑ‚ๆžถๆž„ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ annotation/ # ่‡ชๅฎšไน‰ๆณจ่งฃ๏ผˆAOPๅˆ‡็‚น๏ผ‰ + โ”‚ โ”œโ”€โ”€ @AuditLog # ๅฎก่ฎกๆ—ฅๅฟ—ๆณจ่งฃ + โ”‚ โ”œโ”€โ”€ @RateLimit # ้™ๆตๆณจ่งฃ + โ”‚ โ””โ”€โ”€ @StrongPassword # ๅผบๅฏ†็ ๆ ก้ชŒๆณจ่งฃ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ aspect/ # AOPๅˆ‡้ข + โ”‚ โ”œโ”€โ”€ AuditLogAspect # ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ‡้ข + โ”‚ โ””โ”€โ”€ RateLimitAspect # ้™ๆตๅˆ‡้ข + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ config/ # ้…็ฝฎ็ฑป + โ”‚ โ”œโ”€โ”€ SecurityConfig # Spring Security ๅฎ‰ๅ…จ้…็ฝฎ + โ”‚ โ”œโ”€โ”€ RedisConfig # Redis ้…็ฝฎ + โ”‚ โ”œโ”€โ”€ JpaConfig # JPA ๅฎก่ฎก้…็ฝฎ + โ”‚ โ”œโ”€โ”€ WebConfig # Web MVC ้…็ฝฎ + โ”‚ โ”œโ”€โ”€ JacksonConfig # JSON ๅบๅˆ—ๅŒ–้…็ฝฎ + โ”‚ โ””โ”€โ”€ OpenApiConfig # Swagger ๆ–‡ๆกฃ้…็ฝฎ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ constants/ # ๅธธ้‡ๅฎšไน‰ + โ”‚ โ”œโ”€โ”€ AppConstants # ๅบ”็”จๅธธ้‡ + โ”‚ โ””โ”€โ”€ SecurityConstants # ๅฎ‰ๅ…จๅธธ้‡ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ controller/ # ๆŽงๅˆถๅฑ‚๏ผˆAPIๆŽฅๅฃ๏ผ‰ + โ”‚ โ”œโ”€โ”€ UserController # ็”จๆˆทๆŽฅๅฃ + โ”‚ โ”œโ”€โ”€ RoleController # ่ง’่‰ฒๆŽฅๅฃ + โ”‚ โ”œโ”€โ”€ MenuController # ่œๅ•ๆŽฅๅฃ + โ”‚ โ””โ”€โ”€ PermissionController # ๆƒ้™ๆŽฅๅฃ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ domain/ # ้ข†ๅŸŸๆจกๅž‹ + โ”‚ โ”œโ”€โ”€ entity/ # ๅฎžไฝ“็ฑป๏ผˆๆ•ฐๆฎๅบ“่กจ๏ผ‰ + โ”‚ โ”œโ”€โ”€ dto/ # ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก๏ผˆ่ฏทๆฑ‚ๅ‚ๆ•ฐ๏ผ‰ + โ”‚ โ”œโ”€โ”€ vo/ # ่ง†ๅ›พๅฏน่ฑก๏ผˆๅ“ๅบ”ๆ•ฐๆฎ๏ผ‰ + โ”‚ โ””โ”€โ”€ enums/ # ๆžšไธพ็ฑป + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ service/ # ๆœๅŠกๅฑ‚๏ผˆไธšๅŠก้€ป่พ‘๏ผ‰ + โ”‚ โ””โ”€โ”€ impl/ # ๆœๅŠกๅฎž็Žฐ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ repository/ # ๆ•ฐๆฎ่ฎฟ้—ฎๅฑ‚๏ผˆDAO๏ผ‰ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ filter/ # ่ฟ‡ๆปคๅ™จ + โ”‚ โ””โ”€โ”€ JwtAuthenticationFilter # JWT ่ฎค่ฏ่ฟ‡ๆปคๅ™จ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ handler/ # ๅค„็†ๅ™จ + โ”‚ โ””โ”€โ”€ GlobalExceptionHandler # ๅ…จๅฑ€ๅผ‚ๅธธๅค„็† + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ interceptor/ # ๆ‹ฆๆˆชๅ™จ + โ”‚ โ””โ”€โ”€ LoggingInterceptor # ๆ—ฅๅฟ—ๆ‹ฆๆˆชๅ™จ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ mq/ # ๆถˆๆฏ้˜Ÿๅˆ— + โ”‚ โ”œโ”€โ”€ producer/ # ๆถˆๆฏ็”Ÿไบง่€… + โ”‚ โ”œโ”€โ”€ consumer/ # ๆถˆๆฏๆถˆ่ดน่€… + โ”‚ โ””โ”€โ”€ message/ # ๆถˆๆฏๅฎšไน‰ + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ utils/ # ๅทฅๅ…ท็ฑป + โ”‚ โ”œโ”€โ”€ JwtUtil # JWT ๅทฅๅ…ท + โ”‚ โ”œโ”€โ”€ SecurityUtils # ๅฎ‰ๅ…จๅทฅๅ…ท + โ”‚ โ”œโ”€โ”€ RedisCache # Redis ็ผ“ๅญ˜ + โ”‚ โ””โ”€โ”€ RedisLock # ๅˆ†ๅธƒๅผ้” + โ”‚ + โ”œโ”€โ”€ ๐Ÿ“ examples/ # ไฝฟ็”จ็คบไพ‹ + โ”‚ โ”œโ”€โ”€ RedisUsageExample # Redis ไฝฟ็”จ็คบไพ‹ + โ”‚ โ””โ”€โ”€ TransactionExample # ไบ‹ๅŠกไฝฟ็”จ็คบไพ‹ + โ”‚ + โ””โ”€โ”€ ๐Ÿ“ exception/ # ๅผ‚ๅธธ็ฑป + โ”œโ”€โ”€ BusinessException # ไธšๅŠกๅผ‚ๅธธ + โ””โ”€โ”€ RateLimitExceededException # ้™ๆตๅผ‚ๅธธ + + --- + ไธ‰ใ€ๆ ธๅฟƒๆžถๆž„่ฎพ่ฎก + + 3.1 ่ฎค่ฏๆžถๆž„ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ่ฎค่ฏๆต็จ‹ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + ๅฎขๆˆท็ซฏ Spring Boot JWT Token + โ”‚ โ”‚ โ”‚ + โ”‚ 1. POST /api/v1/user/login โ”‚ โ”‚ + โ”‚ {username, password} โ”‚ โ”‚ + โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ 2. ้ชŒ่ฏ็”จๆˆทๅๅฏ†็  โ”‚ + โ”‚ โ”‚ 3. ๆฃ€ๆŸฅ่ดฆๆˆท้”ๅฎš็Šถๆ€ โ”‚ + โ”‚ โ”‚ 4. ็”Ÿๆˆ Access Token(1h) โ”‚ + โ”‚ โ”‚ 5. ็”Ÿๆˆ Refresh Token(7d) โ”‚ + โ”‚ โ”‚ 6. ๅญ˜ๅ‚จ Refresh Token โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ 7. ่ฟ”ๅ›ž Token โ”‚ โ”‚ + โ”‚ {accessToken, โ”‚ โ”‚ + โ”‚ refreshToken} โ”‚ โ”‚ + โ”‚ <โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ 8. ๅŽ็ปญ่ฏทๆฑ‚ๆบๅธฆ Token โ”‚ โ”‚ + โ”‚ Authorization: Bearer โ”‚ โ”‚ + โ”‚ {accessToken} โ”‚ โ”‚ + โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ 9. JwtAuthenticationFilter โ”‚ + โ”‚ โ”‚ ่งฃๆž Token โ”‚ + โ”‚ โ”‚ ้ชŒ่ฏ็ญพๅ โ”‚ + โ”‚ โ”‚ ๆฃ€ๆŸฅ้ป‘ๅๅ• โ”‚ + โ”‚ โ”‚ ่ฎพ็ฝฎ่ฎค่ฏไธŠไธ‹ๆ–‡ โ”‚ + โ”‚ โ”‚ โ”‚ + โ”‚ 10. ่ฟ”ๅ›žไธšๅŠกๆ•ฐๆฎ โ”‚ โ”‚ + โ”‚ <โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ + + 3.2 ๆƒ้™ๆจกๅž‹๏ผˆRBAC๏ผ‰ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ RBAC ๆƒ้™ๆจกๅž‹ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + ็”จๆˆท(User) โ”€โ”€ๅคšๅฏนๅคšโ”€โ”€ ่ง’่‰ฒ(Role) โ”€โ”€ๅคšๅฏนๅคšโ”€โ”€ ๆƒ้™(Permission) + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”‚ + having having having + accounts roles permissions + + ็คบไพ‹๏ผš + ็”จๆˆท alice โ”€โ”€ๆ‹ฅๆœ‰โ”€โ”€> [ROLE_USER, ROLE_ADMIN] + โ”‚ + โ””โ”€โ”€ ROLE_ADMIN โ”€โ”€ๆ‹ฅๆœ‰โ”€โ”€> [user:read, user:write, + role:read, menu:read] + + ๆƒ้™่ฎก็ฎ—๏ผš + alice ็š„ๆƒ้™ = ROLE_USER็š„ๆƒ้™ โˆช ROLE_ADMIN็š„ๆƒ้™ + + ไฝฟ็”จ @PreAuthorize ๆณจ่งฃๆŽงๅˆถ่ฎฟ้—ฎ๏ผš + @PreAuthorize("hasAuthority('user:write')") + public void updateUser() { ... } + + 3.3 ๅฎ‰ๅ…จ้˜ฒๆŠคๆœบๅˆถ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๅฎ‰ๅ…จ้˜ฒๆŠคไฝ“็ณป โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + ้˜ฒๆŠค็ฑปๅž‹ ๅฎž็Žฐๆ–นๅผ ้˜ˆๅ€ผ + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + ็™ปๅฝ•ๅคฑ่ดฅ้”ๅฎš Redis่ฎกๆ•ฐๅ™จ + ๆ—ถ้—ด็ช—ๅฃ 5ๆฌก/30ๅˆ†้’Ÿ + ๅฏ†็ ้‡็ฝฎ้™ๆต Redis่ฎกๆ•ฐๅ™จ + ๅ†ทๅดๆ—ถ้—ด 1ๆฌก/60็ง’ + APIๆŽฅๅฃ้™ๆต Redis + @RateLimitๅˆ‡้ข ๅฏ้…็ฝฎ + ๅฏ†็ ๅผบๅบฆๆ ก้ชŒ @StrongPasswordๆณจ่งฃ ๅคงๅฐๅ†™+ๆ•ฐๅญ—+็‰นๆฎŠๅญ—็ฌฆ + Token้ป‘ๅๅ• Redis + JTI ็™ปๅ‡บๆ—ถๅŠ ๅ…ฅ + Token่ฝฎๆข Refresh Tokenๆœบๅˆถ ๅˆทๆ–ฐๅŽๆ—งTokenๅคฑๆ•ˆ + ๅฏ†็ ้‡็ฝฎ ้‚ฎ็ฎฑ้ชŒ่ฏ็  6ไฝ/10ๅˆ†้’Ÿๆœ‰ๆ•ˆ + + --- + ๅ››ใ€ๆ•ฐๆฎๅบ“่ฎพ่ฎก + + Searched for 1 pattern, read 2 files (ctrl+o to expand) + +โ— ๆ•ฐๆฎๅบ“่กจ็ป“ๆž„ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๆ•ฐๆฎๅบ“่กจๅ…ณ็ณปๅ›พ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + ๆ ธๅฟƒ่กจๅ…ณ็ณป๏ผš + + users (็”จๆˆท่กจ) + โ”‚ + โ”œโ”€โ”€ 1:N โ”€โ”€> refresh_tokens (ๅˆทๆ–ฐไปค็‰Œ่กจ) + โ”‚ + โ”œโ”€โ”€ N:M โ”€โ”€> sys_role (่ง’่‰ฒ่กจ) โ”€โ”€ N:M โ”€โ”€> sys_permission (ๆƒ้™่กจ) + โ”‚ โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€ N:M โ”€โ”€> sys_menu (่œๅ•่กจ) โ”‚ + โ”‚ โ”‚ + โ””โ”€โ”€ 1:N โ”€โ”€> password_reset_codes (ๅฏ†็ ้‡็ฝฎ็ ่กจ) + + ่พ…ๅŠฉ่กจ๏ผš + โ”œโ”€โ”€ sys_audit_log (ๅฎก่ฎกๆ—ฅๅฟ—่กจ) + โ””โ”€โ”€ sys_user_role (็”จๆˆท-่ง’่‰ฒๅ…ณ่”่กจ) + sys_role_permission (่ง’่‰ฒ-ๆƒ้™ๅ…ณ่”่กจ) + sys_role_menu (่ง’่‰ฒ-่œๅ•ๅ…ณ่”่กจ) + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ่กจๅ โ”‚ ็”จ้€” โ”‚ ๅ…ณ้”ฎๅญ—ๆฎต โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ users โ”‚ ็”จๆˆทไฟกๆฏ โ”‚ username, password, email, status, failed_login_count, locked_until โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ sys_role โ”‚ ่ง’่‰ฒๅฎšไน‰ โ”‚ role_code(ROLE_ADMIN็ญ‰), role_name, status โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ sys_permission โ”‚ ๆƒ้™ๅฎšไน‰ โ”‚ permission_code(user:read็ญ‰), resource, action โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ sys_menu โ”‚ ่œๅ•ๅฎšไน‰ โ”‚ menu_name, menu_type(็›ฎๅฝ•/้กต้ข/ๆŒ‰้’ฎ), parent_id โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ refresh_tokens โ”‚ ๅˆทๆ–ฐไปค็‰Œ โ”‚ token_hash(SHA-256), expires_at, revoked โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ sys_audit_log โ”‚ ๅฎก่ฎกๆ—ฅๅฟ— โ”‚ user_id, action(LOGIN/CREATE็ญ‰), resource, status โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + --- + ไบ”ใ€ๆ ธๅฟƒๆจกๅ—่ฏฆ่งฃ + + 5.1 ่ฎค่ฏๆจกๅ— + + ๆ ธๅฟƒ็ฑปๅ…ณ็ณป๏ผš + SecurityConfig (้…็ฝฎๅ…ฅๅฃ) + โ”‚ + โ”œโ”€โ”€ CustomUserDetailsService (ๅŠ ่ฝฝ็”จๆˆท) + โ”‚ โ””โ”€โ”€ UserRepository (ๆŸฅ่ฏข็”จๆˆท) + โ”‚ + โ”œโ”€โ”€ JwtAuthenticationFilter (JWT่ฟ‡ๆปคๅ™จ) + โ”‚ โ”œโ”€โ”€ JwtUtil (Token็”Ÿๆˆ/่งฃๆž) + โ”‚ โ””โ”€โ”€ TokenService (Token็ฎก็†) + โ”‚ โ””โ”€โ”€ RefreshTokenRepository + โ”‚ + โ””โ”€โ”€ PasswordEncoder (BCryptๅŠ ๅฏ†) + + ่ฎค่ฏๆต็จ‹่ฏฆ่งฃ๏ผš + + Read 1 file (ctrl+o to expand) + +โ— JWT่ฎค่ฏ่ฟ‡ๆปคๅ™จ่ฏฆ่งฃ๏ผš + + Read 1 file (ctrl+o to expand) + +โ— ่ฎค่ฏๆต็จ‹ๅ›พ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๅฎŒๆ•ด่ฎค่ฏๆต็จ‹ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + 1. ็”จๆˆท็™ปๅฝ• + โ”‚ + โ”œโ”€> UserController.login() + โ”‚ โ”‚ + โ”‚ โ”œโ”€> UserService.login() + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€> 1. ๆ ก้ชŒ็”จๆˆทๅๅฏ†็  + โ”‚ โ”‚ โ”œโ”€> 2. ๆฃ€ๆŸฅ่ดฆๆˆท้”ๅฎš็Šถๆ€ + โ”‚ โ”‚ โ”œโ”€> 3. ๆฃ€ๆŸฅ่ดฆๆˆทๅฏ็”จ็Šถๆ€ + โ”‚ โ”‚ โ”œโ”€> 4. ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅ(ๆˆๅŠŸๅˆ™ๆธ…้™ค) + โ”‚ โ”‚ โ”œโ”€> 5. ็”Ÿๆˆ Access Token (1ๅฐๆ—ถ) + โ”‚ โ”‚ โ”œโ”€> 6. ็”Ÿๆˆ Refresh Token (7ๅคฉ) + โ”‚ โ”‚ โ”œโ”€> 7. ๅญ˜ๅ‚จ Refresh Token ๅˆฐๆ•ฐๆฎๅบ“ + โ”‚ โ”‚ โ”œโ”€> 8. ๅ‘้€็™ปๅฝ•ๆถˆๆฏๅˆฐ MQ + โ”‚ โ”‚ โ””โ”€> 9. ่ฟ”ๅ›ž LoginResponseVo + โ”‚ โ”‚ + โ”‚ โ””โ”€> ่ฟ”ๅ›ž { accessToken, refreshToken, expiresIn, userInfo } + + 2. ๅŽ็ปญ่ฏทๆฑ‚ + โ”‚ + โ”œโ”€> JwtAuthenticationFilter.doFilterInternal() + โ”‚ โ”‚ + โ”‚ โ”œโ”€> 1. ๆๅ– Token (Authorization: Bearer {token}) + โ”‚ โ”œโ”€> 2. ้ชŒ่ฏ Token ็ญพๅ + โ”‚ โ”œโ”€> 3. ๆฃ€ๆŸฅ้ป‘ๅๅ• (็™ปๅ‡บๆ—ถๅŠ ๅ…ฅ) + โ”‚ โ”œโ”€> 4. ๆฃ€ๆŸฅ่ฟ‡ๆœŸๆ—ถ้—ด + โ”‚ โ”œโ”€> 5. ๅŠ ่ฝฝ็”จๆˆท่ฏฆๆƒ… + โ”‚ โ””โ”€> 6. ่ฎพ็ฝฎ่ฎค่ฏๅˆฐ SecurityContext + โ”‚ + โ””โ”€> ็ปง็ปญๅค„็†่ฏทๆฑ‚ + + 3. Token ๅˆทๆ–ฐ + โ”‚ + โ”œโ”€> UserController.refreshToken() + โ”‚ โ”‚ + โ”‚ โ””โ”€> TokenService.refreshToken() + โ”‚ โ”‚ + โ”‚ โ”œโ”€> 1. ้ชŒ่ฏ Refresh Token + โ”‚ โ”œโ”€> 2. ๆ’ค้”€ๆ—ง Refresh Token + โ”‚ โ”œโ”€> 3. ็”Ÿๆˆๆ–ฐ็š„ Access Token + โ”‚ โ”œโ”€> 4. ็”Ÿๆˆๆ–ฐ็š„ Refresh Token + โ”‚ โ””โ”€> 5. ่ฟ”ๅ›žๆ–ฐ Token + + 4. ็”จๆˆท็™ปๅ‡บ + โ”‚ + โ”œโ”€> UserController.logout() + โ”‚ โ”‚ + โ”‚ โ””โ”€> TokenService.addTokenToBlacklist() + โ”‚ โ”‚ + โ”‚ โ”œโ”€> 1. ๆๅ– Token ็š„ JTI + โ”‚ โ”œโ”€> 2. ่ฎก็ฎ—ๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ด + โ”‚ โ”œโ”€> 3. ๅญ˜ๅ…ฅ Redis ้ป‘ๅๅ• + โ”‚ โ””โ”€> 4. ๆ’ค้”€ๆ‰€ๆœ‰ Refresh Token + + 5.2 ๆƒ้™ๆจกๅ—่ฏฆ่งฃ + + Read 1 file (ctrl+o to expand) + +โ— ๆƒ้™ๅŠ ่ฝฝๆต็จ‹ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๆƒ้™ๅŠ ่ฝฝไธŽ้ชŒ่ฏๆต็จ‹ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + 1. ็”จๆˆท็™ปๅฝ•ๆ—ถๅŠ ่ฝฝๆƒ้™ + โ”‚ + โ”œโ”€> CustomUserDetailsService.loadUserByUsername(username) + โ”‚ โ”‚ + โ”‚ โ”œโ”€> ๆŸฅ่ฏข็”จๆˆท: User + Roles (JOIN FETCH) + โ”‚ โ”‚ + โ”‚ โ”œโ”€> ้ๅކ็”จๆˆท็š„ๆฏไธช่ง’่‰ฒ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€> ่ง’่‰ฒ ROLE_ADMIN + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€> ๆทปๅŠ ่ง’่‰ฒๆƒ้™: "ROLE_ADMIN" + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€> ๆทปๅŠ ่ง’่‰ฒไธ‹็š„ๅ…ทไฝ“ๆƒ้™ + โ”‚ โ”‚ โ”œโ”€โ”€ "user:create" + โ”‚ โ”‚ โ”œโ”€โ”€ "user:read" + โ”‚ โ”‚ โ”œโ”€โ”€ "user:update" + โ”‚ โ”‚ โ”œโ”€โ”€ "role:read" + โ”‚ โ”‚ โ””โ”€โ”€ ... + โ”‚ โ”‚ + โ”‚ โ””โ”€> ๅˆๅนถๆ‰€ๆœ‰่ง’่‰ฒ็š„ๆƒ้™ โ†’ Set + + 2. ๆŽฅๅฃๆƒ้™้ชŒ่ฏ + โ”‚ + โ”œโ”€> @PreAuthorize("hasAuthority('user:create')") + โ”‚ โ”‚ + โ”‚ โ””โ”€> Spring Security ๆฃ€ๆŸฅๅฝ“ๅ‰็”จๆˆท็š„ๆƒ้™ๅˆ—่กจ + โ”‚ โ”‚ + โ”‚ โ””โ”€> ๅŒ…ๅซ "user:create" โ†’ โœ… ๅ…่ฎธ่ฎฟ้—ฎ + โ”‚ ไธๅŒ…ๅซ โ†’ โŒ ๆ‹’็ป่ฎฟ้—ฎ + + 3. ๆƒ้™็ฑปๅž‹่ฏดๆ˜Ž + โ”‚ + โ”œโ”€โ”€ ่ง’่‰ฒๆƒ้™ (ROLE_XXX) + โ”‚ โ”œโ”€โ”€ ็”จไบŽ็ฒ—็ฒ’ๅบฆๆŽงๅˆถ๏ผšๅˆคๆ–ญ็”จๆˆท่ง’่‰ฒ + โ”‚ โ””โ”€โ”€ ไฝฟ็”จๆ–นๅผ: @PreAuthorize("hasRole('ADMIN')") + โ”‚ ็ญ‰ไปทไบŽ: hasAuthority('ROLE_ADMIN') + โ”‚ + โ””โ”€โ”€ ๅ…ทไฝ“ๆƒ้™ (XXX:YYY) + โ”œโ”€โ”€ ็”จไบŽ็ป†็ฒ’ๅบฆๆŽงๅˆถ๏ผšๅ…ทไฝ“ๆ“ไฝœ + โ””โ”€โ”€ ไฝฟ็”จๆ–นๅผ: @PreAuthorize("hasAuthority('user:create')") + + 5.3 ๅฎ‰ๅ…จ้˜ฒๆŠคๆจกๅ— + + Read 1 file (ctrl+o to expand) + +โ— ๅฎ‰ๅ…จ้˜ฒๆŠคๆœบๅˆถ่ฏฆ่งฃ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๅฎ‰ๅ…จ้˜ฒๆŠคๆœบๅˆถ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + 1. ็™ปๅฝ•ๅคฑ่ดฅ้”ๅฎšๆœบๅˆถ + โ”‚ + โ”œโ”€> ่งฆๅ‘ๆกไปถ๏ผš่ฟž็ปญ็™ปๅฝ•ๅคฑ่ดฅ 5 ๆฌก๏ผˆๅฏ้…็ฝฎ๏ผ‰ + โ”‚ + โ”œโ”€> ้”ๅฎšๆ—ถ้•ฟ๏ผš30 ๅˆ†้’Ÿ๏ผˆๅฏ้…็ฝฎ๏ผ‰ + โ”‚ + โ”œโ”€> ๅฎž็Žฐๆ–นๅผ๏ผš + โ”‚ โ”œโ”€โ”€ users ่กจๅญ˜ๅ‚จๅคฑ่ดฅๆฌกๆ•ฐๅ’Œ้”ๅฎšๆ—ถ้—ด + โ”‚ โ”œโ”€โ”€ failed_login_count๏ผšๅคฑ่ดฅๆฌกๆ•ฐ + โ”‚ โ””โ”€โ”€ locked_until๏ผš้”ๅฎšๅˆฐๆœŸๆ—ถ้—ด + โ”‚ + โ””โ”€> ่‡ชๅŠจ่งฃ้”๏ผš้”ๅฎšๆ—ถ้—ดๅˆฐๆœŸๅŽ่‡ชๅŠจ่งฃ้” + + 2. ้™ๆตๆœบๅˆถ (@RateLimit) + โ”‚ + โ”œโ”€> ๆ”ฏๆŒ็š„้™ๆต็ฑปๅž‹๏ผš + โ”‚ โ”œโ”€โ”€ IP๏ผšๆŒ‰ IP ๅœฐๅ€้™ๆต + โ”‚ โ”œโ”€โ”€ USER๏ผšๆŒ‰็”จๆˆท ID ้™ๆต + โ”‚ โ””โ”€โ”€ GLOBAL๏ผšๅ…จๅฑ€้™ๆต + โ”‚ + โ”œโ”€> ไฝฟ็”จๆ–นๅผ๏ผš + โ”‚ @RateLimit(permitted = 10, seconds = 60, limitType = LimitType.USER) + โ”‚ public void sensitiveOperation() { ... } + โ”‚ + โ””โ”€> ๅฎž็ŽฐๅŽŸ็†๏ผš + โ”œโ”€โ”€ Redis ๅญ˜ๅ‚จ่ฎกๆ•ฐๅ™จ + โ”œโ”€โ”€ ๆ ผๅผ๏ผšrate_limit:user:{userId} + โ”œโ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผšๆ—ถ้—ด็ช—ๅฃ + โ””โ”€โ”€ ่ถ…่ฟ‡้™ๅˆถ๏ผšๆŠ›ๅ‡บ RateLimitExceededException + + 3. ๅฏ†็ ้‡็ฝฎๆœบๅˆถ + โ”‚ + โ”œโ”€> ้ชŒ่ฏ็ ่ง„ๅˆ™๏ผš + โ”‚ โ”œโ”€โ”€ 6 ไฝ้šๆœบๆ•ฐๅญ— + โ”‚ โ”œโ”€โ”€ 10 ๅˆ†้’Ÿๆœ‰ๆ•ˆๆœŸ + โ”‚ โ”œโ”€โ”€ ๆœ€ๅคšๅฐ่ฏ• 5 ๆฌก + โ”‚ โ””โ”€โ”€ 60 ็ง’่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด + โ”‚ + โ”œโ”€> ้˜ฒ็”จๆˆทๅๆžšไธพ๏ผš + โ”‚ โ””โ”€โ”€ ๆ— ่ฎบ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ๏ผŒ้ƒฝ่ฟ”ๅ›ž็›ธๅŒๆถˆๆฏ + โ”‚ + โ””โ”€> ้ชŒ่ฏ็ ๅญ˜ๅ‚จ๏ผš + โ”œโ”€โ”€ ๅ“ˆๅธŒๅญ˜ๅ‚จ๏ผˆSHA-256๏ผ‰ + โ””โ”€โ”€ ๆ•ฐๆฎๅบ“ + ็Šถๆ€็ฎก็† + + 4. Token ้ป‘ๅๅ•ๆœบๅˆถ + โ”‚ + โ”œโ”€> ่งฆๅ‘ๅœบๆ™ฏ๏ผš + โ”‚ โ”œโ”€โ”€ ็”จๆˆทไธปๅŠจ็™ปๅ‡บ + โ”‚ โ”œโ”€โ”€ ็ฎก็†ๅ‘˜ๅผบๅˆถไธ‹็บฟ + โ”‚ โ””โ”€โ”€ ไฟฎๆ”นๅฏ†็ ๅŽ + โ”‚ + โ”œโ”€> ๅญ˜ๅ‚จๆ–นๅผ๏ผšRedis + โ”‚ + โ””โ”€> ้”ฎๆ ผๅผ๏ผštoken:blacklist:{jti} + โ”‚ + โ””โ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผšToken ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆœŸ + + 5.4 ๅฎก่ฎกๆ—ฅๅฟ—ๆจกๅ— + + Read 1 file (ctrl+o to expand) + +โ— ๅฎก่ฎกๆ—ฅๅฟ—ไฝฟ็”จ็คบไพ‹ + + // ๅœจๆ–นๆณ•ไธŠๆทปๅŠ  @AuditLog ๆณจ่งฃ + @AuditLog( + action = "CREATE", + resource = "user", + description = "ๅˆ›ๅปบ็”จๆˆท {0}" + ) + public void createUser(String username) { + // ไธšๅŠก้€ป่พ‘ + } + + // AOP ๅˆ‡้ขไผš่‡ชๅŠจ่ฎฐๅฝ•๏ผš + // - ๆ“ไฝœ็”จๆˆท๏ผˆไปŽ SecurityContext ่Žทๅ–๏ผ‰ + // - ๆ“ไฝœ็ฑปๅž‹๏ผšCREATE + // - ่ต„ๆบ็ฑปๅž‹๏ผšuser + // - ๆ“ไฝœๆ่ฟฐ๏ผš"ๅˆ›ๅปบ็”จๆˆท alice" + // - ่ฏทๆฑ‚ไฟกๆฏ๏ผšIPใ€User-Agent + // - ๆ“ไฝœ็ป“ๆžœ๏ผšๆˆๅŠŸ/ๅคฑ่ดฅ + + 5.5 Redis ็ผ“ๅญ˜ๆจกๅ— + + Read 1 file (ctrl+o to expand) + +โ— Redis ็ผ“ๅญ˜ไฝฟ็”จๅœบๆ™ฏ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Redis ไฝฟ็”จๅœบๆ™ฏๆ€ป็ป“ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + 1. Token ้ป‘ๅๅ• + โ”œโ”€โ”€ ้”ฎๆ ผๅผ๏ผštoken:blacklist:{jti} + โ”œโ”€โ”€ ๅ€ผ๏ผš1๏ผˆๅ›บๅฎšๅ€ผ๏ผŒๅชๅˆคๆ–ญ้”ฎๆ˜ฏๅฆๅญ˜ๅœจ๏ผ‰ + โ””โ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผšToken ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆœŸ + + 2. Refresh Token ๅญ˜ๅ‚จ + โ”œโ”€โ”€ ้”ฎๆ ผๅผ๏ผšrefresh_token:{tokenHash} + โ”œโ”€โ”€ ๅ€ผ๏ผš็”จๆˆทID + โ””โ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผš7 ๅคฉ + + 3. ้™ๆต่ฎกๆ•ฐๅ™จ + โ”œโ”€โ”€ ้”ฎๆ ผๅผ๏ผšrate_limit:{type}:{identifier} + โ”‚ โ”œโ”€โ”€ type: ip / user / global + โ”‚ โ””โ”€โ”€ identifier: IPๅœฐๅ€ / ็”จๆˆทID + โ”œโ”€โ”€ ๅ€ผ๏ผšๅฝ“ๅ‰่ฏทๆฑ‚ๆฌกๆ•ฐ + โ””โ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผšๆ—ถ้—ด็ช—ๅฃ + + 4. ็™ปๅฝ•ๅคฑ่ดฅ่ฎกๆ•ฐ + โ”œโ”€โ”€ ้”ฎๆ ผๅผ๏ผšlogin_attempts:{username} + โ”œโ”€โ”€ ๅ€ผ๏ผšๅคฑ่ดฅๆฌกๆ•ฐ + โ””โ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผš้”ๅฎšๆ—ถ้•ฟ๏ผˆ30ๅˆ†้’Ÿ๏ผ‰ + + 5. ๅˆ†ๅธƒๅผ้” + โ”œโ”€โ”€ ้”ฎๆ ผๅผ๏ผšlock:{resource}:{id} + โ”œโ”€โ”€ ๅ€ผ๏ผšๅ”ฏไธ€ๆ ‡่ฏ†๏ผˆUUID๏ผ‰ + โ”œโ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผš้”่ถ…ๆ—ถๆ—ถ้—ด๏ผˆ้˜ฒๆญขๆญป้”๏ผ‰ + โ””โ”€โ”€ ๅฎž็Žฐๆ–นๅผ๏ผšSET NX EX + Lua ่„šๆœฌ + + 6. ็”จๆˆทไผš่ฏ็ผ“ๅญ˜ + โ”œโ”€โ”€ ้”ฎๆ ผๅผ๏ผšsession:user:{userId} + โ”œโ”€โ”€ ๅ€ผ๏ผšไผš่ฏไฟกๆฏ๏ผˆ็™ปๅฝ•IPใ€ๆ—ถ้—ด็ญ‰๏ผ‰ + โ””โ”€โ”€ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผš24 ๅฐๆ—ถ + + 7. ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + โ”œโ”€โ”€ ๅญ˜ๅ‚จ๏ผšๆ•ฐๆฎๅบ“ password_reset_codes ่กจ + โ””โ”€โ”€ ้™ๅˆถ๏ผšRedis + ๅ†ทๅดๆ—ถ้—ด + + 5.6 ๆถˆๆฏ้˜Ÿๅˆ—ๆจกๅ— + + Read 3 files (ctrl+o to expand) + +โ— RocketMQ ๆถˆๆฏ้˜Ÿๅˆ—ๆžถๆž„ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ RocketMQ ๆถˆๆฏๆต็จ‹ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + ็”จๆˆท็™ปๅฝ• + โ”‚ + โ”œโ”€> UserService.login() + โ”‚ โ”‚ + โ”‚ โ”œโ”€> 1. ้ชŒ่ฏ็”จๆˆท + โ”‚ โ”œโ”€> 2. ็”Ÿๆˆ Token + โ”‚ โ”œโ”€> 3. ๅ‘้€็™ปๅฝ•ๆถˆๆฏๅˆฐ MQ + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€> UserMessageProducer.sendLoginMessage() + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€> rocketMQTemplate.asyncSend(user-topic, message) + โ”‚ โ”‚ + โ”‚ โ””โ”€> 4. ่ฟ”ๅ›žๅ“ๅบ”๏ผˆไธ็ญ‰ๅพ… MQ ๅค„็†๏ผ‰ + โ”‚ + โ”‚ + โ””โ”€> RocketMQ: user-topic + โ”‚ + โ””โ”€> UserMessageConsumer.onMessage() + โ”‚ + โ”œโ”€> 1. ๆ”ถๅˆฐๆถˆๆฏ + โ”œโ”€> 2. ๆ นๆฎ messageType ่ทฏ็”ฑ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€ REGISTER โ†’ handleRegisterMessage() + โ”‚ โ”‚ โ””โ”€> ๅ‘้€ๆฌข่ฟŽ้‚ฎไปถใ€ๅˆๅง‹ๅŒ–ๆ•ฐๆฎ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€ LOGIN โ†’ handleLoginMessage() + โ”‚ โ”‚ โ””โ”€> ่ฎฐๅฝ•็™ปๅฝ•ๆ—ฅๅฟ—ใ€ๆ›ดๆ–ฐๆœ€ๅŽ็™ปๅฝ•ๆ—ถ้—ด + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€ LOGOUT โ†’ handleLogoutMessage() + โ”‚ โ”‚ โ””โ”€> ๆธ…็†็ผ“ๅญ˜ใ€็™ปๅ‡บๅ…ถไป–่ฎพๅค‡ + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€ UPDATE โ†’ handleUpdateMessage() + โ”‚ โ”‚ โ””โ”€> ๅŒๆญฅๆ•ฐๆฎๅˆฐๆ•ฐๆฎไป“ๅบ“ใ€ๆธ…้™ค็ผ“ๅญ˜ + โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€ DELETE โ†’ handleDeleteMessage() + โ”‚ โ””โ”€> ๆธ…็†ๅ…ณ่”ๆ•ฐๆฎใ€ๅฝ’ๆกฃ + โ”‚ + โ””โ”€> 3. ๅค„็†ๅฎŒๆˆ๏ผŒ็กฎ่ฎคๆถˆ่ดน + + --- + ๅ…ญใ€้…็ฝฎๆ–‡ไปถ่งฃๆž + + Searched for 1 pattern, read 2 files (ctrl+o to expand) + +โ— ้…็ฝฎๆ–‡ไปถ็ป“ๆž„ๆ€ป็ป“ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ้…็ฝฎๆ–‡ไปถ็ป“ๆž„ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + application.yaml (ไธป้…็ฝฎ) + โ”œโ”€โ”€ spring.profiles.active: dev (ๆฟ€ๆดป็Žฏๅขƒ) + โ”œโ”€โ”€ server.port: 8080 + โ””โ”€โ”€ ๆ”ฏๆŒ็Žฏๅขƒๅ˜้‡่ฆ†็›– (.env ๆ–‡ไปถ) + + application-dev.yaml (ๅผ€ๅ‘็Žฏๅขƒ) + โ”œโ”€โ”€ ๆ•ฐๆฎๅบ“้…็ฝฎ (MySQL) + โ”‚ โ”œโ”€โ”€ url: jdbc:mysql://localhost:3306/template + โ”‚ โ”œโ”€โ”€ username: root + โ”‚ โ””โ”€โ”€ password: root + โ”œโ”€โ”€ JPA ้…็ฝฎ + โ”‚ โ”œโ”€โ”€ ddl-auto: validate (ไธ่‡ชๅŠจๅปบ่กจ๏ผŒ็”ฑ Flyway ็ฎก็†) + โ”‚ โ””โ”€โ”€ show-sql: false + โ”œโ”€โ”€ Flyway (ๆ•ฐๆฎๅบ“็‰ˆๆœฌ็ฎก็†) + โ”‚ โ”œโ”€โ”€ enabled: true + โ”‚ โ””โ”€โ”€ locations: classpath:db/migration + โ”œโ”€โ”€ Redis ้…็ฝฎ + โ”‚ โ”œโ”€โ”€ host: localhost:6379 + โ”‚ โ””โ”€โ”€ database: 0 + โ”œโ”€โ”€ ้‚ฎไปถ้…็ฝฎ (SMTP) + โ”‚ โ”œโ”€โ”€ host: smtp.163.com + โ”‚ โ””โ”€โ”€ port: 465 + โ”œโ”€โ”€ JWT ้…็ฝฎ + โ”‚ โ”œโ”€โ”€ secret: ๅฏ†้’ฅ๏ผˆHS512 ้œ€่ฆ 64 ๅญ—็ฌฆ๏ผ‰ + โ”‚ โ”œโ”€โ”€ access-token-expiration: 3600 ็ง’ (1 ๅฐๆ—ถ) + โ”‚ โ””โ”€โ”€ refresh-token-expiration: 604800 ็ง’ (7 ๅคฉ) + โ”œโ”€โ”€ ไธšๅŠก้…็ฝฎ + โ”‚ โ”œโ”€โ”€ ็™ปๅฝ•ๅคฑ่ดฅ้”ๅฎš: 5 ๆฌก / 30 ๅˆ†้’Ÿ + โ”‚ โ””โ”€โ”€ ๅฏ†็ ้‡็ฝฎ: 6 ไฝ้ชŒ่ฏ็  / 10 ๅˆ†้’Ÿๆœ‰ๆ•ˆ + โ””โ”€โ”€ RocketMQ ้…็ฝฎ + โ””โ”€โ”€ name-server: localhost:9876 + + --- + ไธƒใ€่ฏทๆฑ‚ๅค„็†ๅฎŒๆ•ดๆต็จ‹ + + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ ๅฎŒๆ•ด่ฏทๆฑ‚ๅค„็†ๆต็จ‹ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + 1. ๅฎขๆˆท็ซฏๅ‘่ตท่ฏทๆฑ‚ + โ”‚ + โ”œโ”€> GET /api/v1/user/info + โ”‚ Headers: + โ”‚ - Authorization: Bearer {accessToken} + โ”‚ - Content-Type: application/json + โ”‚ + โ”‚ + 2. ่ฟ›ๅ…ฅ Spring MVC ๅค„็†้“พ + โ”‚ + โ”œโ”€> LoggingInterceptor (ๆ‹ฆๆˆชๅ™จ) + โ”‚ โ”œโ”€โ”€ ่ฎฐๅฝ•่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ถ้—ด + โ”‚ โ”œโ”€โ”€ ็”Ÿๆˆ่ฏทๆฑ‚ ID (8 ไฝ UUID) + โ”‚ โ”œโ”€โ”€ ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + โ”‚ โ””โ”€โ”€ ่ฎฐๅฝ•่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ฅๅฟ— + โ”‚ + โ”‚ + 3. ่ฟ›ๅ…ฅ Filter ้“พ + โ”‚ + โ”œโ”€> JwtAuthenticationFilter (JWT ่ฟ‡ๆปคๅ™จ) + โ”‚ โ”œโ”€โ”€ ๆๅ– Token: Bearer {token} + โ”‚ โ”œโ”€โ”€ ้ชŒ่ฏ Token ็ญพๅ (HS512) + โ”‚ โ”œโ”€โ”€ ๆฃ€ๆŸฅ้ป‘ๅๅ• (Redis: token:blacklist:{jti}) + โ”‚ โ”œโ”€โ”€ ๆฃ€ๆŸฅ่ฟ‡ๆœŸๆ—ถ้—ด + โ”‚ โ”œโ”€โ”€ ๅŠ ่ฝฝ็”จๆˆท่ฏฆๆƒ… + โ”‚ โ””โ”€โ”€ ่ฎพ็ฝฎ่ฎค่ฏๅˆฐ SecurityContext + โ”‚ + โ”‚ + 4. ๆƒ้™้ชŒ่ฏ + โ”‚ + โ”œโ”€> @PreAuthorize("hasAuthority('user:read')") + โ”‚ โ””โ”€โ”€ ๆฃ€ๆŸฅ็”จๆˆทๆƒ้™ๅˆ—่กจๆ˜ฏๅฆๅŒ…ๅซ "user:read" + โ”‚ + โ”‚ + 5. AOP ๅˆ‡้ขๅค„็† + โ”‚ + โ”œโ”€> AuditLogAspect (ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ‡้ข) + โ”‚ โ”œโ”€โ”€ ๆฃ€ๆŸฅ @AuditLog ๆณจ่งฃ + โ”‚ โ”œโ”€โ”€ ๆๅ–่ฏทๆฑ‚ไฟกๆฏ + โ”‚ โ”œโ”€โ”€ ๆๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + โ”‚ โ””โ”€โ”€ ๅ‡†ๅค‡่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—๏ผˆๅผ‚ๆญฅๆ‰ง่กŒ๏ผ‰ + โ”‚ + โ”‚ + 6. ๆ‰ง่กŒ Controller + โ”‚ + โ”œโ”€> UserController.getUserInfo() + โ”‚ โ”œโ”€โ”€ ไปŽ SecurityContext ่Žทๅ–็”จๆˆท + โ”‚ โ””โ”€โ”€ ่ฟ”ๅ›ž็”จๆˆทไฟกๆฏ (UserVo) + โ”‚ + โ”‚ + 7. ๅ“ๅบ”ๅค„็† + โ”‚ + โ”œโ”€> ๅ…จๅฑ€ๅผ‚ๅธธๅค„็† + โ”‚ โ””โ”€โ”€ GlobalExceptionHandler + โ”‚ โ”œโ”€โ”€ ๆ•่ŽทไธšๅŠกๅผ‚ๅธธ + โ”‚ โ”œโ”€โ”€ ๆ•่Žทๅ‚ๆ•ฐๆ ก้ชŒๅผ‚ๅธธ + โ”‚ โ””โ”€โ”€ ๆ•่Žท็ณป็ปŸๅผ‚ๅธธ + โ”‚ + โ”‚ + 8. ๅŽ็ฝฎๅค„็† + โ”‚ + โ”œโ”€> LoggingInterceptor.afterCompletion() + โ”‚ โ”œโ”€โ”€ ่ฎก็ฎ—่ฏทๆฑ‚่€—ๆ—ถ + โ”‚ โ””โ”€โ”€ ่ฎฐๅฝ•่ฏทๆฑ‚ๅฎŒๆˆๆ—ฅๅฟ— + โ”‚ + โ””โ”€> ่ฟ”ๅ›žๅ“ๅบ” + โ””โ”€> RestBean + โ”œโ”€โ”€ code: 200 + โ”œโ”€โ”€ message: "ๆ“ไฝœๆˆๅŠŸ" + โ””โ”€โ”€ data: UserVo + + --- + ๅ…ซใ€ๆ€ป็ป“ไธŽๅปบ่ฎฎ + + ้กน็›ฎไผ˜ๅŠฟ + + 1. ๆžถๆž„ๆธ…ๆ™ฐ๏ผšๅˆ†ๅฑ‚ๆ˜Ž็กฎ๏ผŒ่Œ่ดฃๅ•ไธ€ + 2. ๅฎ‰ๅ…จๅฎŒๅ–„๏ผšJWT + RBAC + ๅคš้‡้˜ฒๆŠค + 3. ๆ‰ฉๅฑ•ๆ€งๅผบ๏ผšๆจกๅ—ๅŒ–่ฎพ่ฎก๏ผŒๆ˜“ไบŽๆ‰ฉๅฑ• + 4. ๆ–‡ๆกฃๅฎŒๆ•ด๏ผšๅ…จไธญๆ–‡ๆณจ้‡Š๏ผŒไพฟไบŽ็ปดๆŠค + 5. ๅผ‚ๆญฅ่งฃ่€ฆ๏ผšRocketMQ ๆถˆๆฏ้˜Ÿๅˆ— + + ้€‚็”จๅœบๆ™ฏ + + - ไผไธšๅ†…้ƒจ็ฎก็†็ณป็ปŸ + - SaaS ๅคš็งŸๆˆทๅนณๅฐ + - ้œ€่ฆ RBAC ๆƒ้™็š„็ณป็ปŸ + - ้œ€่ฆๅฎก่ฎกๆ—ฅๅฟ—็š„็ณป็ปŸ + - ้œ€่ฆ้ซ˜ๅฎ‰ๅ…จๆ€ง็š„็ณป็ปŸ + + ๆ‰ฉๅฑ•ๅปบ่ฎฎ + + 1. ๆ•ฐๆฎๆƒ้™๏ผšๅœจ RBAC ๅŸบ็ก€ไธŠๅขžๅŠ ๆ•ฐๆฎๆƒ้™ๆŽงๅˆถ + 2. ๅคš็งŸๆˆท๏ผšๆทปๅŠ ็งŸๆˆท้š”็ฆปๆœบๅˆถ + 3. OAuth2๏ผšๆ”ฏๆŒ็ฌฌไธ‰ๆ–น็™ปๅฝ•๏ผˆๅพฎไฟกใ€GitHub็ญ‰๏ผ‰ + 4. ็›‘ๆŽงๅ‘Š่ญฆ๏ผš้›†ๆˆ Prometheus + Grafana + 5. ๆ—ฅๅฟ—ๆ”ถ้›†๏ผš้›†ๆˆ ELK๏ผˆElasticsearch + Logstash + Kibana๏ผ‰ diff --git a/README.md b/README.md index 878c0f6..32cf6b2 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,9 @@ docker run -d -p 8080:8080 \ ่ฏฆ่ง [API_DOCUMENT.md](API_DOCUMENT.md) +## ้กน็›ฎๆฆ‚่งˆ + +่ฏฆ่ง [DETAILS.md](DETAILS.md) ## License Apache License 2.0 diff --git a/README_USAGE.md b/README_USAGE.md new file mode 100644 index 0000000..18a61b8 --- /dev/null +++ b/README_USAGE.md @@ -0,0 +1,559 @@ +# Spring Boot ๆจกๆฟ้กน็›ฎไฝฟ็”จๆ–‡ๆกฃ + +## ้กน็›ฎๆฆ‚่ฟฐ + +่ฟ™ๆ˜ฏไธ€ไธชๅŠŸ่ƒฝๅฎŒๆ•ด็š„ Spring Boot 3.x ้กน็›ฎๆจกๆฟ๏ผŒ้›†ๆˆไบ†ไปฅไธ‹ๆ ธๅฟƒๅŠŸ่ƒฝ๏ผš + +- **็”จๆˆท่ฎค่ฏไธŽๆŽˆๆƒ**๏ผšๅŸบไบŽ JWT + Spring Security +- **RBAC ๆƒ้™ๆจกๅž‹**๏ผš่ง’่‰ฒ-ๆƒ้™-่œๅ•ไธ‰็บงๆƒ้™ๆŽงๅˆถ +- **ๆ•ฐๆฎๅบ“่ฟ็งป**๏ผšFlyway ็‰ˆๆœฌๅŒ–็ฎก็† +- **Redis ็ผ“ๅญ˜**๏ผšๅคš็งๅœบๆ™ฏๅŒ–็ผ“ๅญ˜ๆ”ฏๆŒ +- **RocketMQ ๆถˆๆฏ้˜Ÿๅˆ—**๏ผšๅผ‚ๆญฅๆถˆๆฏๅค„็† +- **ๅˆ†ๅธƒๅผ้”**๏ผšๅŸบไบŽ Redis ๅฎž็Žฐ +- **ๆŽฅๅฃ้™ๆต**๏ผšๅŸบไบŽ Redis + AOP +- **ๅฎก่ฎกๆ—ฅๅฟ—**๏ผšAOP ่‡ชๅŠจ่ฎฐๅฝ•ๆ“ไฝœๆ—ฅๅฟ— +- **่ดฆๆˆทๅฎ‰ๅ…จ**๏ผš็™ปๅฝ•ๅคฑ่ดฅ้”ๅฎšใ€ๅฏ†็ ๅผบๅบฆๆ ก้ชŒ + +--- + +## 1. ๅฟซ้€Ÿๅผ€ๅง‹ + +### 1.1 ็Žฏๅขƒ่ฆๆฑ‚ + +- JDK 17+ +- MySQL 8.0+ +- Redis 6.0+ +- RocketMQ 5.x๏ผˆๅฏ้€‰๏ผ‰ + +### 1.2 ๆ•ฐๆฎๅบ“ๅˆๅง‹ๅŒ– + +้กน็›ฎไฝฟ็”จ Flyway ่‡ชๅŠจ็ฎก็†ๆ•ฐๆฎๅบ“่ฟ็งป๏ผŒๅฏๅŠจๆ—ถไผš่‡ชๅŠจๆ‰ง่กŒไปฅไธ‹ๆ“ไฝœ๏ผš + +1. ๅˆ›ๅปบ็”จๆˆท่กจ +2. ๅˆ›ๅปบ RBAC ็›ธๅ…ณ่กจ๏ผˆ่ง’่‰ฒใ€ๆƒ้™ใ€่œๅ•๏ผ‰ +3. ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—่กจ +4. ๅˆ›ๅปบ Refresh Token ่กจ + +### 1.3 ้…็ฝฎๆ–‡ไปถ + +ไฟฎๆ”น `application-dev.yaml`๏ผš + +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=Asia/Shanghai + username: your_username + password: your_password + + data: + redis: + host: localhost + port: 6379 + password: your_redis_password + +jwt: + secret: your-secret-key-at-least-64-characters-long +``` + +--- + +## 2. ๆ ธๅฟƒๅŠŸ่ƒฝไฝฟ็”จ + +### 2.1 ็”จๆˆท่ฎค่ฏไธŽๆŽˆๆƒ + +#### ๆณจๅ†Œ็”จๆˆท + +```java +@Autowired +private UserService userService; + +UserDto userDto = new UserDto(); +userDto.setUsername("testuser"); +userDto.setPassword("Test123!"); // ๅฟ…้กปๅŒ…ๅซๅคงๅฐๅ†™ๅญ—ๆฏใ€ๆ•ฐๅญ—ใ€็‰นๆฎŠๅญ—็ฌฆ +userDto.setEmail("test@example.com"); + +RestBean result = userService.register(userDto); +``` + +#### ็”จๆˆท็™ปๅฝ• + +```java +UserDto loginDto = new UserDto(); +loginDto.setUsername("testuser"); +loginDto.setPassword("Test123!"); + +RestBean result = userService.login(loginDto); +// ่ฟ”ๅ›ž accessToken ๅ’Œ refreshToken +``` + +#### ไฝฟ็”จ Token ่ฎฟ้—ฎๆŽฅๅฃ + +```bash +curl -H "Authorization: Bearer {accessToken}" \ + http://localhost:8080/api/v1/user/info +``` + +#### Token ๅˆทๆ–ฐ + +```bash +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"refreshToken": "{refreshToken}"}' \ + http://localhost:8080/api/v1/user/refresh +``` + +#### ็™ปๅ‡บ + +```bash +curl -X POST \ + -H "Authorization: Bearer {accessToken}" \ + http://localhost:8080/api/v1/user/logout +``` + +--- + +### 2.2 RBAC ๆƒ้™ๆŽงๅˆถ + +#### ๅˆ›ๅปบ่ง’่‰ฒ + +```java +@Autowired +private SysRoleService roleService; + +RoleDto roleDto = new RoleDto(); +roleDto.setRoleCode("ROLE_EDITOR"); +roleDto.setRoleName("็ผ–่พ‘"); +roleDto.setDescription("ๅ†…ๅฎน็ผ–่พ‘่ง’่‰ฒ"); +roleDto.setSortOrder(3); +roleDto.setStatus(1); + +RoleVo role = roleService.create(roleDto); +``` + +#### ไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ + +```java +List permissionIds = Arrays.asList(1L, 2L, 3L); // ๆ–‡็ซ ๅˆ›ๅปบใ€็ผ–่พ‘ใ€ๅˆ ้™ค +roleService.assignPermissions(roleId, permissionIds); +``` + +#### ไธบ็”จๆˆทๅˆ†้…่ง’่‰ฒ + +```java +@Autowired +private UserService userService; + +UserRoleUpdateDto dto = new UserRoleUpdateDto(); +dto.setRoleIds(Set.of(1L, 2L)); // ่ง’่‰ฒIDๅˆ—่กจ + +userService.updateUserRole(userId, dto); +``` + +#### ๅœจ Controller ไธญไฝฟ็”จๆƒ้™ๆณจ่งฃ + +```java +@RestController +@RequestMapping("/api/v1/articles") +public class ArticleController { + + // ๅชๆœ‰ๅ…ทๆœ‰ 'article:create' ๆƒ้™็š„็”จๆˆทๅฏไปฅ่ฎฟ้—ฎ + @PreAuthorize("hasAuthority('article:create')") + @PostMapping + public RestBean create(@RequestBody ArticleDto dto) { + // ... + } + + // ้œ€่ฆ็ฎก็†ๅ‘˜่ง’่‰ฒๆˆ–ๆ–‡็ซ ็ฎก็†ๆƒ้™ + @PreAuthorize("hasAnyRole('ROLE_ADMIN') or hasAuthority('article:delete')") + @DeleteMapping("/{id}") + public RestBean delete(@PathVariable Long id) { + // ... + } +} +``` + +--- + +### 2.3 Redis ็ผ“ๅญ˜ไฝฟ็”จ + +#### ๅŸบๆœฌ็ผ“ๅญ˜ๆ“ไฝœ + +```java +@Autowired +private RedisCache redisCache; + +// ่ฎพ็ฝฎ็ผ“ๅญ˜๏ผˆ30ๅˆ†้’Ÿ่ฟ‡ๆœŸ๏ผ‰ +redisCache.set("user:" + userId, userObject, 30, TimeUnit.MINUTES); + +// ่Žทๅ–็ผ“ๅญ˜ +User user = redisCache.get("user:" + userId, User.class); + +// ๅˆ ้™ค็ผ“ๅญ˜ +redisCache.delete("user:" + userId); +``` + +#### ่ฎกๆ•ฐๅ™จ๏ผˆ้˜…่ฏปๆ•ฐใ€็‚น่ตžๆ•ฐ๏ผ‰ + +```java +// ๅŽŸๅญ่‡ชๅขž +long viewCount = redisCache.increment("article:view:" + articleId, 1); + +// ๅŽŸๅญ่‡ชๅ‡ +long stock = redisCache.decrement("product:stock:" + productId, quantity); +``` + +#### ๅ“ˆๅธŒ่กจ๏ผˆๅฏน่ฑกๅญ—ๆฎต็ผ“ๅญ˜๏ผ‰ + +```java +// ่ฎพ็ฝฎๅ•ไธชๅญ—ๆฎต +redisCache.hSet("user:profile:" + userId, "nickname", "ๅผ ไธ‰"); +redisCache.hSet("user:profile:" + userId, "age", 25); + +// ่Žทๅ–ๅ•ไธชๅญ—ๆฎต +String nickname = (String) redisCache.hGet("user:profile:" + userId, "nickname"); + +// ่Žทๅ–ๆ‰€ๆœ‰ๅญ—ๆฎต +Map profile = redisCache.hGetAll("user:profile:" + userId); +``` + +#### ๅˆ—่กจ๏ผˆๆถˆๆฏ้˜Ÿๅˆ—๏ผ‰ + +```java +// ไปŽๅณไพงๆŽจๅ…ฅ๏ผˆ้˜Ÿๅˆ—ๅฐพ้ƒจ๏ผ‰ +redisCache.lRightPush("queue:email", emailObject); + +// ไปŽๅทฆไพงๅผนๅ‡บ๏ผˆ้˜Ÿๅˆ—ๅคด้ƒจ๏ผ‰ +Object email = redisCache.lLeftPop("queue:email"); +``` + +#### ้›†ๅˆ๏ผˆๅŽป้‡ใ€ๆ ‡็ญพ๏ผ‰ + +```java +// ๆทปๅŠ ๆ ‡็ญพ๏ผˆ่‡ชๅŠจๅŽป้‡๏ผ‰ +redisCache.sAdd("article:tags:" + articleId, "Java", "Spring", "Redis"); + +// ๆฃ€ๆŸฅๆ ‡็ญพๆ˜ฏๅฆๅญ˜ๅœจ +boolean hasTag = redisCache.sIsMember("article:tags:" + articleId, "Java"); + +// ่Žทๅ–ๆ‰€ๆœ‰ๆ ‡็ญพ +Set tags = redisCache.sMembers("article:tags:" + articleId); +``` + +#### ๆœ‰ๅบ้›†ๅˆ๏ผˆๆŽ’่กŒๆฆœ๏ผ‰ + +```java +// ๆทปๅŠ ๅˆฐๆŽ’่กŒๆฆœ +redisCache.zAdd("leaderboard:user:score", userId, score); + +// ่Žทๅ–็”จๆˆทๆŽ’ๅ +Long rank = redisCache.zReverseRank("leaderboard:user:score", userId); + +// ่Žทๅ–็”จๆˆทๅˆ†ๆ•ฐ +Double score = redisCache.zScore("leaderboard:user:score", userId); + +// ๅขžๅŠ ๅˆ†ๆ•ฐ +redisCache.zIncrementScore("leaderboard:user:score", userId, 10.0); +``` + +--- + +### 2.4 ๅˆ†ๅธƒๅผ้”ไฝฟ็”จ + +```java +@Autowired +private RedisLock redisLock; + +public void processOrder(Long orderId) { + // 1. ่Žทๅ–้”๏ผˆ30็ง’่ฟ‡ๆœŸ๏ผ‰ + String lockValue = redisLock.tryLock("order:" + orderId, 30); + + if (lockValue == null) { + throw new RuntimeException("่ฎขๅ•ๆญฃๅœจๅค„็†ไธญ"); + } + + try { + // 2. ๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + // ... + + } finally { + // 3. ้‡Šๆ”พ้”๏ผˆๅชๆœ‰ๆŒๆœ‰่€…ๆ‰่ƒฝ้‡Šๆ”พ๏ผ‰ + redisLock.unlock("order:" + orderId, lockValue); + } +} +``` + +--- + +### 2.5 ๆŽฅๅฃ้™ๆตไฝฟ็”จ + +```java +@RestController +public class LoginController { + + // ้™ๅˆถๆฏ IP ๆฏๅˆ†้’Ÿๆœ€ๅคš 5 ๆฌก็™ปๅฝ•ๅฐ่ฏ• + @RateLimit(permits = 5, seconds = 60, limitType = RateLimit.LimitType.IP) + @PostMapping("/login") + public RestBean login(@RequestBody UserDto dto) { + // ... + } +} +``` + +--- + +### 2.6 ๅฎก่ฎกๆ—ฅๅฟ—ไฝฟ็”จ + +```java +@RestController +public class UserController { + + // ่‡ชๅŠจ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ— + @AuditLog(action = "update", resource = "user", description = "ๆ›ดๆ–ฐ็”จๆˆทไฟกๆฏ") + @PutMapping("/{id}") + @PreAuthorize("hasAuthority('user:update')") + public RestBean updateUser(@PathVariable Long id, @RequestBody UserDto dto) { + // ๆ“ไฝœไผš่ขซ่‡ชๅŠจ่ฎฐๅฝ•ๅˆฐ sys_audit_log ่กจ + } +} +``` + +--- + +### 2.7 ไบ‹ๅŠกไฝฟ็”จ + +#### ๅŸบๆœฌไบ‹ๅŠก + +```java +@Service +public class UserService { + + // ๆ‰€ๆœ‰ๅผ‚ๅธธ้ƒฝๅ›žๆปš + @Transactional(rollbackFor = Exception.class) + public void createUserWithRole(UserDto userDto, Set roleIds) { + // ๅˆ›ๅปบ็”จๆˆท + User user = new User(); + // ... + userRepository.save(user); + + // ๅˆ†้…่ง’่‰ฒ + for (Long roleId : roleIds) { + SysRole role = roleRepository.findById(roleId) + .orElseThrow(() -> new BusinessException("่ง’่‰ฒไธๅญ˜ๅœจ")); + user.getRoles().add(role); + } + userRepository.save(user); + // ไปปไฝ•ๅผ‚ๅธธ้ƒฝไผšๅ›žๆปšๆ•ดไธชไบ‹ๅŠก + } +} +``` + +#### ๅตŒๅฅ—ไบ‹ๅŠก + +```java +// ๅค–ๅฑ‚ไบ‹ๅŠก +@Transactional(propagation = Propagation.REQUIRED) +public void outerMethod() { + // ไธšๅŠก้€ป่พ‘ + innerMethod(); // ๅŠ ๅ…ฅๅค–ๅฑ‚ไบ‹ๅŠก +} + +// ๅ†…ๅฑ‚ไบ‹ๅŠก +@Transactional(propagation = Propagation.REQUIRED) +public void innerMethod() { + // ๅŠ ๅ…ฅๅค–ๅฑ‚ไบ‹ๅŠก +} +``` + +#### ็‹ฌ็ซ‹ไบ‹ๅŠก + +```java +@Transactional(propagation = Propagation.REQUIRED) +public void mainMethod() { + // ไธปไบ‹ๅŠก้€ป่พ‘ + + // ็‹ฌ็ซ‹ไบ‹ๅŠก๏ผˆๅณไฝฟไธปไบ‹ๅŠกๅ›žๆปšไนŸไธๅฝฑๅ“๏ผ‰ + recordLog(); +} + +@Transactional(propagation = Propagation.REQUIRES_NEW) +public void recordLog() { + // ็‹ฌ็ซ‹ไบ‹ๅŠก +} +``` + +--- + +### 2.8 RocketMQ ๆถˆๆฏ้˜Ÿๅˆ—ไฝฟ็”จ + +#### ๅ‘้€ๆถˆๆฏ + +```java +@Autowired +private UserMessageProducer messageProducer; + +// ๅ‘้€ๆณจๅ†Œๆถˆๆฏ +messageProducer.sendRegisterMessage(userId, username, email); + +// ๅ‘้€็™ปๅฝ•ๆถˆๆฏ +messageProducer.sendLoginMessage(userId, username, ipAddress); +``` + +#### ๆถˆ่ดนๆถˆๆฏ + +ๆถˆๆฏไผš่ขซ `UserMessageConsumer` ่‡ชๅŠจๆถˆ่ดน๏ผŒๆ นๆฎๆถˆๆฏ็ฑปๅž‹ๆ‰ง่กŒไธๅŒๆ“ไฝœ๏ผš + +```java +@RocketMQMessageListener( + consumerGroup = "user-consumer-group", + topic = "user-topic" +) +public class UserMessageConsumer implements RocketMQListener { + + @Override + public void onMessage(UserMessage message) { + switch (message.getMessageType()) { + case "REGISTER": + // ๅค„็†ๆณจๅ†Œๆถˆๆฏ๏ผˆๅ‘้€ๆฌข่ฟŽ้‚ฎไปถ็ญ‰๏ผ‰ + break; + case "LOGIN": + // ๅค„็†็™ปๅฝ•ๆถˆๆฏ๏ผˆ่ฎฐๅฝ•็™ปๅฝ•ๆ—ฅๅฟ—๏ผ‰ + break; + // ... + } + } +} +``` + +--- + +## 3. ้…็ฝฎ่ฏดๆ˜Ž + +### 3.1 JWT ้…็ฝฎ + +```yaml +jwt: + # JWT ๅฏ†้’ฅ๏ผˆ่‡ณๅฐ‘64ๅญ—็ฌฆ๏ผ‰ + secret: your-secret-key-at-least-64-characters-long + # Access Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + access-token-expiration: 3600 # 1ๅฐๆ—ถ + # Refresh Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + refresh-token-expiration: 604800 # 7ๅคฉ +``` + +### 3.2 ็™ปๅฝ•ๅฎ‰ๅ…จ้…็ฝฎ + +```yaml +app: + login: + # ๆœ€ๅคงๅคฑ่ดฅๆฌกๆ•ฐ + max-attempts: 5 + # ้”ๅฎšๆ—ถ้•ฟ๏ผˆๅˆ†้’Ÿ๏ผ‰ + lock-duration-minutes: 30 +``` + +### 3.3 Redis ้…็ฝฎ + +```yaml +spring: + data: + redis: + host: localhost + port: 6379 + password: your-password + database: 0 + timeout: 5000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 2 +``` + +### 3.4 RocketMQ ้…็ฝฎ + +```yaml +rocketmq: + name-server: localhost:9876 + producer: + group: user-producer-group + user-topic: user-topic +``` + +--- + +## 4. ๅธธ่ง้—ฎ้ข˜ + +### 4.1 ๅฆ‚ไฝ•ๆทปๅŠ ๆ–ฐ็š„ๆƒ้™๏ผŸ + +1. ๅœจๆ•ฐๆฎๅบ“ไธญๆ’ๅ…ฅๆƒ้™่ฎฐๅฝ•๏ผˆ้€š่ฟ‡ Flyway ่ฟ็งป่„šๆœฌ๏ผ‰ +2. ๅœจ Controller ไธŠไฝฟ็”จ `@PreAuthorize` ๆณจ่งฃ + +```java +@PreAuthorize("hasAuthority('new:permission')") +public void someMethod() { } +``` + +### 4.2 ๅฆ‚ไฝ•่‡ชๅฎšไน‰้™ๆต่ง„ๅˆ™๏ผŸ + +ไฝฟ็”จ `@RateLimit` ๆณจ่งฃ๏ผš + +```java +@RateLimit( + permits = 10, // 10ๆฌก + seconds = 60, // ๆฏๅˆ†้’Ÿ + limitType = RateLimit.LimitType.USER, // ๆŒ‰็”จๆˆท้™ๆต + keyPrefix = "custom:" // ่‡ชๅฎšไน‰้”ฎๅ‰็ผ€ +) +public void customMethod() { } +``` + +### 4.3 ๅฆ‚ไฝ•ๅฎž็Žฐ็ผ“ๅญ˜้ข„็ƒญ๏ผŸ + +ๅœจๅบ”็”จๅฏๅŠจๆ—ถๅŠ ่ฝฝ็ƒญ็‚นๆ•ฐๆฎ๏ผš + +```java +@Component +public class CacheWarmupRunner implements ApplicationRunner { + + @Autowired + private RedisCache redisCache; + + @Override + public void run(ApplicationArguments args) { + // ้ข„็ƒญ็ƒญ็‚นๆ•ฐๆฎ + List hotUsers = userRepository.findHotUsers(); + for (User user : hotUsers) { + redisCache.set("user:" + user.getId(), user, 1, TimeUnit.HOURS); + } + } +} +``` + +--- + +## 5. ๆœ€ไฝณๅฎž่ทต + +### 5.1 ๅฏ†็ ๅฎ‰ๅ…จ + +- ็”Ÿไบง็Žฏๅขƒๅฟ…้กปไฟฎๆ”น JWT ๅฏ†้’ฅ +- ไฝฟ็”จๅผบๅฏ†็ ็ญ–็•ฅ๏ผˆๅทฒ้›†ๆˆ `@StrongPassword`๏ผ‰ +- ๅฎšๆœŸๆ›ดๆขๅฏ†็  + +### 5.2 ๆ•ฐๆฎๅบ“ไบ‹ๅŠก + +- ๆŸฅ่ฏขๆ“ไฝœไฝฟ็”จ `@Transactional(readOnly = true)` +- ๆ˜Ž็กฎๆŒ‡ๅฎš `rollbackFor = Exception.class` +- ้ฟๅ…ๅคงไบ‹ๅŠก + +### 5.3 Redis ไฝฟ็”จ + +- ๅˆ็†่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด๏ผŒ้ฟๅ…ๅ†…ๅญ˜ๆบขๅ‡บ +- ไฝฟ็”จ Redis ๅˆ†ๅธƒๅผ้”้˜ฒๆญขๅนถๅ‘้—ฎ้ข˜ +- ็ƒญ็‚นๆ•ฐๆฎไฝฟ็”จ็ผ“ๅญ˜๏ผŒๅ‡ๅฐ‘ๆ•ฐๆฎๅบ“ๅŽ‹ๅŠ› + +### 5.4 ๅฎ‰ๅ…จๅปบ่ฎฎ + +- ไฟฎๆ”น CORS ้…็ฝฎ๏ผŒ้™ๅˆถๅ…่ฎธ็š„ๅŸŸๅ +- ไฝฟ็”จ HTTPS +- ๅฎšๆœŸๅฎกๆŸฅ็”จๆˆทๆƒ้™ +- ๅฏ็”จๅฎก่ฎกๆ—ฅๅฟ— diff --git a/pom.xml b/pom.xml index bbd9c97..69db3c3 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,31 @@ com.fasterxml.jackson.core jackson-databind + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-mysql + + + + org.apache.rocketmq + rocketmq-spring-boot-starter + 2.3.1 + + + + org.springframework.boot + spring-boot-starter-aop + + + + com.github.ben-manes.caffeine + caffeine + diff --git a/src/main/java/com/aisi/template/TemplateApplication.java b/src/main/java/com/aisi/template/TemplateApplication.java index 6e5d9b1..3c4ad1d 100644 --- a/src/main/java/com/aisi/template/TemplateApplication.java +++ b/src/main/java/com/aisi/template/TemplateApplication.java @@ -2,6 +2,9 @@ package com.aisi.template; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; @SpringBootApplication public class TemplateApplication { diff --git a/src/main/java/com/aisi/template/annotation/AuditLog.java b/src/main/java/com/aisi/template/annotation/AuditLog.java new file mode 100644 index 0000000..bc72cf8 --- /dev/null +++ b/src/main/java/com/aisi/template/annotation/AuditLog.java @@ -0,0 +1,62 @@ +package com.aisi.template.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ๅฎก่ฎกๆ—ฅๅฟ—ๆณจ่งฃ + * ๆ ‡ๆณจ้œ€่ฆ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—็š„ๆ–นๆณ• + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆท็™ปๅฝ•/็™ปๅ‡บ + * - ๆ•ฐๆฎๅˆ›ๅปบ/ๆ›ดๆ–ฐ/ๅˆ ้™ค + * - ๆ•ๆ„Ÿๆ“ไฝœ่ฎฟ้—ฎ + * + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+ * @AuditLog(
+ *     action = "LOGIN",
+ *     resource = "user",
+ *     description = "็”จๆˆท {0} ็™ปๅฝ•ๆˆๅŠŸ"
+ * )
+ * public void login(String username) { ... }
+ * 
+ * + * @author Claude + * @since 2024-04-09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuditLog { + + /** + * ๆ“ไฝœ็ฑปๅž‹ + * ๅธธ่งๅ€ผ๏ผš + * - LOGIN๏ผš็™ปๅฝ• + * - LOGOUT๏ผš็™ปๅ‡บ + * - CREATE๏ผšๅˆ›ๅปบ + * - UPDATE๏ผšๆ›ดๆ–ฐ + * - DELETE๏ผšๅˆ ้™ค + */ + String action(); + + /** + * ่ต„ๆบ็ฑปๅž‹ + * ๅธธ่งๅ€ผ๏ผš + * - user๏ผš็”จๆˆท + * - role๏ผš่ง’่‰ฒ + * - permission๏ผšๆƒ้™ + * - menu๏ผš่œๅ• + */ + String resource(); + + /** + * ๆ“ไฝœๆ่ฟฐๆจกๆฟ + * ่ฏดๆ˜Ž๏ผš + * - ๆ”ฏๆŒไฝฟ็”จ {0}, {1} ็ญ‰ๅ ไฝ็ฌฆๅผ•็”จๆ–นๆณ•ๅ‚ๆ•ฐ + * - ็คบไพ‹๏ผš"ๆ›ดๆ–ฐ็”จๆˆท {0}"๏ผŒๅ‚ๆ•ฐ๏ผš["123"] โ†’ "ๆ›ดๆ–ฐ็”จๆˆท 123" + */ + String description() default ""; +} diff --git a/src/main/java/com/aisi/template/annotation/PasswordValidator.java b/src/main/java/com/aisi/template/annotation/PasswordValidator.java new file mode 100644 index 0000000..3eb5c6c --- /dev/null +++ b/src/main/java/com/aisi/template/annotation/PasswordValidator.java @@ -0,0 +1,164 @@ +package com.aisi.template.annotation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * ๅฏ†็ ้ชŒ่ฏๅ™จ + * ไธบ @StrongPassword ๆณจ่งฃๆไพ›้ชŒ่ฏ้€ป่พ‘ + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * 1. ๆœ€ๅฐ้•ฟๅบฆๆฃ€ๆŸฅ + * 2. ๅคงๅ†™ๅญ—ๆฏๆฃ€ๆŸฅ + * 3. ๅฐๅ†™ๅญ—ๆฏๆฃ€ๆŸฅ + * 4. ๆ•ฐๅญ—ๆฃ€ๆŸฅ + * 5. ็‰นๆฎŠๅญ—็ฌฆๆฃ€ๆŸฅ + * + * @author Claude + * @since 2024-04-09 + */ +public class PasswordValidator implements ConstraintValidator { + + /** + * ๆœ€ๅฐๅฏ†็ ้•ฟๅบฆ + */ + private int minLength; + + /** + * ๆ˜ฏๅฆ้œ€่ฆๅคงๅ†™ๅญ—ๆฏ + */ + private boolean requireUppercase; + + /** + * ๆ˜ฏๅฆ้œ€่ฆๅฐๅ†™ๅญ—ๆฏ + */ + private boolean requireLowercase; + + /** + * ๆ˜ฏๅฆ้œ€่ฆๆ•ฐๅญ— + */ + private boolean requireDigit; + + /** + * ๆ˜ฏๅฆ้œ€่ฆ็‰นๆฎŠๅญ—็ฌฆ + */ + private boolean requireSpecialChar; + + /** + * ๅˆๅง‹ๅŒ–้ชŒ่ฏๅ™จ + * ๆญฅ้ชค๏ผš + * 1. ไปŽๆณจ่งฃไธญ่ฏปๅ–้…็ฝฎๅ‚ๆ•ฐ + * 2. ไฟๅญ˜ๅˆฐๅฎžไพ‹ๅ˜้‡ + * + * @param constraintAnnotation ๅผบๅฏ†็ ๆณจ่งฃ + */ + @Override + public void initialize(StrongPassword constraintAnnotation) { + this.minLength = constraintAnnotation.minLength(); + this.requireUppercase = constraintAnnotation.requireUppercase(); + this.requireLowercase = constraintAnnotation.requireLowercase(); + this.requireDigit = constraintAnnotation.requireDigit(); + this.requireSpecialChar = constraintAnnotation.requireSpecialChar(); + } + + /** + * ้ชŒ่ฏๅฏ†็ ๆ˜ฏๅฆ็ฌฆๅˆ่ฆๆฑ‚ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅๆ˜ฏๅฆไธบ็ฉบ + * 2. ๆฃ€ๆŸฅๆœ€ๅฐ้•ฟๅบฆ + * 3. ๆฃ€ๆŸฅๅคงๅ†™ๅญ—ๆฏ + * 4. ๆฃ€ๆŸฅๅฐๅ†™ๅญ—ๆฏ + * 5. ๆฃ€ๆŸฅๆ•ฐๅญ— + * 6. ๆฃ€ๆŸฅ็‰นๆฎŠๅญ—็ฌฆ + * + * @param password ๅฏ†็  + * @param context ็บฆๆŸ้ชŒ่ฏไธŠไธ‹ๆ–‡ + * @return ๆ˜ฏๅฆๆœ‰ๆ•ˆ + */ + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + // 1. ๆฃ€ๆŸฅๆ˜ฏๅฆไธบ็ฉบ + if (password == null || password.isEmpty()) { + return false; + } + + // 2. ๆฃ€ๆŸฅๆœ€ๅฐ้•ฟๅบฆ + if (password.length() < minLength) { + return false; + } + + // 3. ๆฃ€ๆŸฅๅคงๅ†™ๅญ—ๆฏ + if (requireUppercase && !containsUppercase(password)) { + return false; + } + + // 4. ๆฃ€ๆŸฅๅฐๅ†™ๅญ—ๆฏ + if (requireLowercase && !containsLowercase(password)) { + return false; + } + + // 5. ๆฃ€ๆŸฅๆ•ฐๅญ— + if (requireDigit && !containsDigit(password)) { + return false; + } + + // 6. ๆฃ€ๆŸฅ็‰นๆฎŠๅญ—็ฌฆ + if (requireSpecialChar && !containsSpecialChar(password)) { + return false; + } + + return true; + } + + /** + * ๆฃ€ๆŸฅๆ˜ฏๅฆๅŒ…ๅซๅคงๅ†™ๅญ—ๆฏ + * ่ฏดๆ˜Ž๏ผš + * - ๅฐ†ๅญ—็ฌฆไธฒ่ฝฌไธบๅฐๅ†™ๅŽไธŽๅŽŸๅญ—็ฌฆไธฒๆฏ”่พƒ + * - ๅฆ‚ๆžœไธๅŒ๏ผŒ่ฏดๆ˜ŽๅŒ…ๅซๅคงๅ†™ๅญ—ๆฏ + * + * @param password ๅฏ†็  + * @return ๆ˜ฏๅฆๅŒ…ๅซๅคงๅ†™ๅญ—ๆฏ + */ + private boolean containsUppercase(String password) { + return !password.equals(password.toLowerCase()); + } + + /** + * ๆฃ€ๆŸฅๆ˜ฏๅฆๅŒ…ๅซๅฐๅ†™ๅญ—ๆฏ + * ่ฏดๆ˜Ž๏ผš + * - ๅฐ†ๅญ—็ฌฆไธฒ่ฝฌไธบๅคงๅ†™ๅŽไธŽๅŽŸๅญ—็ฌฆไธฒๆฏ”่พƒ + * - ๅฆ‚ๆžœไธๅŒ๏ผŒ่ฏดๆ˜ŽๅŒ…ๅซๅฐๅ†™ๅญ—ๆฏ + * + * @param password ๅฏ†็  + * @return ๆ˜ฏๅฆๅŒ…ๅซๅฐๅ†™ๅญ—ๆฏ + */ + private boolean containsLowercase(String password) { + return !password.equals(password.toUpperCase()); + } + + /** + * ๆฃ€ๆŸฅๆ˜ฏๅฆๅŒ…ๅซๆ•ฐๅญ— + * ่ฏดๆ˜Ž๏ผš + * - ไฝฟ็”จๆญฃๅˆ™่กจ่พพๅผๅŒน้… + * - \\d ่กจ็คบๆ•ฐๅญ— + * + * @param password ๅฏ†็  + * @return ๆ˜ฏๅฆๅŒ…ๅซๆ•ฐๅญ— + */ + private boolean containsDigit(String password) { + return password.matches(".*\\d.*"); + } + + /** + * ๆฃ€ๆŸฅๆ˜ฏๅฆๅŒ…ๅซ็‰นๆฎŠๅญ—็ฌฆ + * ่ฏดๆ˜Ž๏ผš + * - ็‰นๆฎŠๅญ—็ฌฆๅŒ…ๆ‹ฌ๏ผš!@#$%^&*()_+-=[]{}|;:',."\\|,.<>/? + * - ไฝฟ็”จๆญฃๅˆ™่กจ่พพๅผๅŒน้… + * + * @param password ๅฏ†็  + * @return ๆ˜ฏๅฆๅŒ…ๅซ็‰นๆฎŠๅญ—็ฌฆ + */ + private boolean containsSpecialChar(String password) { + return password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*"); + } +} diff --git a/src/main/java/com/aisi/template/annotation/RateLimit.java b/src/main/java/com/aisi/template/annotation/RateLimit.java new file mode 100644 index 0000000..94402aa --- /dev/null +++ b/src/main/java/com/aisi/template/annotation/RateLimit.java @@ -0,0 +1,88 @@ +package com.aisi.template.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ้™ๆตๆณจ่งฃ + * ๆ ‡ๆณจ้œ€่ฆ่ฟ›่กŒ้™ๆต็š„ๆ–นๆณ• + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็™ปๅฝ•ๆŽฅๅฃ๏ผš้˜ฒๆญขๆšดๅŠ›็ ด่งฃ + * - API ๆŽฅๅฃ๏ผš้˜ฒๆญขๆถๆ„ๅˆทๆŽฅๅฃ + * - ๆŠข่ดญๆดปๅŠจ๏ผš้˜ฒๆญขๅˆทๅ• + * + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+ * @RateLimit(
+ *     permits = 5,
+ *     seconds = 60,
+ *     limitType = LimitType.IP,
+ *     keyPrefix = "login:"
+ * )
+ * public void login(String username) { ... }
+ * 
+ * + * @author Claude + * @since 2024-04-09 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface RateLimit { + + /** + * ๆ—ถ้—ด็ช—ๅฃๅ†…ๅ…่ฎธ็š„ๆœ€ๅคง่ฏทๆฑ‚ๆ•ฐ + * - ้ป˜่ฎค๏ผš5 ๆฌก + */ + int permits() default 5; + + /** + * ๆ—ถ้—ด็ช—ๅฃ๏ผˆ็ง’๏ผ‰ + * - ้ป˜่ฎค๏ผš60 ็ง’ + */ + int seconds() default 60; + + /** + * ้™ๆต็ฑปๅž‹ + * - IP๏ผšๆŒ‰ IP ๅœฐๅ€้™ๆต + * - USER๏ผšๆŒ‰็”จๆˆทID้™ๆต๏ผˆ้œ€่ฆ็™ปๅฝ•๏ผ‰ + * - GLOBAL๏ผšๅ…จๅฑ€้™ๆต๏ผˆๆ‰€ๆœ‰็”จๆˆทๅ…ฑไบซ่ฎกๆ•ฐๅ™จ๏ผ‰ + */ + LimitType limitType() default LimitType.IP; + + /** + * Redis ้”ฎๅ‰็ผ€ + * - ้ป˜่ฎค๏ผš"rate_limit:" + * - ๅฏ่‡ชๅฎšไน‰๏ผŒๅฆ‚๏ผš"login:", "api:" ็ญ‰ + */ + String keyPrefix() default "rate_limit:"; + + /** + * ้™ๆต็ฑปๅž‹ๆžšไธพ + */ + enum LimitType { + /** + * ๆŒ‰ IP ๅœฐๅ€้™ๆต + * - ๆฏไธช IP ็‹ฌ็ซ‹่ฎกๆ•ฐ + * - ้€‚็”จไบŽ้˜ฒๆญขๆถๆ„ๅˆทๆŽฅๅฃ + */ + IP, + + /** + * ๆŒ‰็”จๆˆท ID ้™ๆต + * - ๆฏไธช็”จๆˆท็‹ฌ็ซ‹่ฎกๆ•ฐ + * - ้œ€่ฆ็”จๆˆท็™ปๅฝ• + * - ้€‚็”จไบŽ็”จๆˆทๆ“ไฝœ้™ๆต + */ + USER, + + /** + * ๅ…จๅฑ€้™ๆต + * - ๆ‰€ๆœ‰็”จๆˆทๅ…ฑไบซ่ฎกๆ•ฐๅ™จ + * - ้€‚็”จไบŽ็ณป็ปŸๆ•ดไฝ“้™ๆต + */ + GLOBAL + } +} diff --git a/src/main/java/com/aisi/template/annotation/StrongPassword.java b/src/main/java/com/aisi/template/annotation/StrongPassword.java new file mode 100644 index 0000000..34aac26 --- /dev/null +++ b/src/main/java/com/aisi/template/annotation/StrongPassword.java @@ -0,0 +1,87 @@ +package com.aisi.template.annotation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * ๅผบๅฏ†็ ้ชŒ่ฏๆณจ่งฃ + * ้ชŒ่ฏๅฏ†็ ๆ˜ฏๅฆ็ฌฆๅˆๅฎ‰ๅ…จ่ฆๆฑ‚ + * + * ้ป˜่ฎค่ฆๆฑ‚๏ผš + * - ่‡ณๅฐ‘ 8 ไธชๅญ—็ฌฆ + * - ๅŒ…ๅซ่‡ณๅฐ‘ไธ€ไธชๅคงๅ†™ๅญ—ๆฏ + * - ๅŒ…ๅซ่‡ณๅฐ‘ไธ€ไธชๅฐๅ†™ๅญ—ๆฏ + * - ๅŒ…ๅซ่‡ณๅฐ‘ไธ€ไธชๆ•ฐๅญ— + * - ๅŒ…ๅซ่‡ณๅฐ‘ไธ€ไธช็‰นๆฎŠๅญ—็ฌฆ + * + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+ * @StrongPassword(
+ *     minLength = 10,
+ *     requireUppercase = true,
+ *     requireLowercase = true,
+ *     requireDigit = true,
+ *     requireSpecialChar = true
+ * )
+ * private String password;
+ * 
+ * + * @author Claude + * @since 2024-04-09 + */ +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PasswordValidator.class) +public @interface StrongPassword { + + /** + * ้”™่ฏฏๆถˆๆฏ + * - ้ป˜่ฎค๏ผš"ๅฏ†็ ๅฟ…้กป่‡ณๅฐ‘8ไธชๅญ—็ฌฆ๏ผŒๅŒ…ๅซๅคงๅฐๅ†™ๅญ—ๆฏใ€ๆ•ฐๅญ—ๅ’Œ็‰นๆฎŠๅญ—็ฌฆ" + */ + String message() default "ๅฏ†็ ๅฟ…้กป่‡ณๅฐ‘8ไธชๅญ—็ฌฆ๏ผŒๅŒ…ๅซๅคงๅฐๅ†™ๅญ—ๆฏใ€ๆ•ฐๅญ—ๅ’Œ็‰นๆฎŠๅญ—็ฌฆ"; + + /** + * ๅˆ†็ป„ + */ + Class[] groups() default {}; + + /** + * ่ดŸ่ฝฝ + */ + Class[] payload() default {}; + + /** + * ๆœ€ๅฐๅฏ†็ ้•ฟๅบฆ + * - ้ป˜่ฎค๏ผš8 + */ + int minLength() default 8; + + /** + * ๆ˜ฏๅฆ้œ€่ฆๅคงๅ†™ๅญ—ๆฏ + * - ้ป˜่ฎค๏ผštrue + */ + boolean requireUppercase() default true; + + /** + * ๆ˜ฏๅฆ้œ€่ฆๅฐๅ†™ๅญ—ๆฏ + * - ้ป˜่ฎค๏ผštrue + */ + boolean requireLowercase() default true; + + /** + * ๆ˜ฏๅฆ้œ€่ฆๆ•ฐๅญ— + * - ้ป˜่ฎค๏ผštrue + */ + boolean requireDigit() default true; + + /** + * ๆ˜ฏๅฆ้œ€่ฆ็‰นๆฎŠๅญ—็ฌฆ + * - ้ป˜่ฎค๏ผštrue + * - ็‰นๆฎŠๅญ—็ฌฆๅŒ…ๆ‹ฌ๏ผš!@#$%^&*()_+-=[]{}|;:,.<>? + */ + boolean requireSpecialChar() default true; +} diff --git a/src/main/java/com/aisi/template/aspect/AuditLogAspect.java b/src/main/java/com/aisi/template/aspect/AuditLogAspect.java new file mode 100644 index 0000000..e665620 --- /dev/null +++ b/src/main/java/com/aisi/template/aspect/AuditLogAspect.java @@ -0,0 +1,217 @@ +package com.aisi.template.aspect; + +import com.aisi.template.annotation.AuditLog; +import com.aisi.template.service.AuditLogService; +import com.aisi.template.utils.SecurityUtils; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Objects; + +/** + * ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ‡้ข + * ่‡ชๅŠจ่ฎฐๅฝ•ๅธฆๆœ‰ @AuditLog ๆณจ่งฃ็š„ๆ–นๆณ•่ฐƒ็”จ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่‡ชๅŠจ่ฎฐๅฝ•ๆ“ไฝœๆ—ฅๅฟ— + * 2. ่ฎฐๅฝ•ๆ“ไฝœ็”จๆˆทใ€ๆ—ถ้—ดใ€IP็ญ‰ไฟกๆฏ + * 3. ่ฎฐๅฝ•ๆ“ไฝœ็ป“ๆžœ๏ผˆๆˆๅŠŸ/ๅคฑ่ดฅ๏ผ‰ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆท็™ปๅฝ•/็™ปๅ‡บ + * - ๆ•ฐๆฎๅˆ›ๅปบ/ๆ›ดๆ–ฐ/ๅˆ ้™ค + * - ๆ•ๆ„Ÿๆ“ไฝœ่ฎฟ้—ฎ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class AuditLogAspect { + + /** + * ๅฎก่ฎกๆ—ฅๅฟ—ๆœๅŠก + */ + private final AuditLogService auditLogService; + + /** + * ็Žฏ็ป•้€š็Ÿฅ๏ผš่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ— + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–่ฏทๆฑ‚ไฟกๆฏ๏ผˆๆ–นๆณ•ใ€URIใ€IP๏ผ‰ + * 2. ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * 3. ๆ‰ง่กŒ็›ฎๆ ‡ๆ–นๆณ• + * 4. ๆ•่Žทๆ‰ง่กŒ็ป“ๆžœๆˆ–ๅผ‚ๅธธ + * 5. ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ— + * + * @param joinPoint ่ฟžๆŽฅ็‚น๏ผˆ่ขซๆ‹ฆๆˆช็š„ๆ–นๆณ•๏ผ‰ + * @param auditLog ๅฎก่ฎกๆ—ฅๅฟ—ๆณจ่งฃ + * @return ๆ–นๆณ•ๆ‰ง่กŒ็ป“ๆžœ + * @throws Throwable ๆ–นๆณ•ๆ‰ง่กŒๅผ‚ๅธธ + */ + @Around("@annotation(auditLog)") + public Object logAudit(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable { + // 1. ่Žทๅ–่ฏทๆฑ‚ไฟกๆฏ + HttpServletRequest request = getRequest(); + String action = auditLog.action(); + String resource = auditLog.resource(); + + // 2. ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + Long userId = null; + String username = null; + try { + userId = SecurityUtils.getUserId(); + username = SecurityUtils.getUsername(); + } catch (Exception e) { + // ็”จๆˆทๆœช็™ปๅฝ•๏ผŒ่ทณ่ฟ‡็”จๆˆทไฟกๆฏ่Žทๅ– + } + + // 3. ๆๅ–่ฏทๆฑ‚ไฟกๆฏ + String requestMethod = request != null ? request.getMethod() : null; + String requestUri = request != null ? request.getRequestURI() : null; + String ipAddress = getClientIp(request); + String userAgent = request != null ? request.getHeader("User-Agent") : null; + + // 4. ๆ‰ง่กŒ็›ฎๆ ‡ๆ–นๆณ•ๅนถ่ฎฐๅฝ•็ป“ๆžœ + Object result = null; + String errorMessage = null; + int status = 1; // 1=ๆˆๅŠŸ๏ผŒ0=ๅคฑ่ดฅ + + try { + // 4.1 ๆ‰ง่กŒ็›ฎๆ ‡ๆ–นๆณ• + result = joinPoint.proceed(); + return result; + } catch (Exception e) { + // 4.2 ๆ•่Žทๅผ‚ๅธธ + status = 0; // ๆ ‡่ฎฐไธบๅคฑ่ดฅ + errorMessage = e.getMessage(); + throw e; + } finally { + // 5. ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ— + try { + // 5.1 ๆž„ๅปบๆ“ไฝœๆ่ฟฐ + String description = buildDescription(auditLog.description(), joinPoint.getArgs()); + + // 5.2 ๆๅ–่ต„ๆบID๏ผˆไปŽๆ–นๆณ•ๅ‚ๆ•ฐไธญ๏ผ‰ + String resourceId = extractResourceId(joinPoint.getArgs()); + + // 5.3 ไฟๅญ˜ๅฎก่ฎกๆ—ฅๅฟ— + auditLogService.log(userId, username, action, resource, resourceId, + description, requestMethod, requestUri, ipAddress, + userAgent, status, errorMessage); + } catch (Exception e) { + log.error("ไฟๅญ˜ๅฎก่ฎกๆ—ฅๅฟ—ๅคฑ่ดฅ", e); + } + } + } + + /** + * ่Žทๅ–ๅฝ“ๅ‰ HTTP ่ฏทๆฑ‚ + * + * @return HttpServletRequest ๅฏน่ฑก + */ + private HttpServletRequest getRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + return attributes != null ? attributes.getRequest() : null; + } + + /** + * ่Žทๅ–ๅฎขๆˆท็ซฏ็œŸๅฎž IP ๅœฐๅ€ + * ๆญฅ้ชค๏ผš + * 1. ๅ…ˆไปŽ X-Forwarded-For ๅคด่Žทๅ–๏ผˆไปฃ็†ๆœๅŠกๅ™จ่ฎพ็ฝฎ๏ผ‰ + * 2. ๅ†ไปŽ X-Real-IP ๅคด่Žทๅ–๏ผˆNginx ็ญ‰่ฎพ็ฝฎ๏ผ‰ + * 3. ๆœ€ๅŽไปŽ remoteAddr ่Žทๅ– + * + * @param request HTTP ่ฏทๆฑ‚ + * @return ๅฎขๆˆท็ซฏ IP ๅœฐๅ€ + */ + private String getClientIp(HttpServletRequest request) { + if (request == null) { + return null; + } + + // 1. ไปŽ X-Forwarded-For ่Žทๅ–๏ผˆๅฏ่ƒฝๆœ‰ๅคšไธช IP๏ผŒๅ–็ฌฌไธ€ไธช๏ผ‰ + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + // 2. ไปŽ X-Real-IP ่Žทๅ– + ip = request.getHeader("X-Real-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + // 3. ไปŽ remoteAddr ่Žทๅ– + ip = request.getRemoteAddr(); + } + + // 4. ๅค„็†ๅคšไธช IP ็š„ๆƒ…ๅ†ต๏ผˆX-Forwarded-For ๆ ผๅผ๏ผšๅฎขๆˆท็ซฏIP, ไปฃ็†1IP, ไปฃ็†2IP๏ผ‰ + if (ip != null && ip.contains(",")) { + ip = ip.split(",")[0].trim(); + } + + return ip; + } + + /** + * ๆž„ๅปบๆ“ไฝœๆ่ฟฐ + * ่ฏดๆ˜Ž๏ผš + * - ๆ”ฏๆŒไฝฟ็”จ {0}, {1} ็ญ‰ๅ ไฝ็ฌฆๅผ•็”จๆ–นๆณ•ๅ‚ๆ•ฐ + * - ็คบไพ‹๏ผšdescription = "ๆ›ดๆ–ฐ็”จๆˆท {0}"๏ผŒargs = ["123"] + * ็ป“ๆžœ = "ๆ›ดๆ–ฐ็”จๆˆท 123" + * + * @param template ๆ่ฟฐๆจกๆฟ + * @param args ๆ–นๆณ•ๅ‚ๆ•ฐ + * @return ๅกซๅ……ๅŽ็š„ๆ่ฟฐ + */ + private String buildDescription(String template, Object[] args) { + if (template == null || template.isEmpty()) { + return ""; + } + + String description = template; + for (int i = 0; i < args.length; i++) { + String placeholder = "{" + i + "}"; + if (description.contains(placeholder)) { + String argValue = args[i] != null ? args[i].toString() : "null"; + description = description.replace(placeholder, argValue); + } + } + + return description; + } + + /** + * ไปŽๆ–นๆณ•ๅ‚ๆ•ฐไธญๆๅ–่ต„ๆบID + * ่ฏดๆ˜Ž๏ผš + * - ๆŸฅๆ‰พ็ฌฌไธ€ไธช Longใ€Integer ๆˆ–ๆ•ฐๅญ—ๅญ—็ฌฆไธฒ็ฑปๅž‹็š„ๅ‚ๆ•ฐ + * - ไฝœไธบ่ต„ๆบID่ฎฐๅฝ• + * + * @param args ๆ–นๆณ•ๅ‚ๆ•ฐ + * @return ่ต„ๆบID + */ + private String extractResourceId(Object[] args) { + // ้ๅކๆ–นๆณ•ๅ‚ๆ•ฐ๏ผŒๆŸฅๆ‰พ ID + for (Object arg : args) { + if (arg instanceof Long) { + return arg.toString(); + } + if (arg instanceof Integer) { + return arg.toString(); + } + if (arg instanceof String) { + String str = (String) arg; + // ๆฃ€ๆŸฅๆ˜ฏๅฆไธบๆ•ฐๅญ—ๅญ—็ฌฆไธฒ๏ผˆๅฏ่ƒฝไธบ ID๏ผ‰ + if (str.matches("\\d+")) { + return str; + } + } + } + return null; + } +} diff --git a/src/main/java/com/aisi/template/aspect/RateLimitAspect.java b/src/main/java/com/aisi/template/aspect/RateLimitAspect.java new file mode 100644 index 0000000..19b4876 --- /dev/null +++ b/src/main/java/com/aisi/template/aspect/RateLimitAspect.java @@ -0,0 +1,174 @@ +package com.aisi.template.aspect; + +import com.aisi.template.annotation.RateLimit; +import com.aisi.template.exception.RateLimitExceededException; +import com.aisi.template.utils.RedisUtils; +import com.aisi.template.utils.SecurityUtils; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.concurrent.TimeUnit; + +/** + * ้™ๆตๅˆ‡้ข + * ๅŸบไบŽ Redis ๅฎž็Žฐ็š„ๅˆ†ๅธƒๅผ้™ๆตๅŠŸ่ƒฝ + * + * ๅฎž็ŽฐๅŽŸ็†๏ผš + * 1. ไฝฟ็”จ Redis ่ฎกๆ•ฐๅ™จ่ฎฐๅฝ•่ฏทๆฑ‚ๆฌกๆ•ฐ + * 2. ๅœจๆŒ‡ๅฎšๆ—ถ้—ด็ช—ๅฃๅ†…๏ผŒ่ฏทๆฑ‚ๆฌกๆ•ฐ่ถ…่ฟ‡้™ๅˆถๅˆ™ๆ‹’็ป + * 3. ๆ”ฏๆŒๆŒ‰ IPใ€็”จๆˆทID ๆˆ–ๅ…จๅฑ€้™ๆต + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็™ปๅฝ•ๆŽฅๅฃ๏ผš้˜ฒๆญขๆšดๅŠ›็ ด่งฃ + * - API ๆŽฅๅฃ๏ผš้˜ฒๆญขๆถๆ„ๅˆทๆŽฅๅฃ + * - ๆŠข่ดญๆดปๅŠจ๏ผš้˜ฒๆญขๅˆทๅ• + * + * ้™ๆต็ฎ—ๆณ•๏ผšๅ›บๅฎš็ช—ๅฃ่ฎกๆ•ฐๅ™จ + * ไผ˜็‚น๏ผšๅฎž็Žฐ็ฎ€ๅ•๏ผŒๆ€ง่ƒฝๅฅฝ + * ็ผบ็‚น๏ผš็ช—ๅฃ่พน็•Œๅฏ่ƒฝๅ‡บ็Žฐๆต้‡็ชๅขž + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class RateLimitAspect { + + /** + * Redis ๅทฅๅ…ท็ฑป + */ + private final RedisUtils redisUtils; + + /** + * ้™ๆตๆฃ€ๆŸฅๆ–นๆณ• + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎ้™ๆต็ฑปๅž‹ๆž„ๅปบ Redis ้”ฎ + * 2. ่Žทๅ–ๅฝ“ๅ‰่ฎกๆ•ฐ + * 3. ๆฃ€ๆŸฅๆ˜ฏๅฆ่ถ…่ฟ‡้™ๅˆถ + * 4. ๅฆ‚ๆžœๆœช่ถ…่ฟ‡๏ผŒๅขžๅŠ ่ฎกๆ•ฐ + * + * @param joinPoint ่ฟžๆŽฅ็‚น๏ผˆ่ขซๆ‹ฆๆˆช็š„ๆ–นๆณ•๏ผ‰ + * @param rateLimit ้™ๆตๆณจ่งฃ + * @throws RateLimitExceededException ๅฝ“่ถ…่ฟ‡้™ๆต้˜ˆๅ€ผๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Before("@annotation(rateLimit)") + public void rateLimit(JoinPoint joinPoint, RateLimit rateLimit) { + // 1. ๆž„ๅปบ Redis ้”ฎ + String key = buildKey(rateLimit); + + // 2. ๅŽŸๅญ้€’ๅขž่ฎกๆ•ฐ + Long currentCount = redisUtils.increment(key); + if (currentCount == null) { + currentCount = 1L; + } + + // 3. ้ฆ–ๆฌก่ฏทๆฑ‚่ฎพ็ฝฎๅ›บๅฎš็ช—ๅฃ่ฟ‡ๆœŸๆ—ถ้—ด + if (currentCount == 1L) { + redisUtils.expire(key, rateLimit.seconds(), TimeUnit.SECONDS); + } + + // 4. ๆฃ€ๆŸฅๆ˜ฏๅฆ่ถ…่ฟ‡้™ๅˆถ + if (currentCount > rateLimit.permits()) { + log.warn("่งฆๅ‘้™ๆต - key: {}, count: {}, limit: {}", + key, currentCount, rateLimit.permits()); + throw new RateLimitExceededException(); + } + + log.debug("้™ๆตๆฃ€ๆŸฅ้€š่ฟ‡ - key: {}, count: {}/{}", + key, currentCount, rateLimit.permits()); + } + + /** + * ๆž„ๅปบ้™ๆต Redis ้”ฎ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅ‰็ผ€๏ผˆrate_limit:๏ผ‰ + * 2. ๆ นๆฎ้™ๆต็ฑปๅž‹ๆทปๅŠ ๆ ‡่ฏ† + * - IP: rate_limit:ip:127.0.0.1 + * - USER: rate_limit:user:123 + * - GLOBAL: rate_limit:global + * + * @param rateLimit ้™ๆตๆณจ่งฃ + * @return Redis ้”ฎ + */ + private String buildKey(RateLimit rateLimit) { + StringBuilder keyBuilder = new StringBuilder(rateLimit.keyPrefix()); + + // 1. ๆ นๆฎ้™ๆต็ฑปๅž‹ๆž„ๅปบ้”ฎ + switch (rateLimit.limitType()) { + case IP: + // ๆŒ‰ IP ้™ๆต + String ip = getClientIp(); + keyBuilder.append("ip:").append(ip); + break; + + case USER: + // ๆŒ‰็”จๆˆท ID ้™ๆต + Long userId = SecurityUtils.getUserId(); + if (userId != null) { + keyBuilder.append("user:").append(userId); + } else { + // ็”จๆˆทๆœช็™ปๅฝ•๏ผŒ้™็บงไธบ IP ้™ๆต + keyBuilder.append("ip:").append(getClientIp()); + } + break; + + case GLOBAL: + // ๅ…จๅฑ€้™ๆต๏ผˆๆ‰€ๆœ‰็”จๆˆทๅ…ฑไบซ่ฎกๆ•ฐๅ™จ๏ผ‰ + keyBuilder.append("global"); + break; + } + + return keyBuilder.toString(); + } + + /** + * ่Žทๅ–ๅฎขๆˆท็ซฏ็œŸๅฎž IP ๅœฐๅ€ + * ๆญฅ้ชค๏ผš + * 1. ๅ…ˆไปŽ X-Forwarded-For ๅคด่Žทๅ–๏ผˆๅฏ่ƒฝๆœ‰ๅคšไธช IP๏ผŒๅ–็ฌฌไธ€ไธช๏ผ‰ + * 2. ๅ†ไปŽ X-Real-IP ๅคด่Žทๅ– + * 3. ๆœ€ๅŽไปŽ remoteAddr ่Žทๅ– + * + * ่ฏดๆ˜Ž๏ผš + * - X-Forwarded-For: ่ฎฐๅฝ•่ฏทๆฑ‚็ป่ฟ‡็š„ๆ‰€ๆœ‰ไปฃ็† IP + * - ๆ ผๅผ๏ผšๅฎขๆˆท็ซฏIP, ไปฃ็†1IP, ไปฃ็†2IP, ... + * - ๅ–็ฌฌไธ€ไธช IP ๅณไธบ็œŸๅฎžๅฎขๆˆท็ซฏ IP + * + * @return ๅฎขๆˆท็ซฏ IP ๅœฐๅ€ + */ + private String getClientIp() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + return "unknown"; + } + + HttpServletRequest request = attributes.getRequest(); + String ip = request.getHeader("X-Forwarded-For"); + + // 1. ไปŽ X-Forwarded-For ่Žทๅ– + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + + // 2. ไปŽ X-Real-IP ่Žทๅ– + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + // 3. ๅค„็†ๅคšไธช IP ็š„ๆƒ…ๅ†ต๏ผˆX-Forwarded-For ๅฏ่ƒฝๅŒ…ๅซๅคšไธช IP๏ผ‰ + if (ip != null && ip.contains(",")) { + // ๅ–็ฌฌไธ€ไธช IP๏ผˆ็œŸๅฎžๅฎขๆˆท็ซฏ IP๏ผ‰ + ip = ip.split(",")[0].trim(); + } + + return ip != null ? ip : "unknown"; + } +} diff --git a/src/main/java/com/aisi/template/config/JacksonConfig.java b/src/main/java/com/aisi/template/config/JacksonConfig.java index f8d2890..ba71c65 100644 --- a/src/main/java/com/aisi/template/config/JacksonConfig.java +++ b/src/main/java/com/aisi/template/config/JacksonConfig.java @@ -10,25 +10,47 @@ import org.springframework.context.annotation.Configuration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +/** + * Jackson ้…็ฝฎ็ฑป + * ้…็ฝฎ JSON ๅบๅˆ—ๅŒ–็›ธๅ…ณ่ฎพ็ฝฎ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผๅŒ–๏ผš็ปŸไธ€ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ + * 2. ็ฆ็”จๆ—ถ้—ดๆˆณ๏ผšไฝฟ็”จๅฏ่ฏป็š„ๆ—ฅๆœŸๅญ—็ฌฆไธฒ + * + * @author Claude + * @since 2024-04-09 + */ @Configuration public class JacksonConfig { + /** + * ้…็ฝฎ ObjectMapper + * ๆญฅ้ชค๏ผš + * 1. ๅˆ›ๅปบ ObjectMapper ๅฏน่ฑก + * 2. ๆณจๅ†Œ JavaTimeModule๏ผˆๆ”ฏๆŒ Java 8 ๆ—ฅๆœŸๆ—ถ้—ด็ฑปๅž‹๏ผ‰ + * 3. ้…็ฝฎ LocalDateTime ๅบๅˆ—ๅŒ–ๆ ผๅผ + * 4. ็ฆ็”จๆ—ถ้—ดๆˆณๆ ผๅผ + * + * @return ObjectMapper ๅฏน่ฑก + */ @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); - // ้…็ฝฎๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ + // 1. ้…็ฝฎๆ—ฅๆœŸๆ—ถ้—ดๆจกๅ— JavaTimeModule javaTimeModule = new JavaTimeModule(); - // ๅฎšไน‰ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ + // 2. ๅฎšไน‰ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ๏ผšyyyy-MM-dd HH:mm:ss DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter)); + // 3. ๆณจๅ†Œๆ—ฅๆœŸๆ—ถ้—ดๆจกๅ— mapper.registerModule(javaTimeModule); - // ็ฆ็”จๆ—ถ้—ดๆˆณๆ ผๅผ + // 4. ็ฆ็”จๆ—ถ้—ดๆˆณๆ ผๅผ๏ผˆไฝฟ็”จๅฏ่ฏป็š„ๆ—ฅๆœŸๅญ—็ฌฆไธฒ๏ผ‰ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } -} \ No newline at end of file +} diff --git a/src/main/java/com/aisi/template/config/JpaConfig.java b/src/main/java/com/aisi/template/config/JpaConfig.java index 4f617cd..353fc71 100644 --- a/src/main/java/com/aisi/template/config/JpaConfig.java +++ b/src/main/java/com/aisi/template/config/JpaConfig.java @@ -5,6 +5,19 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing; /** * JPA ้…็ฝฎ็ฑป + * ้…็ฝฎ JPA ๅฎก่ฎกๅŠŸ่ƒฝ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่‡ชๅŠจๅกซๅ……ๅˆ›ๅปบๆ—ถ้—ด๏ผš@CreatedDate + * 2. ่‡ชๅŠจๅกซๅ……ๆ›ดๆ–ฐๆ—ถ้—ด๏ผš@LastModifiedDate + * + * ไฝฟ็”จ่ฏดๆ˜Ž๏ผš + * - ๅœจๅฎžไฝ“็ฑปไธŠๆทปๅŠ  @EntityListeners(AuditingEntityListener.class) + * - ๅœจๅญ—ๆฎตไธŠไฝฟ็”จ @CreatedDate ๅ’Œ @LastModifiedDate ๆณจ่งฃ + * - JPA ไผš่‡ชๅŠจ็ปดๆŠค่ฟ™ไบ›ๅญ—ๆฎต็š„ๅ€ผ + * + * @author Claude + * @since 2024-04-09 */ @Configuration @EnableJpaAuditing diff --git a/src/main/java/com/aisi/template/config/OpenApiConfig.java b/src/main/java/com/aisi/template/config/OpenApiConfig.java index 813576a..0ea02b6 100644 --- a/src/main/java/com/aisi/template/config/OpenApiConfig.java +++ b/src/main/java/com/aisi/template/config/OpenApiConfig.java @@ -2,14 +2,49 @@ package com.aisi.template.config; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.List; + +/** + * OpenAPI (Swagger) ้…็ฝฎ็ฑป + * ้…็ฝฎ API ๆ–‡ๆกฃ็š„ๅฎ‰ๅ…จ่ฎค่ฏ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ้…็ฝฎ JWT Bearer ่ฎค่ฏ + * 2. ๅœจ Swagger UI ไธญๆ”ฏๆŒ JWT Token ๆต‹่ฏ• + * + * ไฝฟ็”จ่ฏดๆ˜Ž๏ผš + * 1. ่ฎฟ้—ฎ http://localhost:8080/swagger-ui.html + * 2. ็‚นๅ‡ปๅณไธŠ่ง’ "Authorize" ๆŒ‰้’ฎ + * 3. ่พ“ๅ…ฅ JWT Token๏ผˆๆ ผๅผ๏ผšBearer {token}๏ผ‰ + * 4. ็‚นๅ‡ป "Authorize" ๅฎŒๆˆ่ฎค่ฏ + * + * @author Claude + * @since 2024-04-09 + */ @Configuration @SecurityScheme( + // ๅฎ‰ๅ…จๆ–นๆกˆๅ็งฐ name = "Bearer Authentication", + // ๅฎ‰ๅ…จๆ–นๆกˆ็ฑปๅž‹๏ผšHTTP type = SecuritySchemeType.HTTP, + // Bearer ๆ ผๅผ๏ผšJWT bearerFormat = "JWT", + // ่ฎค่ฏๆ–นๆกˆ๏ผšbearer scheme = "bearer" ) public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI(@Value("${app.openapi.server-url:http://localhost:8080}") String serverUrl) { + return new OpenAPI() + .servers(List.of(new Server() + .url(serverUrl) + .description("Default server"))); + } } diff --git a/src/main/java/com/aisi/template/config/RedisConfig.java b/src/main/java/com/aisi/template/config/RedisConfig.java index 9f7445c..4b06b94 100644 --- a/src/main/java/com/aisi/template/config/RedisConfig.java +++ b/src/main/java/com/aisi/template/config/RedisConfig.java @@ -13,14 +13,45 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; +/** + * Redis ้…็ฝฎ็ฑป + * ้…็ฝฎ Redis ๅบๅˆ—ๅŒ–ๆ–นๅผ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. RedisTemplate๏ผš็”จไบŽๆ“ไฝœๅฏน่ฑก็ฑปๅž‹ๆ•ฐๆฎ + * 2. StringRedisTemplate๏ผš็”จไบŽๆ“ไฝœๅญ—็ฌฆไธฒ็ฑปๅž‹ๆ•ฐๆฎ + * + * ๅบๅˆ—ๅŒ–้…็ฝฎ๏ผš + * - Key๏ผšไฝฟ็”จ String ๅบๅˆ—ๅŒ– + * - Value๏ผšไฝฟ็”จ JSON ๅบๅˆ—ๅŒ–๏ผˆๆ”ฏๆŒ็ฑปๅž‹ไฟกๆฏ๏ผ‰ + * - HashKey๏ผšไฝฟ็”จ String ๅบๅˆ—ๅŒ– + * - HashValue๏ผšไฝฟ็”จ JSON ๅบๅˆ—ๅŒ– + * + * @author Claude + * @since 2024-04-09 + */ @Configuration public class RedisConfig { + /** + * ้…็ฝฎ RedisTemplate๏ผˆ็”จไบŽๆ“ไฝœๅฏน่ฑก๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅˆ›ๅปบ RedisTemplate ๅฏน่ฑก + * 2. ้…็ฝฎ ObjectMapper๏ผˆๆ”ฏๆŒ็ฑปๅž‹ไฟกๆฏ๏ผ‰ + * 3. ้…็ฝฎ Key ๅบๅˆ—ๅŒ–ๆ–นๅผ + * 4. ้…็ฝฎ Value ๅบๅˆ—ๅŒ–ๆ–นๅผ + * + * @param factory Redis ่ฟžๆŽฅๅทฅๅŽ‚ + * @param objectMapper Jackson ObjectMapper + * @return RedisTemplate ๅฏน่ฑก + */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory, ObjectMapper objectMapper) { + // 1. ๅˆ›ๅปบ RedisTemplate ๅฏน่ฑก RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(factory); + // 2. ้…็ฝฎ ObjectMapper๏ผˆๆ”ฏๆŒ็ฑปๅž‹ไฟกๆฏ๏ผŒ็”จไบŽๅๅบๅˆ—ๅŒ–ๆ—ถ็กฎๅฎš็ฑปๅž‹๏ผ‰ ObjectMapper redisMapper = objectMapper.copy(); redisMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); redisMapper.activateDefaultTyping( @@ -29,18 +60,35 @@ public class RedisConfig { JsonTypeInfo.As.PROPERTY ); + // 3. ้…็ฝฎๅบๅˆ—ๅŒ–ๅ™จ StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(redisMapper); + // 3.1 Key ไฝฟ็”จ String ๅบๅˆ—ๅŒ– template.setKeySerializer(stringSerializer); + // 3.2 HashKey ไฝฟ็”จ String ๅบๅˆ—ๅŒ– template.setHashKeySerializer(stringSerializer); + // 3.3 Value ไฝฟ็”จ JSON ๅบๅˆ—ๅŒ– template.setValueSerializer(jsonSerializer); + // 3.4 HashValue ไฝฟ็”จ JSON ๅบๅˆ—ๅŒ– template.setHashValueSerializer(jsonSerializer); + + // 4. ๆ‰ง่กŒๅˆๅง‹ๅŒ– template.afterPropertiesSet(); return template; } + /** + * ้…็ฝฎ StringRedisTemplate๏ผˆ็”จไบŽๆ“ไฝœๅญ—็ฌฆไธฒ๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - StringRedisTemplate ไธ“้—จ็”จไบŽๅค„็†ๅญ—็ฌฆไธฒ็ฑปๅž‹ๆ•ฐๆฎ + * - Key ๅ’Œ Value ้ƒฝไฝฟ็”จ String ๅบๅˆ—ๅŒ– + * - ้€‚็”จไบŽ็ฎ€ๅ•็š„้”ฎๅ€ผๅฏนๆ“ไฝœ + * + * @param factory Redis ่ฟžๆŽฅๅทฅๅŽ‚ + * @return StringRedisTemplate ๅฏน่ฑก + */ @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) { return new StringRedisTemplate(factory); diff --git a/src/main/java/com/aisi/template/config/SecurityConfig.java b/src/main/java/com/aisi/template/config/SecurityConfig.java index 9c3d8e0..34e8d9b 100644 --- a/src/main/java/com/aisi/template/config/SecurityConfig.java +++ b/src/main/java/com/aisi/template/config/SecurityConfig.java @@ -2,6 +2,7 @@ package com.aisi.template.config; import com.aisi.template.filter.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -19,46 +20,128 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; import java.util.List; +/** + * Spring Security ้…็ฝฎ็ฑป + * ้…็ฝฎๅฎ‰ๅ…จ่ฎค่ฏๅ’ŒๆŽˆๆƒ็›ธๅ…ณ่ฎพ็ฝฎ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. JWT ่ฎค่ฏ๏ผš้…็ฝฎ JWT ่ฟ‡ๆปคๅ™จ + * 2. CORS ้…็ฝฎ๏ผš่ทจๅŸŸ่ต„ๆบๅ…ฑไบซ้…็ฝฎ + * 3. ไผš่ฏ็ฎก็†๏ผšๆ— ็Šถๆ€ไผš่ฏ๏ผˆJWT๏ผ‰ + * 4. ๆŽˆๆƒ่ง„ๅˆ™๏ผš้…็ฝฎๅ“ชไบ›่ฏทๆฑ‚้œ€่ฆ่ฎค่ฏ + * 5. ๅฏ†็ ็ผ–็ ๏ผš้…็ฝฎ BCrypt ๅฏ†็ ็ผ–็ ๅ™จ + * + * @author Claude + * @since 2024-04-09 + */ @Configuration @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { - + /** + * JWT ่ฎค่ฏ่ฟ‡ๆปคๅ™จ + * ็”จไบŽๆ‹ฆๆˆช่ฏทๆฑ‚ๅนถ้ชŒ่ฏ JWT Token + */ private final JwtAuthenticationFilter jwtAuthenticationFilter; + /** + * ๅ…่ฎธ็š„่ทจๅŸŸๆฅๆบ + * ไปŽ้…็ฝฎๆ–‡ไปถ่ฏปๅ–๏ผŒ้ป˜่ฎค๏ผšhttp://localhost:5173, http://localhost:3000 + */ + @Value("${app.cors.allowed-origins:http://localhost:5173,http://localhost:3000}") + private List allowedOrigins; + + /** + * ้…็ฝฎๅฎ‰ๅ…จ่ฟ‡ๆปคๅ™จ้“พ + * ๆญฅ้ชค๏ผš + * 1. ็ฆ็”จ CSRF๏ผˆไฝฟ็”จ JWT ไธ้œ€่ฆ CSRF ไฟๆŠค๏ผ‰ + * 2. ้…็ฝฎ CORS + * 3. ่ฎพ็ฝฎไผš่ฏ็ฎก็†ไธบๆ— ็Šถๆ€๏ผˆJWT๏ผ‰ + * 4. ้…็ฝฎๆŽˆๆƒ่ง„ๅˆ™ + * 5. ๆทปๅŠ  JWT ่ฎค่ฏ่ฟ‡ๆปคๅ™จ + * + * @param http HttpSecurity ๅฏน่ฑก + * @return ๅฎ‰ๅ…จ่ฟ‡ๆปคๅ™จ้“พ + */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http + // 1. ็ฆ็”จ CSRF๏ผˆJWT ไธ้œ€่ฆ๏ผ‰ .csrf(AbstractHttpConfigurer::disable) + // 2. ้…็ฝฎ CORS .cors(cors -> cors.configurationSource(corsConfigurationSource())) + // 3. ่ฎพ็ฝฎไผš่ฏ็ฎก็†ไธบๆ— ็Šถๆ€ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 4. ้…็ฝฎๆŽˆๆƒ่ง„ๅˆ™ .authorizeHttpRequests(auth -> auth - .requestMatchers("/v3/api-docs/**","/swagger-ui/**", "/swagger-ui.html").permitAll() - .requestMatchers("/api/v1/user/register", "/api/v1/user/login").permitAll() + // 4.1 Swagger ๆ–‡ๆกฃ๏ผšๅ…่ฎธๅŒฟๅ่ฎฟ้—ฎ + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() + // 4.2 ็”จๆˆทๆณจๅ†Œใ€็™ปๅฝ•ๅ’Œๅˆทๆ–ฐไปค็‰Œ๏ผšๅ…่ฎธๅŒฟๅ่ฎฟ้—ฎ + .requestMatchers("/api/v1/user/register", "/api/v1/user/login", "/api/v1/user/refresh").permitAll() + // 4.3 ๅฏ†็ ้‡็ฝฎ๏ผšๅ…่ฎธๅŒฟๅ่ฎฟ้—ฎ .requestMatchers("/api/v1/user/password-reset/**").permitAll() + // 4.4 ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ๏ผš้œ€่ฆ่ฎค่ฏ .requestMatchers("/api/v1/user/info").authenticated() + // 4.5 ๅ…ถไป–ๆ‰€ๆœ‰่ฏทๆฑ‚๏ผš้œ€่ฆ่ฎค่ฏ .anyRequest().authenticated() ) + // 5. ๆทปๅŠ  JWT ่ฎค่ฏ่ฟ‡ๆปคๅ™จ๏ผˆๅœจ็”จๆˆทๅๅฏ†็ ่ฟ‡ๆปคๅ™จไน‹ๅ‰๏ผ‰ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } + /** + * ้…็ฝฎ CORS๏ผˆ่ทจๅŸŸ่ต„ๆบๅ…ฑไบซ๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅˆ›ๅปบ CORS ้…็ฝฎ + * 2. ่ฎพ็ฝฎๅ…่ฎธ็š„ๆฅๆบ + * 3. ่ฎพ็ฝฎๅ…่ฎธ็š„ HTTP ๆ–นๆณ• + * 4. ่ฎพ็ฝฎๅ…่ฎธ็š„่ฏทๆฑ‚ๅคด + * 5. ่ฎพ็ฝฎๆšด้œฒ็š„ๅ“ๅบ”ๅคด + * 6. ๅ…่ฎธๆบๅธฆๅ‡ญ่ฏ๏ผˆCookie๏ผ‰ + * 7. ่ฎพ็ฝฎ้ข„ๆฃ€่ฏทๆฑ‚็ผ“ๅญ˜ๆ—ถ้—ด + * + * @return CORS ้…็ฝฎๆบ + */ @Bean public CorsConfigurationSource corsConfigurationSource() { + // 1. ๅˆ›ๅปบ CORS ้…็ฝฎ CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOriginPatterns(List.of("*")); - configuration.setAllowedMethods(List.of("*")); - configuration.setAllowedHeaders(List.of("*")); + + // 2. ่ฎพ็ฝฎๅ…่ฎธ็š„ๆฅๆบ + configuration.setAllowedOrigins(allowedOrigins); + + // 3. ่ฎพ็ฝฎๅ…่ฎธ็š„ HTTP ๆ–นๆณ• + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + + // 4. ่ฎพ็ฝฎๅ…่ฎธ็š„่ฏทๆฑ‚ๅคด + configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "Accept", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers")); + + // 5. ่ฎพ็ฝฎๆšด้œฒ็š„ๅ“ๅบ”ๅคด + configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type")); + + // 6. ๅ…่ฎธๆบๅธฆๅ‡ญ่ฏ๏ผˆCookie๏ผ‰ configuration.setAllowCredentials(true); - configuration.setExposedHeaders(List.of("*")); + + // 7. ่ฎพ็ฝฎ้ข„ๆฃ€่ฏทๆฑ‚็ผ“ๅญ˜ๆ—ถ้—ด๏ผˆ1 ๅฐๆ—ถ๏ผ‰ configuration.setMaxAge(3600L); + // 8. ๆณจๅ†Œ CORS ้…็ฝฎ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } + /** + * ้…็ฝฎๅฏ†็ ็ผ–็ ๅ™จ + * ่ฏดๆ˜Ž๏ผš + * - ไฝฟ็”จ BCrypt ็ฎ—ๆณ•ๅฏนๅฏ†็ ่ฟ›่กŒๅŠ ๅฏ† + * - ๆฏๆฌกๅŠ ๅฏ†้ƒฝไผš็”ŸๆˆไธๅŒ็š„ๅ“ˆๅธŒๅ€ผ๏ผˆ่‡ชๅธฆ็›ๅ€ผ๏ผ‰ + * - ๅผบๅบฆๅ› ๅญ้ป˜่ฎคไธบ 10 + * + * @return BCrypt ๅฏ†็ ็ผ–็ ๅ™จ + */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/main/java/com/aisi/template/config/WebConfig.java b/src/main/java/com/aisi/template/config/WebConfig.java index cea7ae7..471571b 100644 --- a/src/main/java/com/aisi/template/config/WebConfig.java +++ b/src/main/java/com/aisi/template/config/WebConfig.java @@ -6,17 +6,41 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +/** + * Web MVC ้…็ฝฎ็ฑป + * ้…็ฝฎๆ‹ฆๆˆชๅ™จๅ’Œๅ…ถไป– Web ็›ธๅ…ณ่ฎพ็ฝฎ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๆ‹ฆๆˆชๅ™จ้…็ฝฎ๏ผšๆณจๅ†Œๆ—ฅๅฟ—ๆ‹ฆๆˆชๅ™จ + * + * @author Claude + * @since 2024-04-09 + */ @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + /** + * ๆ—ฅๅฟ—ๆ‹ฆๆˆชๅ™จ + * ็”จไบŽ่ฎฐๅฝ•่ฏทๆฑ‚ๅ’Œๅ“ๅบ”ไฟกๆฏ + */ private final LoggingInterceptor loggingInterceptor; - + /** + * ้…็ฝฎๆ‹ฆๆˆชๅ™จ + * ๆญฅ้ชค๏ผš + * 1. ๆณจๅ†Œๆ—ฅๅฟ—ๆ‹ฆๆˆชๅ™จ + * 2. ๆ‹ฆๆˆชๆ‰€ๆœ‰่ฏทๆฑ‚๏ผˆ/**๏ผ‰ + * 3. ๅฏ้…็ฝฎๆŽ’้™ค่ทฏๅพ„ + * + * @param registry ๆ‹ฆๆˆชๅ™จๆณจๅ†Œ่กจ + */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loggingInterceptor) + // 1. ๆ‹ฆๆˆชๆ‰€ๆœ‰่ฏทๆฑ‚ .addPathPatterns("/**") + // 2. ๆŽ’้™ค่ทฏๅพ„๏ผˆๅฏๅœจๆญคๆทปๅŠ ้œ€่ฆๆŽ’้™ค็š„่ทฏๅพ„๏ผ‰ .excludePathPatterns(); } } diff --git a/src/main/java/com/aisi/template/constants/AppConstants.java b/src/main/java/com/aisi/template/constants/AppConstants.java new file mode 100644 index 0000000..cf0b90a --- /dev/null +++ b/src/main/java/com/aisi/template/constants/AppConstants.java @@ -0,0 +1,134 @@ +package com.aisi.template.constants; + +/** + * ๅบ”็”จ็บงๅธธ้‡็ฑป + * ๅฎšไน‰็ณป็ปŸไธญ็š„้€š็”จๅธธ้‡ + * + * ไธป่ฆๅˆ†็ฑป๏ผš + * 1. ๅˆ†้กต้…็ฝฎ๏ผš้ป˜่ฎค้กตๅคงๅฐใ€ๆœ€ๅคง้กตๅคงๅฐ + * 2. ๆ—ฅๆœŸๆ—ถ้—ด๏ผšๆ ผๅผใ€ๆ—ถๅŒบ + * 3. ็”จๆˆท็Šถๆ€๏ผšๅฏ็”จใ€็ฆ็”จ + * 4. ่œๅ•็ฑปๅž‹๏ผš็›ฎๅฝ•ใ€่œๅ•ใ€ๆŒ‰้’ฎ + * 5. ๅฏ†็ ้‡็ฝฎ๏ผš่ฟ‡ๆœŸๆ—ถ้—ดใ€ๅ†ทๅดๆ—ถ้—ด + * + * @author Claude + * @since 2024-04-09 + */ +public class AppConstants { + + /** + * ้ป˜่ฎคๅˆ†้กตๅคงๅฐ + * - ๆฏ้กต่ฟ”ๅ›ž็š„่ฎฐๅฝ•ๆ•ฐ + */ + public static final int DEFAULT_PAGE_SIZE = 10; + + /** + * ๆœ€ๅคงๅˆ†้กตๅคงๅฐ + * - ้˜ฒๆญขไธ€ๆฌกๆŸฅ่ฏข่ฟ‡ๅคšๆ•ฐๆฎ + */ + public static final int MAX_PAGE_SIZE = 100; + + /** + * ๆ—ฅๆœŸๆ—ถ้—ดๆ ผๅผ๏ผˆJSON ๅบๅˆ—ๅŒ–๏ผ‰ + * - ๆ ผๅผ๏ผšyyyy-MM-dd HH:mm:ss + * - ็คบไพ‹๏ผš2024-04-09 14:30:00 + */ + public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + /** + * ไธญๅ›ฝๆ—ถๅŒบ + * - ไธœๅ…ซๅŒบ๏ผˆUTC+8๏ผ‰ + */ + public static final String TIME_ZONE_CHINA = "Asia/Shanghai"; + + /** + * ้ป˜่ฎค่ฏญ่จ€็Žฏๅขƒ + * - ็ฎ€ไฝ“ไธญๆ–‡ + */ + public static final String DEFAULT_LOCALE = "zh_CN"; + + /** + * ๅฎก่ฎกๆ—ฅๅฟ—ไฟ็•™ๅคฉๆ•ฐ + * - ่ถ…่ฟ‡ๆญคๅคฉๆ•ฐ็š„ๆ—ฅๅฟ—ๅฐ†่ขซๅˆ ้™ค + */ + public static final int AUDIT_LOG_RETENTION_DAYS = 90; + + /** + * ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆๅˆ†้’Ÿ๏ผ‰ + * - ้ชŒ่ฏ็ ็š„ๆœ‰ๆ•ˆๆœŸ + */ + public static final int PASSWORD_RESET_CODE_EXPIRE_MINUTES = 10; + + /** + * ๅฏ†็ ้‡็ฝฎ่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * - ๅŒไธ€้‚ฎ็ฎฑไธคๆฌก่ฏทๆฑ‚็š„ๆœ€ๅฐ้—ด้š” + */ + public static final int PASSWORD_RESET_REQUEST_COOLDOWN_SECONDS = 60; + + /** + * ๅฏ†็ ้‡็ฝฎๆœ€ๅคงๅฐ่ฏ•ๆฌกๆ•ฐ + * - ้ชŒ่ฏ็ ้ชŒ่ฏๅคฑ่ดฅ่ถ…่ฟ‡ๆญคๆฌกๆ•ฐๅŽๅคฑๆ•ˆ + */ + public static final int PASSWORD_RESET_MAX_ATTEMPTS = 5; + + /** + * ็”จๆˆท็Šถๆ€ๅธธ้‡ + */ + public static final class UserStatus { + /** + * ็”จๆˆท็ฆ็”จ + */ + public static final int DISABLED = 0; + + /** + * ็”จๆˆทๅฏ็”จ + */ + public static final int ENABLED = 1; + } + + /** + * ่œๅ•็ฑปๅž‹ๅธธ้‡ + */ + public static final class MenuType { + /** + * ็›ฎๅฝ• + * - ็”จไบŽๅˆ†็ป„๏ผŒไธๅฏนๅบ”ๅ…ทไฝ“้กต้ข + */ + public static final int DIRECTORY = 1; + + /** + * ่œๅ• + * - ๅฏนๅบ”ๅ…ทไฝ“้กต้ข + */ + public static final int PAGE = 2; + + /** + * ๆŒ‰้’ฎ + * - ้กต้ขๅ†…็š„ๆ“ไฝœๆŒ‰้’ฎ + */ + public static final int BUTTON = 3; + } + + /** + * ่œๅ•ๅฏ่งๆ€งๅธธ้‡ + */ + public static final class MenuVisibility { + /** + * ้š่— + */ + public static final int HIDDEN = 0; + + /** + * ๅฏ่ง + */ + public static final int VISIBLE = 1; + } + + /** + * ็งๆœ‰ๆž„้€ ๅ‡ฝๆ•ฐ + * - ้˜ฒๆญขๅฎžไพ‹ๅŒ– + */ + private AppConstants() { + // ้˜ฒๆญขๅฎžไพ‹ๅŒ– + } +} diff --git a/src/main/java/com/aisi/template/constants/SecurityConstants.java b/src/main/java/com/aisi/template/constants/SecurityConstants.java new file mode 100644 index 0000000..1a77f23 --- /dev/null +++ b/src/main/java/com/aisi/template/constants/SecurityConstants.java @@ -0,0 +1,103 @@ +package com.aisi.template.constants; + +/** + * ๅฎ‰ๅ…จ็›ธๅ…ณๅธธ้‡็ฑป + * ๅฎšไน‰็ณป็ปŸไธญ็š„ๅฎ‰ๅ…จ็›ธๅ…ณๅธธ้‡ + * + * ไธป่ฆๅˆ†็ฑป๏ผš + * 1. Token ็›ธๅ…ณ๏ผšToken ๅ‰็ผ€ใ€่ฟ‡ๆœŸๆ—ถ้—ด + * 2. Redis ้”ฎๅ‰็ผ€๏ผš้ป‘ๅๅ•ใ€ๅˆทๆ–ฐไปค็‰Œใ€้™ๆตใ€็™ปๅฝ•ๅฐ่ฏ• + * 3. ๅฎ‰ๅ…จ้…็ฝฎ๏ผšๆœ€ๅคงๅคฑ่ดฅๆฌกๆ•ฐใ€้”ๅฎšๆ—ถ้•ฟ + * + * @author Claude + * @since 2024-04-09 + */ +public class SecurityConstants { + + /** + * JWT Token ่ฏทๆฑ‚ๅคดๅ็งฐ + * - HTTP ่ฏทๆฑ‚ๅคด๏ผšAuthorization + * - ็คบไพ‹๏ผšAuthorization: Bearer {token} + */ + public static final String TOKEN_HEADER = "Authorization"; + + /** + * JWT Token ๅ‰็ผ€ + * - Bearer ่ฎค่ฏๆ–นๆกˆ + * - ็คบไพ‹๏ผšBearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + */ + public static final String TOKEN_PREFIX = "Bearer "; + + /** + * JWT Token ็ฑปๅž‹ + * - ็”จไบŽๆ–‡ๆกฃๆ ‡่ฏ† + * - ๅ€ผ๏ผšBearer + */ + public static final String TOKEN_TYPE = "Bearer"; + + /** + * Token ้ป‘ๅๅ• Redis ้”ฎๅ‰็ผ€ + * - ๆ ผๅผ๏ผštoken:blacklist:{jti} + * - jti๏ผšJWT ID๏ผˆJWT ๅ”ฏไธ€ๆ ‡่ฏ†๏ผ‰ + * - ็คบไพ‹๏ผštoken:blacklist:abc123-def456 + */ + public static final String REDIS_KEY_BLACKLIST_PREFIX = "token:blacklist:"; + + /** + * Refresh Token Redis ้”ฎๅ‰็ผ€ + * - ๆ ผๅผ๏ผšrefresh_token:{tokenHash} + * - tokenHash๏ผšToken ็š„ SHA-256 ๅ“ˆๅธŒๅ€ผ + * - ็คบไพ‹๏ผšrefresh_token:a1b2c3... + */ + public static final String REDIS_KEY_REFRESH_TOKEN_PREFIX = "refresh_token:"; + + /** + * ้™ๆต Redis ้”ฎๅ‰็ผ€ + * - ๆ ผๅผ๏ผšrate_limit:{type}:{identifier} + * - type๏ผšip๏ผˆๆŒ‰ IP๏ผ‰ใ€user๏ผˆๆŒ‰็”จๆˆท๏ผ‰ใ€global๏ผˆๅ…จๅฑ€๏ผ‰ + * - identifier๏ผšIP ๅœฐๅ€ใ€็”จๆˆทID ็ญ‰ + * - ็คบไพ‹๏ผšrate_limit:ip:192.168.1.1 + */ + public static final String REDIS_KEY_RATE_LIMIT_PREFIX = "rate_limit:"; + + /** + * ็™ปๅฝ•ๅฐ่ฏ• Redis ้”ฎๅ‰็ผ€ + * - ๆ ผๅผ๏ผšlogin_attempts:{username} + * - ็คบไพ‹๏ผšlogin_attempts:user01 + */ + public static final String REDIS_KEY_LOGIN_ATTEMPTS_PREFIX = "login_attempts:"; + + /** + * ้ป˜่ฎค Access Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * - 1 ๅฐๆ—ถ = 3600 ็ง’ + */ + public static final long DEFAULT_ACCESS_TOKEN_EXPIRATION = 3600; + + /** + * ้ป˜่ฎค Refresh Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * - 7 ๅคฉ = 604800 ็ง’ + */ + public static final long DEFAULT_REFRESH_TOKEN_EXPIRATION = 604800; + + /** + * ๆœ€ๅคง็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ + * - ่ถ…่ฟ‡ๆญคๆฌกๆ•ฐๅŽ้”ๅฎš่ดฆๆˆท + * - ้ป˜่ฎค๏ผš5 ๆฌก + */ + public static final int MAX_LOGIN_ATTEMPTS = 5; + + /** + * ้ป˜่ฎค่ดฆๆˆท้”ๅฎšๆ—ถ้•ฟ๏ผˆๅˆ†้’Ÿ๏ผ‰ + * - ่ฟž็ปญๅคฑ่ดฅ่พพๅˆฐ้˜ˆๅ€ผๅŽ็š„้”ๅฎšๆ—ถ้•ฟ + * - ้ป˜่ฎค๏ผš30 ๅˆ†้’Ÿ + */ + public static final int DEFAULT_LOCK_DURATION_MINUTES = 30; + + /** + * ็งๆœ‰ๆž„้€ ๅ‡ฝๆ•ฐ + * - ้˜ฒๆญขๅฎžไพ‹ๅŒ– + */ + private SecurityConstants() { + // ้˜ฒๆญขๅฎžไพ‹ๅŒ– + } +} diff --git a/src/main/java/com/aisi/template/controller/MenuController.java b/src/main/java/com/aisi/template/controller/MenuController.java new file mode 100644 index 0000000..0fb3dcb --- /dev/null +++ b/src/main/java/com/aisi/template/controller/MenuController.java @@ -0,0 +1,213 @@ +package com.aisi.template.controller; + +import com.aisi.template.domain.RestBean; +import com.aisi.template.domain.CustomUserDetails; +import com.aisi.template.domain.dto.MenuDto; +import com.aisi.template.domain.vo.MenuVo; +import com.aisi.template.service.SysMenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * ่œๅ•็ฎก็†ๆŽงๅˆถๅ™จ + * ๆไพ›่œๅ•็š„ๅขžๅˆ ๆ”นๆŸฅๅ’Œๆ ‘ๅฝข็ป“ๆž„ๆŽฅๅฃ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่œๅ•ๅŸบๆœฌๆ“ไฝœ๏ผšๅˆ›ๅปบใ€ๆ›ดๆ–ฐใ€ๅˆ ้™คใ€ๆŸฅ่ฏข + * 2. ่œๅ•ๆ ‘็ป“ๆž„๏ผš่Žทๅ–ๅฎŒๆ•ด็š„่œๅ•ๆ ‘ + * 3. ็”จๆˆท่œๅ•๏ผšๆ นๆฎ็”จๆˆท่ง’่‰ฒ่Žทๅ–ๅฏ่ง่œๅ• + * 4. ๅฑ‚็บงๆŸฅ่ฏข๏ผšๆ นๆฎ็ˆถ่Š‚็‚นๆŸฅ่ฏขๅญ่œๅ• + * + * ๆƒ้™่ฏดๆ˜Ž๏ผš + * - ๆ‰€ๆœ‰ๆŽฅๅฃ้ƒฝ้œ€่ฆ็™ปๅฝ• + * - ็ฎก็†ๆŽฅๅฃ้œ€่ฆ็›ธๅบ”ๆƒ้™๏ผˆๅฆ‚ menu:createใ€menu:update๏ผ‰ + * - ็”จๆˆท่œๅ•ๆŽฅๅฃ็™ปๅฝ•ๅŽๅณๅฏ่ฎฟ้—ฎ + * + * ่œๅ•็ฑปๅž‹๏ผš + * - 1๏ผš็›ฎๅฝ•๏ผˆไธ€็บง่œๅ•๏ผ‰ + * - 2๏ผš้กต้ข๏ผˆไบŒ็บง่œๅ•๏ผ‰ + * - 3๏ผšๆŒ‰้’ฎ๏ผˆ้กต้ขๅ†…็š„ๆ“ไฝœๆŒ‰้’ฎ๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Tag(name = "่œๅ•็ฎก็†", description = "่œๅ•็š„ๅขžๅˆ ๆ”นๆŸฅๅ’Œๆ ‘ๅฝข็ป“ๆž„ๆŽฅๅฃ") +@RestController +@RequestMapping("/api/v1/menus") +@RequiredArgsConstructor +@SecurityRequirement(name = "bearerAuth") +public class MenuController { + + /** + * ่œๅ•ๆœๅŠก + * ๅค„็†่œๅ•็›ธๅ…ณ็š„ไธšๅŠก้€ป่พ‘ + */ + private final SysMenuService menuService; + + /** + * ๅˆ›ๅปบ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ็ˆถ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆ ก้ชŒ่œๅ•ๅ็งฐๆ˜ฏๅฆ้‡ๅค + * 3. ๅˆ›ๅปบ่œๅ•ๅนถไฟๅญ˜ + * + * ๆƒ้™๏ผš้œ€่ฆ menu:create ๆƒ้™ + * + * @param menuDto ่œๅ•ไฟกๆฏ๏ผˆๅ็งฐใ€็ฑปๅž‹ใ€็ˆถ่Š‚็‚นใ€่ทฏๅพ„็ญ‰๏ผ‰ + * @return ๅˆ›ๅปบๅŽ็š„่œๅ•ไฟกๆฏ + */ + @PostMapping + @Operation(summary = "ๅˆ›ๅปบ่œๅ•", description = "ๅˆ›ๅปบๆ–ฐ็š„่œๅ•้กน") + @PreAuthorize("hasAuthority('menu:create')") + public RestBean create(@Valid @RequestBody MenuDto menuDto) { + MenuVo menu = menuService.create(menuDto); + return RestBean.success(menu); + } + + /** + * ๆ›ดๆ–ฐ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๅฆ‚ๆžœไฟฎๆ”น็ˆถ่Š‚็‚น๏ผŒๆฃ€ๆŸฅๆ–ฐ็ˆถ่Š‚็‚นๆ˜ฏๅฆๅญ˜ๅœจ + * 3. ๆ›ดๆ–ฐ่œๅ•ไฟกๆฏ + * + * ๆƒ้™๏ผš้œ€่ฆ menu:update ๆƒ้™ + * + * @param id ่œๅ•ID + * @param menuDto ่œๅ•ไฟกๆฏ + * @return ๆ›ดๆ–ฐๅŽ็š„่œๅ•ไฟกๆฏ + */ + @PutMapping("/{id}") + @Operation(summary = "ๆ›ดๆ–ฐ่œๅ•", description = "ๆ›ดๆ–ฐ่œๅ•็š„ๅ็งฐใ€่ทฏๅพ„็ญ‰ไฟกๆฏ") + @PreAuthorize("hasAuthority('menu:update')") + public RestBean update(@PathVariable Long id, @Valid @RequestBody MenuDto menuDto) { + MenuVo menu = menuService.update(id, menuDto); + return RestBean.success(menu); + } + + /** + * ๅˆ ้™ค่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰ๅญ่œๅ• + * 3. ๅˆ ้™ค่œๅ•๏ผˆๅญ่œๅ•ไผšไธ€ๅนถๅˆ ้™ค๏ผ‰ + * + * ๆƒ้™๏ผš้œ€่ฆ menu:delete ๆƒ้™ + * + * ๆณจๆ„๏ผš + * - ๅˆ ้™ค็›ฎๅฝ•ไผšไธ€ๅนถๅˆ ้™คๅ…ถไธ‹ๆ‰€ๆœ‰ๅญ่œๅ• + * - ๅปบ่ฎฎๅ…ˆๅˆ ้™คๅญ่œๅ•ๅ†ๅˆ ้™ค็ˆถ่œๅ• + * + * @param id ่œๅ•ID + * @return ๆˆๅŠŸๅ“ๅบ” + */ + @DeleteMapping("/{id}") + @Operation(summary = "ๅˆ ้™ค่œๅ•", description = "ๅˆ ้™คๆŒ‡ๅฎš็š„่œๅ•ๅŠๅ…ถๅญ่œๅ•") + @PreAuthorize("hasAuthority('menu:delete')") + public RestBean delete(@PathVariable Long id) { + menuService.delete(id); + return RestBean.success(); + } + + /** + * ๆ นๆฎID่Žทๅ–่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่œๅ•ๅŸบๆœฌไฟกๆฏ + * 2. ่ฟ”ๅ›ž่ฏฆ็ป†ไฟกๆฏ + * + * ๆƒ้™๏ผš้œ€่ฆ menu:read ๆƒ้™ + * + * @param id ่œๅ•ID + * @return ่œๅ•่ฏฆ็ป†ไฟกๆฏ + */ + @GetMapping("/{id}") + @Operation(summary = "่Žทๅ–่œๅ•่ฏฆๆƒ…", description = "ๆ นๆฎID่Žทๅ–่œๅ•่ฏฆ็ป†ไฟกๆฏ") + @PreAuthorize("hasAuthority('menu:read')") + public RestBean getById(@PathVariable Long id) { + MenuVo menu = menuService.getById(id); + return RestBean.success(menu); + } + + /** + * ่Žทๅ–ๅฎŒๆ•ด่œๅ•ๆ ‘ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่œๅ• + * 2. ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„๏ผˆ็ˆถๅญๅ…ณ็ณป๏ผ‰ + * 3. ่ฟ”ๅ›žๅฎŒๆ•ด็š„่œๅ•ๆ ‘ + * + * ๆƒ้™๏ผš้œ€่ฆ menu:list ๆƒ้™ + * + * ๆณจๆ„๏ผš + * - ่ฟ”ๅ›ž็š„ๆ˜ฏๅฎŒๆ•ดๆ ‘ๅฝข็ป“ๆž„๏ผŒๅŒ…ๅซ็›ฎๅฝ•ใ€้กต้ขใ€ๆŒ‰้’ฎ + * - ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * @return ่œๅ•ๆ ‘ๅˆ—่กจ + */ + @GetMapping("/tree") + @Operation(summary = "่Žทๅ–่œๅ•ๆ ‘", description = "่Žทๅ–ๅฎŒๆ•ด็š„่œๅ•ๆ ‘ๅฝข็ป“ๆž„") + @PreAuthorize("hasAuthority('menu:list')") + public RestBean> getMenuTree() { + List menus = menuService.getMenuTree(); + return RestBean.success(menus); + } + + /** + * ่Žทๅ–ๅฝ“ๅ‰็”จๆˆท็š„่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ไปŽ่ฎค่ฏไฟกๆฏไธญ่Žทๅ–็”จๆˆทID + * 2. ๆŸฅ่ฏข็”จๆˆท็š„่ง’่‰ฒ + * 3. ๆŸฅ่ฏข่ง’่‰ฒๅ…ณ่”็š„่œๅ• + * 4. ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„๏ผˆๅชๅŒ…ๅซ็›ฎๅฝ•ๅ’Œ้กต้ข๏ผ‰ + * 5. ่ฟ‡ๆปคไธๅฏ่งๅ’Œ็ฆ็”จ็š„่œๅ• + * + * ๆณจๆ„๏ผš + * - ๅช่ฟ”ๅ›ž็›ฎๅฝ•ๅ’Œ้กต้ข๏ผŒไธๅŒ…ๅซๆŒ‰้’ฎ + * - ๆ นๆฎ็”จๆˆท่ง’่‰ฒๅŠจๆ€็”Ÿๆˆ + * - ็”จไบŽๅ‰็ซฏๅŠจๆ€ๆธฒๆŸ“ๅฏผ่ˆช่œๅ• + * + * @param authentication Spring Security ่ฎค่ฏไฟกๆฏ + * @return ็”จๆˆทๅฏ่ง็š„่œๅ•ๆ ‘ + */ + @GetMapping("/user") + @Operation(summary = "่Žทๅ–ๅฝ“ๅ‰็”จๆˆท่œๅ•", description = "่Žทๅ–ๅฝ“ๅ‰็”จๆˆทๅฏ่ง็š„่œๅ•ๆ ‘") + public RestBean> getUserMenus(Authentication authentication) { + // 1. ไปŽ่ฎค่ฏไฟกๆฏ่Žทๅ–็”จๆˆทID + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + Long userId = userDetails.getId(); + // 2. ๆŸฅ่ฏข็”จๆˆท่œๅ•๏ผˆๅทฒ่ฟ‡ๆปค๏ผ‰ + List menus = menuService.getUserMenus(userId); + return RestBean.success(menus); + } + + /** + * ๆ นๆฎ็ˆถ่Š‚็‚นID่Žทๅ–ๅญ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆŒ‡ๅฎš็ˆถ่Š‚็‚นไธ‹็š„ๆ‰€ๆœ‰็›ดๆŽฅๅญ่œๅ• + * 2. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * 3. ่ฟ”ๅ›žๅˆ—่กจ + * + * ๆƒ้™๏ผš้œ€่ฆ menu:list ๆƒ้™ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅŠจๆ€ๅŠ ่ฝฝๅญ่œๅ• + * - ๅ‰็ซฏๆ ‘ๅฝขๆŽงไปถๅฑ•ๅผ€ๆ—ถๅŠ ่ฝฝ + * + * @param parentId ็ˆถ่Š‚็‚นID๏ผˆ0 ่กจ็คบๆ น่Š‚็‚น๏ผ‰ + * @return ๅญ่œๅ•ๅˆ—่กจ + */ + @GetMapping("/parent/{parentId}") + @Operation(summary = "่Žทๅ–ๅญ่œๅ•", description = "ๆ นๆฎ็ˆถ่Š‚็‚นID่Žทๅ–็›ดๆŽฅๅญ่œๅ•") + @PreAuthorize("hasAuthority('menu:list')") + public RestBean> getByParentId(@PathVariable Long parentId) { + List menus = menuService.getByParentId(parentId); + return RestBean.success(menus); + } +} diff --git a/src/main/java/com/aisi/template/controller/PermissionController.java b/src/main/java/com/aisi/template/controller/PermissionController.java new file mode 100644 index 0000000..351d015 --- /dev/null +++ b/src/main/java/com/aisi/template/controller/PermissionController.java @@ -0,0 +1,132 @@ +package com.aisi.template.controller; + +import com.aisi.template.domain.RestBean; +import com.aisi.template.domain.vo.PermissionVo; +import com.aisi.template.service.SysPermissionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * ๆƒ้™็ฎก็†ๆŽงๅˆถๅ™จ + * ๆไพ›ๆƒ้™็š„ๆŸฅ่ฏขๆŽฅๅฃ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰ๆƒ้™ + * 2. ๆ นๆฎID่Žทๅ–ๆƒ้™่ฏฆๆƒ… + * 3. ๆŒ‰่ต„ๆบ็ฑปๅž‹ๆŸฅ่ฏขๆƒ้™ + * 4. ๆŒ‰ๆ“ไฝœ็ฑปๅž‹ๆŸฅ่ฏขๆƒ้™ + * + * ๆƒ้™่ฏดๆ˜Ž๏ผš + * - ๆ‰€ๆœ‰ๆŽฅๅฃ้ƒฝ้œ€่ฆ็™ปๅฝ• + * - ้œ€่ฆ็›ธๅบ”ๆƒ้™๏ผˆๅฆ‚ permission:list๏ผ‰ + * + * ๆƒ้™็ผ–็ ่ง„่Œƒ๏ผš + * - ๆ ผๅผ๏ผš{่ต„ๆบ}:{ๆ“ไฝœ} + * - ็คบไพ‹๏ผšuser:create, role:update, menu:delete + * + * @author Claude + * @since 2024-04-09 + */ +@Tag(name = "ๆƒ้™็ฎก็†", description = "็ณป็ปŸๆƒ้™ๆŸฅ่ฏขๆŽฅๅฃ") +@RestController +@RequestMapping("/api/v1/permissions") +@RequiredArgsConstructor +@SecurityRequirement(name = "bearerAuth") +public class PermissionController { + + /** + * ๆƒ้™ๆœๅŠก + * ๅค„็†ๆƒ้™็›ธๅ…ณ็š„ไธšๅŠก้€ป่พ‘ + */ + private final SysPermissionService permissionService; + + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰็Šถๆ€ไธบๅฏ็”จ็š„ๆƒ้™ + * 2. ๆŒ‰่ต„ๆบๅ’Œๆ“ไฝœๆŽ’ๅบ + * 3. ่ฟ”ๅ›žๅˆ—่กจ + * + * ๆƒ้™๏ผš้œ€่ฆ permission:list ๆƒ้™ + * + * @return ๆ‰€ๆœ‰ๆƒ้™ๅˆ—่กจ + */ + @GetMapping + @Operation(summary = "่Žทๅ–ๆ‰€ๆœ‰ๆƒ้™", description = "่Žทๅ–็ณป็ปŸไธญๆ‰€ๆœ‰ๅฏ็”จ็š„ๆƒ้™") + @PreAuthorize("hasAuthority('permission:list')") + public RestBean> getAllPermissions() { + List permissions = permissionService.getAllPermissions(); + return RestBean.success(permissions); + } + + /** + * ๆ นๆฎID่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆƒ้™ๅŸบๆœฌไฟกๆฏ + * 2. ่ฟ”ๅ›ž่ฏฆ็ป†ไฟกๆฏ + * + * ๆƒ้™๏ผš้œ€่ฆ permission:list ๆƒ้™ + * + * @param id ๆƒ้™ID + * @return ๆƒ้™่ฏฆ็ป†ไฟกๆฏ + */ + @GetMapping("/{id}") + @Operation(summary = "่Žทๅ–ๆƒ้™่ฏฆๆƒ…", description = "ๆ นๆฎID่Žทๅ–ๆƒ้™่ฏฆ็ป†ไฟกๆฏ") + @PreAuthorize("hasAuthority('permission:list')") + public RestBean getById(@PathVariable Long id) { + PermissionVo permission = permissionService.getById(id); + return RestBean.success(permission); + } + + /** + * ๆŒ‰่ต„ๆบ็ฑปๅž‹่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎ่ต„ๆบ็ฑปๅž‹๏ผˆๅฆ‚ userใ€role๏ผ‰ๆŸฅ่ฏข + * 2. ่ฟ”ๅ›ž่ฏฅ่ต„ๆบ็š„ๆ‰€ๆœ‰ๆ“ไฝœๆƒ้™ + * + * ๆƒ้™๏ผš้œ€่ฆ permission:list ๆƒ้™ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅˆ†้…ๆƒ้™ๆ—ถ๏ผŒๆŒ‰่ต„ๆบ็ฑปๅž‹ๅฑ•็คบ + * - ๆƒ้™็ฎก็†้กต้ขๆŒ‰่ต„ๆบๅˆ†็ป„ๆ˜พ็คบ + * + * @param resource ่ต„ๆบ็ฑปๅž‹๏ผˆๅฆ‚ userใ€roleใ€permissionใ€menu๏ผ‰ + * @return ่ฏฅ่ต„ๆบ็š„ๆ‰€ๆœ‰ๆƒ้™ + */ + @GetMapping("/resource/{resource}") + @Operation(summary = "ๆŒ‰่ต„ๆบ่Žทๅ–ๆƒ้™", description = "่Žทๅ–ๆŒ‡ๅฎš่ต„ๆบ็š„ๆ‰€ๆœ‰ๆƒ้™") + @PreAuthorize("hasAuthority('permission:list')") + public RestBean> getByResource(@PathVariable String resource) { + List permissions = permissionService.getByResource(resource); + return RestBean.success(permissions); + } + + /** + * ๆŒ‰ๆ“ไฝœ็ฑปๅž‹่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎๆ“ไฝœ็ฑปๅž‹๏ผˆๅฆ‚ createใ€updateใ€delete๏ผ‰ๆŸฅ่ฏข + * 2. ่ฟ”ๅ›ž่ฏฅๆ“ไฝœ็š„ๆ‰€ๆœ‰่ต„ๆบๆƒ้™ + * + * ๆƒ้™๏ผš้œ€่ฆ permission:list ๆƒ้™ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๆŸฅ่ฏขๆ‰€ๆœ‰ๅˆ›ๅปบๆƒ้™ + * - ๆŸฅ่ฏขๆ‰€ๆœ‰ๅˆ ้™คๆƒ้™ + * + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆcreateใ€readใ€updateใ€deleteใ€list๏ผ‰ + * @return ่ฏฅๆ“ไฝœ็š„ๆ‰€ๆœ‰ๆƒ้™ + */ + @GetMapping("/action/{action}") + @Operation(summary = "ๆŒ‰ๆ“ไฝœ่Žทๅ–ๆƒ้™", description = "่Žทๅ–ๆŒ‡ๅฎšๆ“ไฝœ็š„ๆ‰€ๆœ‰ๆƒ้™") + @PreAuthorize("hasAuthority('permission:list')") + public RestBean> getByAction(@PathVariable String action) { + List permissions = permissionService.getByAction(action); + return RestBean.success(permissions); + } +} diff --git a/src/main/java/com/aisi/template/controller/RoleController.java b/src/main/java/com/aisi/template/controller/RoleController.java new file mode 100644 index 0000000..f5c66b8 --- /dev/null +++ b/src/main/java/com/aisi/template/controller/RoleController.java @@ -0,0 +1,229 @@ +package com.aisi.template.controller; + +import com.aisi.template.domain.RestBean; +import com.aisi.template.domain.dto.RoleDto; +import com.aisi.template.domain.dto.RoleQueryDto; +import com.aisi.template.domain.dto.PageResult; +import com.aisi.template.domain.vo.RoleVo; +import com.aisi.template.service.SysRoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * ่ง’่‰ฒ็ฎก็†ๆŽงๅˆถๅ™จ + * ๆไพ›่ง’่‰ฒ็š„ๅขžๅˆ ๆ”นๆŸฅๅ’Œๆƒ้™ๅˆ†้…ๆŽฅๅฃ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่ง’่‰ฒๅŸบๆœฌๆ“ไฝœ๏ผšๅˆ›ๅปบใ€ๆ›ดๆ–ฐใ€ๅˆ ้™คใ€ๆŸฅ่ฏข + * 2. ๆƒ้™็ฎก็†๏ผšไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ใ€่Žทๅ–่ง’่‰ฒๆƒ้™ + * 3. ๅˆ†้กตๆŸฅ่ฏข๏ผšๆ”ฏๆŒๆกไปถๆŸฅ่ฏขๅ’Œๅˆ†้กต + * + * ๆƒ้™่ฏดๆ˜Ž๏ผš + * - ๆ‰€ๆœ‰ๆŽฅๅฃ้ƒฝ้œ€่ฆ็™ปๅฝ• + * - ้œ€่ฆ็›ธๅบ”็š„่ง’่‰ฒๆƒ้™๏ผˆๅฆ‚ role:createใ€role:update๏ผ‰ + * + * ่ง’่‰ฒ็ผ–็ ่ง„่Œƒ๏ผš + * - ROLE_SUPER_ADMIN๏ผš่ถ…็บง็ฎก็†ๅ‘˜ + * - ROLE_ADMIN๏ผš็ฎก็†ๅ‘˜ + * - ROLE_USER๏ผšๆ™ฎ้€š็”จๆˆท + * + * @author Claude + * @since 2024-04-09 + */ +@Tag(name = "่ง’่‰ฒ็ฎก็†", description = "่ง’่‰ฒ็š„ๅขžๅˆ ๆ”นๆŸฅๅ’Œๆƒ้™ๅˆ†้…ๆŽฅๅฃ") +@RestController +@RequestMapping("/api/v1/roles") +@RequiredArgsConstructor +@SecurityRequirement(name = "bearerAuth") +public class RoleController { + + /** + * ่ง’่‰ฒๆœๅŠก + * ๅค„็†่ง’่‰ฒ็›ธๅ…ณ็š„ไธšๅŠก้€ป่พ‘ + */ + private final SysRoleService roleService; + + /** + * ๅˆ›ๅปบ่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ่ง’่‰ฒ็ผ–็ ๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + * 2. ๆ ก้ชŒ่ง’่‰ฒๅ็งฐๆ˜ฏๅฆๅˆๆณ• + * 3. ๅˆ›ๅปบ่ง’่‰ฒๅนถไฟๅญ˜ + * + * ๆƒ้™๏ผš้œ€่ฆ role:create ๆƒ้™ + * + * @param roleDto ่ง’่‰ฒไฟกๆฏ๏ผˆ็ผ–็ ใ€ๅ็งฐใ€ๆ่ฟฐ็ญ‰๏ผ‰ + * @return ๅˆ›ๅปบๅŽ็š„่ง’่‰ฒไฟกๆฏ + */ + @PostMapping + @Operation(summary = "ๅˆ›ๅปบ่ง’่‰ฒ", description = "ๅˆ›ๅปบๆ–ฐ็š„่ง’่‰ฒ๏ผŒ้œ€่ฆๆไพ›่ง’่‰ฒ็ผ–็ ๅ’Œๅ็งฐ") + @PreAuthorize("hasAuthority('role:create')") + public RestBean create(@Valid @RequestBody RoleDto roleDto) { + RoleVo role = roleService.create(roleDto); + return RestBean.success(role); + } + + /** + * ๆ›ดๆ–ฐ่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๅฆ‚ๆžœไฟฎๆ”น่ง’่‰ฒ็ผ–็ ๏ผŒๆฃ€ๆŸฅๆ–ฐ็ผ–็ ๆ˜ฏๅฆๅ†ฒ็ช + * 3. ๆ›ดๆ–ฐ่ง’่‰ฒไฟกๆฏ + * + * ๆƒ้™๏ผš้œ€่ฆ role:update ๆƒ้™ + * + * @param id ่ง’่‰ฒID + * @param roleDto ่ง’่‰ฒไฟกๆฏ + * @return ๆ›ดๆ–ฐๅŽ็š„่ง’่‰ฒไฟกๆฏ + */ + @PutMapping("/{id}") + @Operation(summary = "ๆ›ดๆ–ฐ่ง’่‰ฒ", description = "ๆ›ดๆ–ฐ่ง’่‰ฒ็š„ๅ็งฐใ€ๆ่ฟฐ็ญ‰ไฟกๆฏ") + @PreAuthorize("hasAuthority('role:update')") + public RestBean update(@PathVariable Long id, @Valid @RequestBody RoleDto roleDto) { + RoleVo role = roleService.update(id, roleDto); + return RestBean.success(role); + } + + /** + * ๅˆ ้™ค่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅทฒๅˆ†้…็ป™็”จๆˆท + * 3. ๅˆ ้™ค่ง’่‰ฒ๏ผˆๅ…ณ่”ๅ…ณ็ณปไผš่‡ชๅŠจ็บง่”ๅˆ ้™ค๏ผ‰ + * + * ๆƒ้™๏ผš้œ€่ฆ role:delete ๆƒ้™ + * + * ๆณจๆ„๏ผš + * - ็ณป็ปŸๅ†…็ฝฎ่ง’่‰ฒ๏ผˆๅฆ‚ ROLE_ADMIN๏ผ‰ไธๅ…่ฎธๅˆ ้™ค + * - ๅทฒๅˆ†้…็ป™็”จๆˆท็š„่ง’่‰ฒ้œ€่ฆๅ…ˆ่งฃ้™คๅ…ณ่” + * + * @param id ่ง’่‰ฒID + * @return ๆˆๅŠŸๅ“ๅบ” + */ + @DeleteMapping("/{id}") + @Operation(summary = "ๅˆ ้™ค่ง’่‰ฒ", description = "ๅˆ ้™คๆŒ‡ๅฎš็š„่ง’่‰ฒ") + @PreAuthorize("hasAuthority('role:delete')") + public RestBean delete(@PathVariable Long id) { + roleService.delete(id); + return RestBean.success(); + } + + /** + * ๆ นๆฎID่Žทๅ–่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒๅŸบๆœฌไฟกๆฏ + * 2. ๅŠ ่ฝฝๅ…ณ่”็š„ๆƒ้™ๅˆ—่กจ + * 3. ่ฟ”ๅ›ž่ฏฆ็ป†ไฟกๆฏ + * + * ๆƒ้™๏ผš้œ€่ฆ role:read ๆƒ้™ + * + * @param id ่ง’่‰ฒID + * @return ่ง’่‰ฒ่ฏฆ็ป†ไฟกๆฏ๏ผˆๅŒ…ๅซๆƒ้™ๅˆ—่กจ๏ผ‰ + */ + @GetMapping("/{id}") + @Operation(summary = "่Žทๅ–่ง’่‰ฒ่ฏฆๆƒ…", description = "ๆ นๆฎID่Žทๅ–่ง’่‰ฒๅŠๅ…ถๆƒ้™ไฟกๆฏ") + @PreAuthorize("hasAuthority('role:read')") + public RestBean getById(@PathVariable Long id) { + RoleVo role = roleService.getById(id); + return RestBean.success(role); + } + + /** + * ่Žทๅ–ๆ‰€ๆœ‰่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่ง’่‰ฒ + * 2. ๅŠ ่ฝฝๆฏไธช่ง’่‰ฒ็š„ๆƒ้™ + * 3. ่ฟ”ๅ›žๅˆ—่กจ + * + * ๆƒ้™๏ผš้œ€่ฆ role:list ๆƒ้™ + * + * @return ๆ‰€ๆœ‰่ง’่‰ฒๅˆ—่กจ๏ผˆๅŒ…ๅซๆƒ้™๏ผ‰ + */ + @GetMapping + @Operation(summary = "่Žทๅ–ๆ‰€ๆœ‰่ง’่‰ฒ", description = "่Žทๅ–็ณป็ปŸไธญๆ‰€ๆœ‰่ง’่‰ฒๅŠๅ…ถๆƒ้™") + @PreAuthorize("hasAuthority('role:list')") + public RestBean> getAllRoles() { + List roles = roleService.getAllRoles(); + return RestBean.success(roles); + } + + /** + * ๅˆ†้กตๆŸฅ่ฏข่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎๆŸฅ่ฏขๆกไปถๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏข + * 2. ๆ”ฏๆŒๆŒ‰่ง’่‰ฒ็ผ–็ ใ€ๅ็งฐใ€็Šถๆ€็ญ›้€‰ + * 3. ่ฟ”ๅ›žๅˆ†้กต็ป“ๆžœ + * + * ๆƒ้™๏ผš้œ€่ฆ role:list ๆƒ้™ + * + * @param queryDto ๆŸฅ่ฏขๆกไปถ๏ผˆ่ง’่‰ฒ็ผ–็ ใ€ๅ็งฐใ€็Šถๆ€๏ผ‰ + * @param page ้กต็ ๏ผˆไปŽ 0 ๅผ€ๅง‹๏ผ‰ + * @param size ๆฏ้กตๅคงๅฐ + * @return ๅˆ†้กต่ง’่‰ฒๅˆ—่กจ + */ + @PostMapping("/query") + @Operation(summary = "ๅˆ†้กตๆŸฅ่ฏข่ง’่‰ฒ", description = "ๆ”ฏๆŒๆกไปถ็ญ›้€‰ๅ’Œๅˆ†้กต") + @PreAuthorize("hasAuthority('role:list')") + public RestBean> queryRoles( + @RequestBody RoleQueryDto queryDto, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + PageResult result = roleService.queryRoles(queryDto, page, size); + return RestBean.success(result); + } + + /** + * ไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ‰€ๆœ‰ๆƒ้™IDๆ˜ฏๅฆๅญ˜ๅœจ + * 3. ๆธ…็ฉบ่ง’่‰ฒๅŽŸๆœ‰ๆƒ้™ + * 4. ๆทปๅŠ ๆ–ฐ็š„ๆƒ้™ + * + * ๆƒ้™๏ผš้œ€่ฆ role:assign-permission ๆƒ้™ + * + * ๆณจๆ„๏ผš + * - ๆƒ้™ๅ˜ๆ›ดๅŽ้œ€่ฆ็”จๆˆท้‡ๆ–ฐ็™ปๅฝ•ๆ‰่ƒฝ็”Ÿๆ•ˆ + * - ๅปบ่ฎฎๅœจ้ž้ซ˜ๅณฐๆœŸๆ“ไฝœ + * + * @param id ่ง’่‰ฒID + * @param permissionIds ๆƒ้™IDๅˆ—่กจ + * @return ๆˆๅŠŸๅ“ๅบ” + */ + @PostMapping("/{id}/permissions") + @Operation(summary = "ไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™", description = "ๆ‰น้‡ๅˆ†้…ๆƒ้™็ป™่ง’่‰ฒ") + @PreAuthorize("hasAuthority('role:assign-permission')") + public RestBean assignPermissions( + @PathVariable Long id, + @RequestBody List permissionIds) { + roleService.assignPermissions(id, permissionIds); + return RestBean.success(); + } + + /** + * ่Žทๅ–่ง’่‰ฒ็š„ๆƒ้™IDๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒๅŸบๆœฌไฟกๆฏ + * 2. ๆๅ–ๆ‰€ๆœ‰ๅ…ณ่”็š„ๆƒ้™ID + * 3. ่ฟ”ๅ›žIDๅˆ—่กจ + * + * ๆƒ้™๏ผš้œ€่ฆ role:read ๆƒ้™ + * + * @param id ่ง’่‰ฒID + * @return ๆƒ้™IDๅˆ—่กจ + */ + @GetMapping("/{id}/permissions") + @Operation(summary = "่Žทๅ–่ง’่‰ฒๆƒ้™", description = "่Žทๅ–่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆ‰€ๆœ‰ๆƒ้™ID") + @PreAuthorize("hasAuthority('role:read')") + public RestBean> getRolePermissionIds(@PathVariable Long id) { + List permissionIds = roleService.getRolePermissionIds(id); + return RestBean.success(permissionIds); + } +} diff --git a/src/main/java/com/aisi/template/controller/UserController.java b/src/main/java/com/aisi/template/controller/UserController.java index 541a4a8..ed09670 100644 --- a/src/main/java/com/aisi/template/controller/UserController.java +++ b/src/main/java/com/aisi/template/controller/UserController.java @@ -4,84 +4,352 @@ import com.aisi.template.domain.RestBean; import com.aisi.template.domain.dto.PageResult; import com.aisi.template.domain.dto.PasswordResetConfirmDto; import com.aisi.template.domain.dto.PasswordResetRequestDto; +import com.aisi.template.domain.dto.RefreshTokenDto; import com.aisi.template.domain.dto.UserDto; import com.aisi.template.domain.dto.UserQueryDto; import com.aisi.template.domain.dto.UserRoleUpdateDto; import com.aisi.template.domain.dto.UserStatusUpdateDto; +import com.aisi.template.domain.vo.LoginResponseVo; import com.aisi.template.domain.vo.UserVo; import com.aisi.template.service.PasswordResetService; +import com.aisi.template.service.TokenService; import com.aisi.template.service.UserService; +import com.aisi.template.utils.JwtUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; +import java.util.Date; + +/** + * ็”จๆˆท็ฎก็†ๆŽงๅˆถๅ™จ + * ๆไพ›็”จๆˆทๆณจๅ†Œใ€็™ปๅฝ•ใ€ไฟกๆฏ็ฎก็†็ญ‰็›ธๅ…ณๆŽฅๅฃ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ็”จๆˆท่ฎค่ฏ๏ผšๆณจๅ†Œใ€็™ปๅฝ•ใ€็™ปๅ‡บใ€Token ๅˆทๆ–ฐ + * 2. ๅฏ†็ ็ฎก็†๏ผšๅฏ†็ ้‡็ฝฎ๏ผˆ้‚ฎไปถ้ชŒ่ฏ็ ๏ผ‰ + * 3. ็”จๆˆทไฟกๆฏๆŸฅ่ฏข๏ผš่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * 4. ็”จๆˆท็ฎก็†๏ผš็”จๆˆทๅˆ—่กจๆŸฅ่ฏข๏ผˆ้œ€่ฆๆƒ้™๏ผ‰ใ€็Šถๆ€ๆ›ดๆ–ฐใ€่ง’่‰ฒๅˆ†้… + * + * ๆƒ้™่ฏดๆ˜Ž๏ผš + * - ็”จๆˆทๆณจๅ†Œใ€็™ปๅฝ•ใ€ๅฏ†็ ้‡็ฝฎ๏ผšๆ— ้œ€่ฎค่ฏ + * - ่Žทๅ–็”จๆˆทไฟกๆฏ๏ผš้œ€่ฆ็™ปๅฝ• + * - ็”จๆˆท็ฎก็†ๆŽฅๅฃ๏ผš้œ€่ฆ็›ธๅบ”ๆƒ้™๏ผˆๅฆ‚ user:listใ€user:update๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ @RestController @RequestMapping("/api/v1/user") @RequiredArgsConstructor -@Tag(name = "็”จๆˆทๆŽฅๅฃ") +@Tag(name = "็”จๆˆทๆŽฅๅฃ", description = "็”จๆˆทๆณจๅ†Œใ€็™ปๅฝ•ใ€ไฟกๆฏ็ฎก็†็ญ‰็›ธๅ…ณๆŽฅๅฃ") public class UserController { + /** + * ็”จๆˆทๆœๅŠก + * ๅค„็†็”จๆˆท็›ธๅ…ณ็š„ไธšๅŠก้€ป่พ‘ + */ private final UserService userService; + + /** + * ๅฏ†็ ้‡็ฝฎๆœๅŠก + * ๅค„็†ๅฏ†็ ้‡็ฝฎ็š„ไธšๅŠก้€ป่พ‘ + */ private final PasswordResetService passwordResetService; + /** + * Token ๆœๅŠก + * ๅค„็† Refresh Token ๅ’Œ้ป‘ๅๅ•็ฎก็† + */ + private final TokenService tokenService; + + /** + * JWT ๅทฅๅ…ท็ฑป + * ็”จไบŽ็”Ÿๆˆๅ’Œ่งฃๆž JWT Token + */ + private final JwtUtil jwtUtil; + + /** + * ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ SecurityContext ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * 2. ่ฟ”ๅ›ž็”จๆˆท่ฏฆ็ป†ไฟกๆฏ + * + * ๆƒ้™๏ผš้œ€่ฆ็™ปๅฝ•๏ผˆBearer Token๏ผ‰ + * + * @return ็”จๆˆทไฟกๆฏ่ง†ๅ›พๅฏน่ฑก + */ @GetMapping("info") - @Operation(summary = "็”จๆˆทไฟกๆฏ") + @Operation(summary = "่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ", description = "่Žทๅ–ๅทฒ็™ปๅฝ•็”จๆˆท็š„่ฏฆ็ป†ไฟกๆฏ") @SecurityRequirement(name = "Bearer Authentication") public RestBean getUserInfo() { return userService.getUserInfo(); } + /** + * ็”จๆˆทๆณจๅ†Œ + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ็”จๆˆทๅใ€้‚ฎ็ฎฑๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + * 2. ๆ ก้ชŒๅฏ†็ ๅผบๅบฆ๏ผˆๅฟ…้กปๅŒ…ๅซๅคงๅฐๅ†™ๅญ—ๆฏใ€ๆ•ฐๅญ—ใ€็‰นๆฎŠๅญ—็ฌฆ๏ผ‰ + * 3. ๅˆ›ๅปบ็”จๆˆทๅนถๅˆ†้…้ป˜่ฎค่ง’่‰ฒ๏ผˆROLE_USER๏ผ‰ + * 4. ็”Ÿๆˆ Access Token ๅ’Œ Refresh Token + * + * ๆณจๆ„๏ผš + * - ๅฏ†็ ไผšไฝฟ็”จ BCrypt ๅŠ ๅฏ†ๅญ˜ๅ‚จ + * - ๆ–ฐ็”จๆˆท้ป˜่ฎคๅˆ†้… ROLE_USER ่ง’่‰ฒ + * + * @param userDto ็”จๆˆทๆณจๅ†Œไฟกๆฏ๏ผˆ็”จๆˆทๅใ€ๅฏ†็ ใ€้‚ฎ็ฎฑ๏ผ‰ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซ Token ๅ’Œ็”จๆˆทไฟกๆฏ๏ผ‰ + */ @PostMapping("register") - @Operation(summary = "็”จๆˆทๆณจๅ†Œ") - public RestBean register(@Valid @RequestBody UserDto userDto) { + @Operation(summary = "็”จๆˆทๆณจๅ†Œ", description = "ๅˆ›ๅปบๆ–ฐ็”จๆˆท่ดฆๅทๅนถ่ฟ”ๅ›ž็™ปๅฝ•ๅ‡ญ่ฏ") + public RestBean register(@Valid @RequestBody UserDto userDto) { return userService.register(userDto); } + /** + * ็”จๆˆท็™ปๅฝ• + * ๆญฅ้ชค๏ผš + * 1. ้ชŒ่ฏ็”จๆˆทๅๅ’Œๅฏ†็  + * 2. ๆฃ€ๆŸฅ่ดฆๆˆท็Šถๆ€๏ผˆๆ˜ฏๅฆ่ขซ็ฆ็”จใ€้”ๅฎš๏ผ‰ + * 3. ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ๏ผˆ่ถ…่ฟ‡้˜ˆๅ€ผๅˆ™้”ๅฎš่ดฆๆˆท๏ผ‰ + * 4. ็”Ÿๆˆ Access Token ๅ’Œ Refresh Token + * 5. ๅ‘้€็™ปๅฝ•ๆถˆๆฏๅˆฐ MQ + * + * ๆณจๆ„๏ผš + * - ่ฟž็ปญ็™ปๅฝ•ๅคฑ่ดฅ 5 ๆฌกไผš้”ๅฎš่ดฆๆˆท 30 ๅˆ†้’Ÿ + * - ๅฏ†็ ้”™่ฏฏไนŸไผš่ขซ่ฎฐๅฝ• + * + * @param userDto ็™ปๅฝ•ไฟกๆฏ๏ผˆ็”จๆˆทๅใ€ๅฏ†็ ๏ผ‰ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซ Token ๅ’Œ็”จๆˆทไฟกๆฏ๏ผ‰ + */ @PostMapping("login") - @Operation(summary = "็”จๆˆท็™ปๅฝ•") - RestBean login(@Valid @RequestBody UserDto userDto) { + @Operation(summary = "็”จๆˆท็™ปๅฝ•", description = "ไฝฟ็”จ็”จๆˆทๅๅฏ†็ ็™ปๅฝ•๏ผŒ่ฟ”ๅ›ž่ฎฟ้—ฎไปค็‰Œ") + RestBean login(@Valid @RequestBody UserDto userDto) { return userService.login(userDto); } + /** + * ็”จๆˆท็™ปๅ‡บ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ่ฏทๆฑ‚ๅคด่Žทๅ–ๅฝ“ๅ‰ Token + * 2. ๆๅ– Token ็š„ JTI๏ผˆJWT ID๏ผ‰ + * 3. ๅฐ† Token ๅŠ ๅ…ฅ้ป‘ๅๅ•๏ผˆๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ดๅ†…ๆœ‰ๆ•ˆ๏ผ‰ + * 4. ๆ’ค้”€็”จๆˆทๆ‰€ๆœ‰ Refresh Token + * + * ๆƒ้™๏ผš้œ€่ฆ็™ปๅฝ•๏ผˆBearer Token๏ผ‰ + * + * @param request HTTP ่ฏทๆฑ‚ๅฏน่ฑก๏ผˆ็”จไบŽ่Žทๅ– Token๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ + @PostMapping("logout") + @Operation(summary = "็”จๆˆท็™ปๅ‡บ", description = "้€€ๅ‡บ็™ปๅฝ•๏ผŒไฝฟๅฝ“ๅ‰ Token ๅคฑๆ•ˆ") + @SecurityRequirement(name = "Bearer Authentication") + public RestBean logout(HttpServletRequest request) { + // 1. ไปŽ่ฏทๆฑ‚ๅคดๆๅ– Token + String token = extractToken(request); + if (token != null) { + // 2. ๆๅ– Token ็š„ JTI๏ผˆJWT ๅ”ฏไธ€ๆ ‡่ฏ†๏ผ‰ + String jti = jwtUtil.extractJti(token); + if (jti != null) { + // 3. ่ฎก็ฎ—ๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ด + long remainingSeconds = getRemainingExpiration(token); + // 4. ๅฐ† Token ๅŠ ๅ…ฅ้ป‘ๅๅ•๏ผˆ่ฟ‡ๆœŸๅŽ่‡ชๅŠจๅˆ ้™ค๏ผ‰ + tokenService.addTokenToBlacklist(jti, remainingSeconds); + } + + // 5. ๆ’ค้”€ๅฝ“ๅ‰็”จๆˆท็š„ๆ‰€ๆœ‰ Refresh Token + Long userId = jwtUtil.extractUserId(token); + if (userId != null) { + tokenService.revokeAllUserTokens(userId); + } + } + return RestBean.success(); + } + + /** + * ๅˆทๆ–ฐ่ฎฟ้—ฎไปค็‰Œ + * ๆญฅ้ชค๏ผš + * 1. ้ชŒ่ฏ Refresh Token ๆ˜ฏๅฆๆœ‰ๆ•ˆ + * 2. ็”Ÿๆˆๆ–ฐ็š„ Access Token + * 3. ็”Ÿๆˆๆ–ฐ็š„ Refresh Token๏ผˆToken ่ฝฎๆขๆœบๅˆถ๏ผ‰ + * 4. ๆ’ค้”€ๆ—ง็š„ Refresh Token + * + * ๆณจๆ„๏ผš + * - Refresh Token ไฝฟ็”จไธ€ๆฌกๅŽๅณๅคฑๆ•ˆ + * - ไฝฟ็”จ Refresh Token ่ฝฎๆขๆ้ซ˜ๅฎ‰ๅ…จๆ€ง + * + * @param refreshTokenDto ๅˆทๆ–ฐไปค็‰Œ่ฏทๆฑ‚ + * @return ๆ–ฐ็š„็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซๆ–ฐ็š„ Token๏ผ‰ + */ + @PostMapping("refresh") + @Operation(summary = "ๅˆทๆ–ฐ่ฎฟ้—ฎไปค็‰Œ", description = "ไฝฟ็”จ Refresh Token ่Žทๅ–ๆ–ฐ็š„ Access Token") + public RestBean refreshToken(@Valid @RequestBody RefreshTokenDto refreshTokenDto) { + LoginResponseVo response = tokenService.refreshToken(refreshTokenDto.getRefreshToken()); + return RestBean.success(response); + } + + /** + * ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ้‚ฎ็ฎฑๆ ผๅผ + * 2. ๆฃ€ๆŸฅ่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด๏ผˆ้˜ฒๆญข้ข‘็น่ฏทๆฑ‚๏ผ‰ + * 3. ็”Ÿๆˆ 6 ไฝๆ•ฐๅญ—้ชŒ่ฏ็  + * 4. ๅ‘้€้ชŒ่ฏ็ ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ + * 5. ้ชŒ่ฏ็ ๅ“ˆๅธŒๅŽๅญ˜ๅ‚จๅˆฐ Redis๏ผˆ10ๅˆ†้’Ÿๆœ‰ๆ•ˆ๏ผ‰ + * + * ๆณจๆ„๏ผš + * - ๅŒไธ€้‚ฎ็ฎฑ 60 ็ง’ๅ†…ๅช่ƒฝ่ฏทๆฑ‚ไธ€ๆฌก + * - ้ชŒ่ฏ็ ๆœ€ๅคšๅฐ่ฏ• 5 ๆฌก + * + * @param requestDto ๅฏ†็ ้‡็ฝฎ่ฏทๆฑ‚๏ผˆ้‚ฎ็ฎฑ๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ @PostMapping("password-reset/request") - @Operation(summary = "ๅ‘้€ๆ‰พๅ›žๅฏ†็ ้ชŒ่ฏ็ ") + @Operation(summary = "ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ", description = "ๅ‘้€้ชŒ่ฏ็ ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ") public RestBean sendPasswordResetCode(@Valid @RequestBody PasswordResetRequestDto requestDto) { return passwordResetService.sendResetCode(requestDto); } + /** + * ไฝฟ็”จ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็  + * ๆญฅ้ชค๏ผš + * 1. ้ชŒ่ฏ้‚ฎ็ฎฑๅ’Œ้ชŒ่ฏ็ ๆ˜ฏๅฆๅŒน้… + * 2. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * 3. ้ชŒ่ฏ้ชŒ่ฏ็ ๅฐ่ฏ•ๆฌกๆ•ฐ + * 4. ๆ ก้ชŒๆ–ฐๅฏ†็ ๅผบๅบฆ + * 5. ๆ›ดๆ–ฐ็”จๆˆทๅฏ†็  + * 6. ๅˆ ้™คๅทฒไฝฟ็”จ็š„้ชŒ่ฏ็  + * + * @param confirmDto ๅฏ†็ ้‡็ฝฎ็กฎ่ฎค๏ผˆ้‚ฎ็ฎฑใ€้ชŒ่ฏ็ ใ€ๆ–ฐๅฏ†็ ๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ @PostMapping("password-reset/confirm") - @Operation(summary = "้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็ ") + @Operation(summary = "้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็ ", description = "ไฝฟ็”จ้‚ฎ็ฎฑ้ชŒ่ฏ็ ้‡็ฝฎ็”จๆˆทๅฏ†็ ") public RestBean confirmPasswordReset(@Valid @RequestBody PasswordResetConfirmDto confirmDto) { return passwordResetService.resetPassword(confirmDto); } + /** + * ๅˆ†้กต่Žทๅ–็”จๆˆทๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎๆŸฅ่ฏขๆกไปถๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏข + * 2. ๆ”ฏๆŒๆŒ‰็”จๆˆทๅ/้‚ฎ็ฎฑๆจก็ณŠๆœ็ดข + * 3. ๆ”ฏๆŒๆŒ‰็Šถๆ€็ญ›้€‰ + * 4. ่ฟ”ๅ›žๅˆ†้กต็ป“ๆžœ + * + * ๆƒ้™๏ผš้œ€่ฆ user:list ๆƒ้™ + * + * @param queryDto ๆŸฅ่ฏขๆกไปถ๏ผˆๅ…ณ้”ฎ่ฏใ€็Šถๆ€็ญ‰๏ผ‰ + * @return ๅˆ†้กต็”จๆˆทๅˆ—่กจ + */ @GetMapping("list") - @PreAuthorize("hasRole('ADMIN')") - @Operation(summary = "ๅˆ†้กต่Žทๅ–็”จๆˆทๅˆ—่กจ") + @PreAuthorize("hasAuthority('user:list')") + @Operation(summary = "ๅˆ†้กต่Žทๅ–็”จๆˆทๅˆ—่กจ", description = "ๆŸฅ่ฏข็”จๆˆทๅˆ—่กจ๏ผŒๆ”ฏๆŒๅˆ†้กตๅ’Œๆกไปถ็ญ›้€‰") @SecurityRequirement(name = "Bearer Authentication") public RestBean> getUserList(UserQueryDto queryDto) { return userService.getUserList(queryDto); } + /** + * ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ็ฆๆญขไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„็Šถๆ€ + * 3. ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€๏ผˆๅฏ็”จ/็ฆ็”จ๏ผ‰ + * + * ๆƒ้™๏ผš้œ€่ฆ user:update ๆƒ้™ + * + * ๆณจๆ„๏ผš + * - ไธ่ƒฝ็ฆ็”จๅฝ“ๅ‰็™ปๅฝ•็š„็”จๆˆท + * - ็ฆ็”จๅŽ็”จๆˆทๆ— ๆณ•็™ปๅฝ• + * + * @param userId ็”จๆˆทID + * @param updateDto ็Šถๆ€ๆ›ดๆ–ฐ่ฏทๆฑ‚ + * @return ๆ›ดๆ–ฐๅŽ็š„็”จๆˆทไฟกๆฏ + */ @PutMapping("{userId}/status") - @PreAuthorize("hasRole('ADMIN')") - @Operation(summary = "ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€") + @PreAuthorize("hasAuthority('user:update')") + @Operation(summary = "ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€", description = "ๅฏ็”จๆˆ–็ฆ็”จ็”จๆˆท่ดฆๅท") @SecurityRequirement(name = "Bearer Authentication") public RestBean updateUserStatus(@PathVariable Long userId, @Valid @RequestBody UserStatusUpdateDto updateDto) { return userService.updateUserStatus(userId, updateDto); } + /** + * ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ็ฆๆญขไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ + * 3. ้ชŒ่ฏ่ง’่‰ฒIDๆ˜ฏๅฆๅญ˜ๅœจ + * 4. ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒๅ…ณ็ณป + * + * ๆƒ้™๏ผš้œ€่ฆ user:update ๆƒ้™ + * + * ๆณจๆ„๏ผš + * - ไธ่ƒฝไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ + * - ่ง’่‰ฒๅ˜ๆ›ดๅŽ้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ•ๆ‰่ƒฝ็”Ÿๆ•ˆ + * + * @param userId ็”จๆˆทID + * @param updateDto ่ง’่‰ฒๆ›ดๆ–ฐ่ฏทๆฑ‚๏ผˆ่ง’่‰ฒIDๅˆ—่กจ๏ผ‰ + * @return ๆ›ดๆ–ฐๅŽ็š„็”จๆˆทไฟกๆฏ + */ @PutMapping("{userId}/role") - @PreAuthorize("hasRole('ADMIN')") - @Operation(summary = "ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒ") + @PreAuthorize("hasAuthority('user:update')") + @Operation(summary = "ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒ", description = "ไธบ็”จๆˆทๅˆ†้…ๆˆ–็งป้™ค่ง’่‰ฒ") @SecurityRequirement(name = "Bearer Authentication") public RestBean updateUserRole(@PathVariable Long userId, @Valid @RequestBody UserRoleUpdateDto updateDto) { return userService.updateUserRole(userId, updateDto); } + + /** + * ไปŽ HTTP ่ฏทๆฑ‚ไธญๆๅ– JWT Token + * ๆญฅ้ชค๏ผš + * 1. ไปŽ Authorization ๅคด่Žทๅ–ๅ€ผ + * 2. ้ชŒ่ฏๆ ผๅผ๏ผˆBearer ๅ‰็ผ€๏ผ‰ + * 3. ๆๅ– Token ้ƒจๅˆ†๏ผˆๅŽปๆމ "Bearer " ๅ‰็ผ€๏ผ‰ + * + * @param request HTTP ่ฏทๆฑ‚ๅฏน่ฑก + * @return JWT Token ๅญ—็ฌฆไธฒ๏ผŒๆ— ๆ•ˆ่ฟ”ๅ›ž null + */ + private String extractToken(HttpServletRequest request) { + // 1. ่Žทๅ– Authorization ๅคด + String authorization = request.getHeader("Authorization"); + if (authorization == null || !authorization.startsWith("Bearer ")) { + return null; + } + // 2. ๅŽปๆމ "Bearer " ๅ‰็ผ€๏ผˆๆณจๆ„็ฉบๆ ผ๏ผ‰ + return authorization.substring(7); + } + + /** + * ่ฎก็ฎ— Token ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๆๅ– Token ่ฟ‡ๆœŸๆ—ถ้—ด + * 2. ่ฎก็ฎ—ไธŽๅฝ“ๅ‰ๆ—ถ้—ด็š„ๅทฎๅ€ผ + * 3. ่ฝฌๆขไธบ็ง’ๅนถ็กฎไฟไธไธบ่ดŸๆ•ฐ + * + * @param token JWT Token ๅญ—็ฌฆไธฒ + * @return ๅ‰ฉไฝ™็ง’ๆ•ฐ๏ผŒๅทฒ่ฟ‡ๆœŸ่ฟ”ๅ›ž 0 + */ + private long getRemainingExpiration(String token) { + try { + // 1. ่Žทๅ–่ฟ‡ๆœŸๆ—ถ้—ด + Date expiration = jwtUtil.extractExpiration(token); + // 2. ่ฎก็ฎ—ๅ‰ฉไฝ™ๆฏซ็ง’ๆ•ฐ + long remainingMillis = expiration.getTime() - System.currentTimeMillis(); + // 3. ่ฝฌๆขไธบ็ง’๏ผˆ่‡ณๅฐ‘ไธบ 0๏ผ‰ + return Math.max(0, remainingMillis / 1000); + } catch (Exception e) { + // Token ่งฃๆžๅคฑ่ดฅ๏ผŒ่ฟ”ๅ›ž 0 + return 0; + } + } } diff --git a/src/main/java/com/aisi/template/domain/CustomUserDetails.java b/src/main/java/com/aisi/template/domain/CustomUserDetails.java index dfc3027..97cb29c 100644 --- a/src/main/java/com/aisi/template/domain/CustomUserDetails.java +++ b/src/main/java/com/aisi/template/domain/CustomUserDetails.java @@ -5,57 +5,159 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; +import java.util.Set; +/** + * ่‡ชๅฎšไน‰็”จๆˆท่ฏฆๆƒ…็ฑป + * ๅฎž็Žฐ Spring Security ็š„ UserDetails ๆŽฅๅฃ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ็”จๆˆท่ฎค่ฏ๏ผšๆไพ›็”จๆˆทไฟกๆฏ๏ผˆ็”จๆˆทๅใ€ๅฏ†็ ๏ผ‰ + * 2. ็”จๆˆทๆŽˆๆƒ๏ผšๆไพ›็”จๆˆทๆƒ้™ๅ’Œ่ง’่‰ฒ + * 3. ็”จๆˆท็Šถๆ€๏ผšๆไพ›็”จๆˆท่ดฆๆˆท็Šถๆ€ไฟกๆฏ + * + * ่ฎพ่ฎก่ฏดๆ˜Ž๏ผš + * - ๆ‰ฉๅฑ•ไบ†ๆ ‡ๅ‡† UserDetails๏ผŒๅขžๅŠ ไบ†็”จๆˆทIDๅ’Œ่ง’่‰ฒๅˆ—่กจ + * - ็”จไบŽ Spring Security ็š„่ฎค่ฏๅ’ŒๆŽˆๆƒๆต็จ‹ + * + * @author Claude + * @since 2024-04-09 + */ public class CustomUserDetails implements UserDetails { + /** + * ็”จๆˆทID + */ @Getter private final Long id; - private final String username; - private final String password; - private final Collection authorities; - private final boolean enabled; - @Getter - private final String role; - public CustomUserDetails(Long id, String username, String password, Collection authorities, boolean enabled, String role) { + /** + * ็”จๆˆทๅ + */ + private final String username; + + /** + * ๅฏ†็ ๏ผˆๅŠ ๅฏ†ๅŽ็š„๏ผ‰ + */ + private final String password; + + /** + * ็”จๆˆทๆƒ้™ๅˆ—่กจ + * - ๅŒ…ๅซ่ง’่‰ฒๅ’Œๆƒ้™ + * - ๆ ผๅผ๏ผšROLE_XXX๏ผˆ่ง’่‰ฒ๏ผ‰ๆˆ– XXX:YYY๏ผˆๆƒ้™๏ผ‰ + */ + private final Collection authorities; + + /** + * ็”จๆˆทๆ˜ฏๅฆๅฏ็”จ + * - true๏ผšๅฏ็”จ + * - false๏ผš็ฆ็”จ + */ + private final boolean enabled; + + /** + * ็”จๆˆท่ง’่‰ฒๅˆ—่กจ + * - ๅŒ…ๅซ่ง’่‰ฒ็ผ–็  + * - ไพ‹ๅฆ‚๏ผšROLE_USER, ROLE_ADMIN + */ + @Getter + private final Set roles; + + /** + * ๆž„้€ ๅ‡ฝๆ•ฐ + * + * @param id ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param password ๅฏ†็  + * @param authorities ๆƒ้™ๅˆ—่กจ + * @param enabled ๆ˜ฏๅฆๅฏ็”จ + * @param roles ่ง’่‰ฒๅˆ—่กจ + */ + public CustomUserDetails(Long id, String username, String password, + Collection authorities, + boolean enabled, Set roles) { this.id = id; this.username = username; this.password = password; this.authorities = authorities; this.enabled = enabled; - this.role = role; + this.roles = roles; } + /** + * ่Žทๅ–็”จๆˆทๅ + * + * @return ็”จๆˆทๅ + */ @Override public String getUsername() { return username; } + /** + * ่Žทๅ–ๅฏ†็  + * + * @return ๅฏ†็  + */ @Override public String getPassword() { return password; } + /** + * ่Žทๅ–็”จๆˆทๆƒ้™ๅˆ—่กจ + * + * @return ๆƒ้™ๅˆ—่กจ + */ @Override public Collection getAuthorities() { return authorities; } + /** + * ่ดฆๆˆทๆ˜ฏๅฆๆœช่ฟ‡ๆœŸ + * ่ฏดๆ˜Ž๏ผš + * - ๅฝ“ๅ‰ๅฎž็Žฐ่ฟ”ๅ›ž true๏ผˆ่ดฆๆˆทๆฐธไธ่ฟ‡ๆœŸ๏ผ‰ + * - ๅฏๆ นๆฎไธšๅŠก้œ€ๆฑ‚ๆ‰ฉๅฑ• + * + * @return true + */ @Override public boolean isAccountNonExpired() { return true; } + /** + * ่ดฆๆˆทๆ˜ฏๅฆๆœช้”ๅฎš + * ่ฏดๆ˜Ž๏ผš + * - ๅฝ“ๅ‰ๅฎž็Žฐ่ฟ”ๅ›ž true๏ผˆไธๅœจๆญคๅค„ๅˆคๆ–ญ้”ๅฎš๏ผ‰ + * - ้”ๅฎš็Šถๆ€็”ฑ User.isLocked() ๅˆคๆ–ญ + * + * @return true + */ @Override public boolean isAccountNonLocked() { return true; } + /** + * ๅฏ†็ ๆ˜ฏๅฆๆœช่ฟ‡ๆœŸ + * ่ฏดๆ˜Ž๏ผš + * - ๅฝ“ๅ‰ๅฎž็Žฐ่ฟ”ๅ›ž true๏ผˆๅฏ†็ ๆฐธไธ่ฟ‡ๆœŸ๏ผ‰ + * - ๅฏๆ นๆฎไธšๅŠก้œ€ๆฑ‚ๆ‰ฉๅฑ•๏ผˆๅฆ‚ๅฏ†็ ่ฟ‡ๆœŸ็ญ–็•ฅ๏ผ‰ + * + * @return true + */ @Override public boolean isCredentialsNonExpired() { return true; } + /** + * ่ดฆๆˆทๆ˜ฏๅฆๅฏ็”จ + * + * @return ๆ˜ฏๅฆๅฏ็”จ + */ @Override public boolean isEnabled() { return enabled; diff --git a/src/main/java/com/aisi/template/domain/RestBean.java b/src/main/java/com/aisi/template/domain/RestBean.java index 8e8f948..b4da125 100644 --- a/src/main/java/com/aisi/template/domain/RestBean.java +++ b/src/main/java/com/aisi/template/domain/RestBean.java @@ -6,23 +6,62 @@ import lombok.NoArgsConstructor; /** * ้€š็”จ็š„ REST API ๅ“ๅบ”ๅฐ่ฃ…็ฑป + * ็ปŸไธ€ API ๅ“ๅบ”ๆ ผๅผ + * + * ๅ“ๅบ”ๆ ผๅผ๏ผš + *
+ * {
+ *   "code": 200,
+ *   "message": "ๆ“ไฝœๆˆๅŠŸ",
+ *   "data": {...}
+ * }
+ * 
+ * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ็ปŸไธ€ๅ“ๅบ”ๆ ผๅผ๏ผšๆ‰€ๆœ‰ๆŽฅๅฃ่ฟ”ๅ›ž็›ธๅŒ็š„็ป“ๆž„ + * 2. ๆˆๅŠŸๅ“ๅบ”๏ผšๆไพ›ๅคš็งๆˆๅŠŸๅ“ๅบ”ๆž„้€ ๆ–นๆณ• + * 3. ๅคฑ่ดฅๅ“ๅบ”๏ผšๆไพ›ๅคš็งๅคฑ่ดฅๅ“ๅบ”ๆž„้€ ๆ–นๆณ• * * @param ่ฟ”ๅ›žๆ•ฐๆฎ็š„็ฑปๅž‹ + * @author Claude + * @since 2024-04-09 */ @Data @AllArgsConstructor @NoArgsConstructor public class RestBean { - /** ็Šถๆ€็  */ + /** + * ็Šถๆ€็  + * - 200๏ผšๆˆๅŠŸ + * - 400๏ผšๅฎขๆˆท็ซฏ้”™่ฏฏ + * - 401๏ผšๆœช็™ปๅฝ• + * - 403๏ผšๆ— ๆƒ้™ + * - 404๏ผš่ต„ๆบไธๅญ˜ๅœจ + * - 500๏ผšๆœๅŠกๅ™จ้”™่ฏฏ + */ private int code; - /** ๆ็คบๆถˆๆฏ */ + + /** + * ๆ็คบๆถˆๆฏ + * - ๆˆๅŠŸๆ—ถ่ฟ”ๅ›žๆˆๅŠŸไฟกๆฏ + * - ๅคฑ่ดฅๆ—ถ่ฟ”ๅ›ž้”™่ฏฏไฟกๆฏ + */ private String message; - /** ๅ…ทไฝ“ๆ•ฐๆฎ */ + + /** + * ๅ…ทไฝ“ๆ•ฐๆฎ + * - ๆˆๅŠŸๆ—ถ่ฟ”ๅ›žไธšๅŠกๆ•ฐๆฎ + * - ๅคฑ่ดฅๆ—ถๅฏ่ƒฝ่ฟ”ๅ›ž null ๆˆ–้”™่ฏฏ่ฏฆๆƒ… + */ private V data; /** - * ๆˆๅŠŸๅ“ๅบ”๏ผˆ้ป˜่ฎคไฝฟ็”จ {@link RestCode#SUCCESS}๏ผ‰ + * ๆˆๅŠŸๅ“ๅบ”๏ผˆ้ป˜่ฎคไฝฟ็”จ RestCode#SUCCESS๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - ็Šถๆ€็ ๏ผš200 + * - ๆถˆๆฏ๏ผš"ๆ“ไฝœๆˆๅŠŸ" + * - ่ฟ”ๅ›žไธšๅŠกๆ•ฐๆฎ * * @param data ่ฟ”ๅ›ž็š„ๆ•ฐๆฎ * @param ๆณ›ๅž‹ๅ‚ๆ•ฐ @@ -32,6 +71,19 @@ public class RestBean { return success(RestCode.SUCCESS, data); } + /** + * ๆˆๅŠŸๅ“ๅบ”๏ผˆๆ— ๆ•ฐๆฎ่ฟ”ๅ›ž๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅˆ ้™คๆ“ไฝœ + * - ๆ›ดๆ–ฐๆ“ไฝœ + * + * @param ๆณ›ๅž‹ๅ‚ๆ•ฐ + * @return RestBean ๅŒ…่ฃ…ๅฏน่ฑก + */ + public static RestBean success() { + return success(RestCode.SUCCESS, null); + } + /** * ๆˆๅŠŸๅ“ๅบ”๏ผˆๆŒ‡ๅฎš RestCode ๅ’Œๆ•ฐๆฎ๏ผ‰ * @@ -46,6 +98,9 @@ public class RestBean { /** * ๆˆๅŠŸๅ“ๅบ”๏ผˆๅช่ฟ”ๅ›ž็Šถๆ€็ ๅ’Œๆถˆๆฏ๏ผŒไธๅธฆๆ•ฐๆฎ๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅˆ ้™คๆ“ไฝœ๏ผˆไธ้œ€่ฆ่ฟ”ๅ›žๆ•ฐๆฎ๏ผ‰ + * - ๆ›ดๆ–ฐๆ“ไฝœ๏ผˆไธ้œ€่ฆ่ฟ”ๅ›žๆ•ฐๆฎ๏ผ‰ * * @param restCode ็Šถๆ€็ ๆžšไธพ * @param ๆณ›ๅž‹ๅ‚ๆ•ฐ @@ -69,7 +124,10 @@ public class RestBean { } /** - * ๅคฑ่ดฅๅ“ๅบ”๏ผˆ้ป˜่ฎคไฝฟ็”จ {@link RestCode#FAILURE}๏ผ‰ + * ๅคฑ่ดฅๅ“ๅบ”๏ผˆ้ป˜่ฎคไฝฟ็”จ RestCode#FAILURE๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - ็Šถๆ€็ ๏ผš400 + * - ๆถˆๆฏ๏ผš"ๆ“ไฝœๅคฑ่ดฅ" * * @param data ่ฟ”ๅ›ž็š„ๆ•ฐๆฎ * @param ๆณ›ๅž‹ๅ‚ๆ•ฐ @@ -115,4 +173,16 @@ public class RestBean { return new RestBean<>(code, message, data); } + /** + * ๅคฑ่ดฅๅ“ๅบ”๏ผˆ่‡ชๅฎšไน‰ code ๅ’Œ message๏ผŒๆ— ๆ•ฐๆฎ๏ผ‰ + * + * @param code ็Šถๆ€็  + * @param message ๆ็คบๆถˆๆฏ + * @param ๆณ›ๅž‹ๅ‚ๆ•ฐ + * @return RestBean ๅŒ…่ฃ…ๅฏน่ฑก + */ + public static RestBean failure(int code, String message) { + return new RestBean<>(code, message, null); + } + } diff --git a/src/main/java/com/aisi/template/domain/RestCode.java b/src/main/java/com/aisi/template/domain/RestCode.java index dd6e663..10b2a9a 100644 --- a/src/main/java/com/aisi/template/domain/RestCode.java +++ b/src/main/java/com/aisi/template/domain/RestCode.java @@ -7,26 +7,214 @@ import lombok.ToString; import java.util.HashMap; import java.util.Map; +/** + * REST API ็Šถๆ€็ ๆžšไธพ + * ๅฎšไน‰ๆ‰€ๆœ‰ API ๅ“ๅบ”็š„็Šถๆ€็ ๅ’Œๆถˆๆฏ + * + * ็Šถๆ€็ ๅˆ†็ฑป๏ผš + * - ้€š็”จ้”™่ฏฏ็ ๏ผš200-599 + * - ่ฎค่ฏ็›ธๅ…ณ๏ผš1000-1099 + * - ็”จๆˆท็›ธๅ…ณ๏ผš1100-1199 + * - ่ง’่‰ฒ็›ธๅ…ณ๏ผš1200-1299 + * - ๆƒ้™็›ธๅ…ณ๏ผš1300-1399 + * - ่œๅ•็›ธๅ…ณ๏ผš1400-1499 + * + * @author Claude + * @since 2024-04-09 + */ @Getter @ToString public enum RestCode { - SUCCESS(200,"ๆ“ไฝœๆˆๅŠŸ"), - FAILURE(400,"ๆ“ไฝœๅคฑ่ดฅ"), - USERNAME_OR_PASSWORD_ERROR(402,"็”จๆˆทๅๆˆ–ๅฏ†็ ้”™่ฏฏ"), - NO_LOGIN(401,"็”จๆˆทๆœช็™ปๅฝ•"), - UNAUTHORIZED(403,"ๆœชๆŽˆๆƒ"), - TOKEN_EXPIRE(403, "tokenๅทฒ่ฟ‡ๆœŸ"), - TOKEN_EMPTY(403, "tokenไธ่ƒฝไธบ็ฉบ"), - TOKEN_INVALID(403, "token้žๆณ•"), - SYSTEM_ERROR(500,"็ณป็ปŸ้”™่ฏฏ๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜" ), - DATA_NOT_FOUND(404,"ๆ•ฐๆฎไธๅญ˜ๅœจ"), - DATA_ALREADY_FOUND(409,"ๆ•ฐๆฎๅทฒๅญ˜ๅœจ"), - METHOD_NOT_SUPPORT(405,"ไธๆ”ฏๆŒ่ฏฅ่ฏทๆฑ‚ๆ–นๆณ•"); + // ==================== ้€š็”จ้”™่ฏฏ็  ==================== + /** + * ๆ“ไฝœๆˆๅŠŸ + * ็Šถๆ€็ ๏ผš200 + */ + SUCCESS(200, "ๆ“ไฝœๆˆๅŠŸ"), + + /** + * ๆ“ไฝœๅคฑ่ดฅ + * ็Šถๆ€็ ๏ผš400 + */ + FAILURE(400, "ๆ“ไฝœๅคฑ่ดฅ"), + + /** + * ็”จๆˆทๆœช็™ปๅฝ• + * ็Šถๆ€็ ๏ผš401 + */ + NO_LOGIN(401, "็”จๆˆทๆœช็™ปๅฝ•"), + + /** + * ๆœชๆŽˆๆƒ + * ็Šถๆ€็ ๏ผš403 + */ + UNAUTHORIZED(403, "ๆœชๆŽˆๆƒ"), + + /** + * Token ๅทฒ่ฟ‡ๆœŸ + * ็Šถๆ€็ ๏ผš403 + */ + TOKEN_EXPIRE(403, "Token ๅทฒ่ฟ‡ๆœŸ"), + + /** + * Token ไธ่ƒฝไธบ็ฉบ + * ็Šถๆ€็ ๏ผš403 + */ + TOKEN_EMPTY(403, "Token ไธ่ƒฝไธบ็ฉบ"), + + /** + * Token ้žๆณ• + * ็Šถๆ€็ ๏ผš403 + */ + TOKEN_INVALID(403, "Token ้žๆณ•"), + + /** + * ๆ•ฐๆฎไธๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš404 + */ + DATA_NOT_FOUND(404, "ๆ•ฐๆฎไธๅญ˜ๅœจ"), + + /** + * ๆ•ฐๆฎๅทฒๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš409 + */ + DATA_ALREADY_FOUND(409, "ๆ•ฐๆฎๅทฒๅญ˜ๅœจ"), + + /** + * ไธๆ”ฏๆŒ่ฏฅ่ฏทๆฑ‚ๆ–นๆณ• + * ็Šถๆ€็ ๏ผš405 + */ + METHOD_NOT_SUPPORT(405, "ไธๆ”ฏๆŒ่ฏฅ่ฏทๆฑ‚ๆ–นๆณ•"), + + /** + * ็ณป็ปŸ้”™่ฏฏ + * ็Šถๆ€็ ๏ผš500 + */ + SYSTEM_ERROR(500, "็ณป็ปŸ้”™่ฏฏ๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜"), + + // ==================== ่ฎค่ฏ็›ธๅ…ณ (1000 - 1099) ==================== + + /** + * ็”จๆˆทๅๆˆ–ๅฏ†็ ้”™่ฏฏ + * ็Šถๆ€็ ๏ผš1001 + */ + USERNAME_OR_PASSWORD_ERROR(1001, "็”จๆˆทๅๆˆ–ๅฏ†็ ้”™่ฏฏ"), + + /** + * ๅ‚ๆ•ฐ้”™่ฏฏ + * ็Šถๆ€็ ๏ผš1002 + */ + PARAM_ERROR(1002, "ๅ‚ๆ•ฐ้”™่ฏฏ"), + + // ==================== ็”จๆˆท็›ธๅ…ณ (1100 - 1199) ==================== + + /** + * ็”จๆˆทไธๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1100 + */ + USER_NOT_FOUND(1100, "็”จๆˆทไธๅญ˜ๅœจ"), + + /** + * ๅฏ†็ ้”™่ฏฏ + * ็Šถๆ€็ ๏ผš1101 + */ + PASSWORD_ERROR(1101, "ๅฏ†็ ้”™่ฏฏ"), + + /** + * ่ดฆๅทๅทฒ่ขซ้”ๅฎš + * ็Šถๆ€็ ๏ผš1102 + */ + USER_LOCKED(1102, "่ดฆๅทๅทฒ่ขซ้”ๅฎš"), + + /** + * ่ดฆๅทๅทฒ่ขซ็ฆ็”จ + * ็Šถๆ€็ ๏ผš1103 + */ + USER_DISABLED(1103, "่ดฆๅทๅทฒ่ขซ็ฆ็”จ"), + + /** + * ็”จๆˆทๅทฒๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1104 + */ + USER_ALREADY_EXISTS(1104, "็”จๆˆทๅทฒๅญ˜ๅœจ"), + + // ==================== ่ง’่‰ฒ็›ธๅ…ณ (1200 - 1299) ==================== + + /** + * ่ง’่‰ฒไธๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1200 + */ + ROLE_NOT_FOUND(1200, "่ง’่‰ฒไธๅญ˜ๅœจ"), + + /** + * ่ง’่‰ฒๅทฒๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1201 + */ + ROLE_ALREADY_EXISTS(1201, "่ง’่‰ฒๅทฒๅญ˜ๅœจ"), + + /** + * ่ง’่‰ฒๆญฃๅœจไฝฟ็”จไธญ๏ผŒๆ— ๆณ•ๅˆ ้™ค + * ็Šถๆ€็ ๏ผš1202 + */ + ROLE_IN_USE(1202, "่ง’่‰ฒๆญฃๅœจไฝฟ็”จไธญ๏ผŒๆ— ๆณ•ๅˆ ้™ค"), + + // ==================== ๆƒ้™็›ธๅ…ณ (1300 - 1399) ==================== + + /** + * ๆƒ้™ไธๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1300 + */ + PERMISSION_NOT_FOUND(1300, "ๆƒ้™ไธๅญ˜ๅœจ"), + + /** + * ๆƒ้™ๅทฒๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1301 + */ + PERMISSION_ALREADY_EXISTS(1301, "ๆƒ้™ๅทฒๅญ˜ๅœจ"), + + // ==================== ่œๅ•็›ธๅ…ณ (1400 - 1499) ==================== + + /** + * ่œๅ•ไธๅญ˜ๅœจ + * ็Šถๆ€็ ๏ผš1400 + */ + MENU_NOT_FOUND(1400, "่œๅ•ไธๅญ˜ๅœจ"), + + /** + * ่œๅ•ๅญ˜ๅœจๅญ่œๅ•๏ผŒๆ— ๆณ•ๅˆ ้™ค + * ็Šถๆ€็ ๏ผš1401 + */ + MENU_HAS_CHILDREN(1401, "่œๅ•ๅญ˜ๅœจๅญ่œๅ•๏ผŒๆ— ๆณ•ๅˆ ้™ค"), + + /** + * ่œๅ•ๆญฃๅœจไฝฟ็”จไธญ + * ็Šถๆ€็ ๏ผš1402 + */ + MENU_IN_USE(1402, "่œๅ•ๆญฃๅœจไฝฟ็”จไธญ"); + + /** + * ็Šถๆ€็  + */ private final int code; + + /** + * ๆถˆๆฏ + */ private final String message; - private final Map json; // ้ข„ๅ…ˆๅˆ›ๅปบ็š„ไธๅฏๅ˜ Map + + /** + * JSON ๆ ผๅผ็š„ๆ•ฐๆฎ๏ผˆ็”จไบŽๅบๅˆ—ๅŒ–๏ผ‰ + */ + private final Map json; + + /** + * ๆž„้€ ๅ‡ฝๆ•ฐ + * + * @param code ็Šถๆ€็  + * @param message ๆถˆๆฏ + */ RestCode(Integer code, String message) { this.code = code; this.message = message; @@ -36,9 +224,16 @@ public enum RestCode { this.json = map; } + /** + * ๅบๅˆ—ๅŒ–ไธบ JSON + * ่ฏดๆ˜Ž๏ผš + * - ๅฝ“ไฝฟ็”จ @JsonValue ๆณจ่งฃๆ—ถ๏ผŒ + * - Jackson ไผš่ฐƒ็”จๆญคๆ–นๆณ•่Žทๅ–ๅบๅˆ—ๅŒ–็š„ๅ€ผ + * + * @return JSON ๆ ผๅผ็š„ๆ•ฐๆฎ + */ @JsonValue - public Map toJson(){ + public Map toJson() { return json; } - } diff --git a/src/main/java/com/aisi/template/domain/dto/MenuDto.java b/src/main/java/com/aisi/template/domain/dto/MenuDto.java new file mode 100644 index 0000000..bd35288 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/dto/MenuDto.java @@ -0,0 +1,114 @@ +package com.aisi.template.domain.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * ่œๅ•ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽๅˆ›ๅปบๅ’Œๆ›ดๆ–ฐ่œๅ• + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ็ˆถ่œๅ•ID๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้ป˜่ฎค 0๏ผˆๆ น่œๅ•๏ผ‰ + * - ่œๅ•ๅ็งฐ๏ผšไธ่ƒฝไธบ็ฉบ + * - ่œๅ•็ฑปๅž‹๏ผšไธ่ƒฝไธบ็ฉบ๏ผˆ1=็›ฎๅฝ•๏ผŒ2=่œๅ•๏ผŒ3=ๆŒ‰้’ฎ๏ผ‰ + * - ๆŽ’ๅบๅญ—ๆฎต๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้ป˜่ฎค 0 + * - ๅฏ่งๆ€ง๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้ป˜่ฎค 1๏ผˆๅฏ่ง๏ผ‰ + * - ็Šถๆ€๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้ป˜่ฎค 1๏ผˆๅฏ็”จ๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Data +public class MenuDto { + + /** + * ่œๅ•ID + * - ๆ›ดๆ–ฐๆ—ถ้œ€่ฆๆไพ› + * - ๅˆ›ๅปบๆ—ถไธ้œ€่ฆๆไพ› + */ + private Long id; + + /** + * ็ˆถ่œๅ•ID + * - ไธ่ƒฝไธบ็ฉบ + * - 0 ๆˆ– null๏ผšๆ น่œๅ• + * - ๅ…ถไป–ๅ€ผ๏ผšๅญ่œๅ• + * - ้ป˜่ฎคๅ€ผ๏ผš0 + */ + @NotNull(message = "็ˆถ่œๅ•IDไธ่ƒฝไธบ็ฉบ") + private Long parentId = 0L; + + /** + * ่œๅ•ๅ็งฐ + * - ไธ่ƒฝไธบ็ฉบ + * - ็คบไพ‹๏ผš็”จๆˆท็ฎก็†ใ€่ง’่‰ฒ็ฎก็† + */ + @NotBlank(message = "่œๅ•ๅ็งฐไธ่ƒฝไธบ็ฉบ") + private String menuName; + + /** + * ่œๅ•็ฑปๅž‹ + * - ไธ่ƒฝไธบ็ฉบ + * - 1๏ผš็›ฎๅฝ•๏ผˆDIRECTORY๏ผ‰ + * - 2๏ผš่œๅ•๏ผˆMENU๏ผ‰ + * - 3๏ผšๆŒ‰้’ฎ๏ผˆBUTTON๏ผ‰ + */ + @NotNull(message = "่œๅ•็ฑปๅž‹ไธ่ƒฝไธบ็ฉบ") + private Integer menuType; + + /** + * ่œๅ•่ทฏๅพ„ + * - ็”จไบŽๅ‰็ซฏ่ทฏ็”ฑ + * - ็คบไพ‹๏ผš/user, /role + */ + private String menuPath; + + /** + * ็ป„ไปถ่ทฏๅพ„ + * - ๅ‰็ซฏ็ป„ไปถๆ–‡ไปถ่ทฏๅพ„ + * - ็คบไพ‹๏ผšviews/user/List.vue + */ + private String component; + + /** + * ่œๅ•ๅ›พๆ ‡ + * - ๅ›พๆ ‡ๅ็งฐ + * - ็คบไพ‹๏ผšuser, role, setting + */ + private String icon; + + /** + * ๆŽ’ๅบๅญ—ๆฎต + * - ไธ่ƒฝไธบ็ฉบ + * - ๆ•ฐๅ€ผ่ถŠๅฐ่ถŠ้ ๅ‰ + * - ้ป˜่ฎคๅ€ผ๏ผš0 + */ + @NotNull(message = "ๆŽ’ๅบๅญ—ๆฎตไธ่ƒฝไธบ็ฉบ") + private Integer sortOrder = 0; + + /** + * ๅฏ่งๆ€ง + * - ไธ่ƒฝไธบ็ฉบ + * - 1๏ผšๅฏ่ง๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผšไธๅฏ่ง + */ + @NotNull(message = "ๅฏ่งๆ€งไธ่ƒฝไธบ็ฉบ") + private Integer visible = 1; + + /** + * ่œๅ•็Šถๆ€ + * - ไธ่ƒฝไธบ็ฉบ + * - 1๏ผšๅฏ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ + @NotNull(message = "็Šถๆ€ไธ่ƒฝไธบ็ฉบ") + private Integer status = 1; + + /** + * ๆƒ้™็ผ–็  + * - ็”จไบŽๆŽงๅˆถ่œๅ•็š„่ฎฟ้—ฎๆƒ้™ + * - ็คบไพ‹๏ผšuser:read, role:read + */ + private String permissionCode; +} diff --git a/src/main/java/com/aisi/template/domain/dto/PasswordResetConfirmDto.java b/src/main/java/com/aisi/template/domain/dto/PasswordResetConfirmDto.java index d8354bb..38fbab6 100644 --- a/src/main/java/com/aisi/template/domain/dto/PasswordResetConfirmDto.java +++ b/src/main/java/com/aisi/template/domain/dto/PasswordResetConfirmDto.java @@ -1,28 +1,66 @@ package com.aisi.template.domain.dto; +import com.aisi.template.annotation.StrongPassword; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; import lombok.Data; +/** + * ๅฏ†็ ้‡็ฝฎ็กฎ่ฎคๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽไฝฟ็”จ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็  + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ้‚ฎ็ฎฑ๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒๅฟ…้กป็ฌฆๅˆ้‚ฎ็ฎฑๆ ผๅผ + * - ้ชŒ่ฏ็ ๏ผšๅฟ…้กปๆ˜ฏ 6 ไฝๆ•ฐๅญ— + * - ๆ–ฐๅฏ†็ ๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้œ€ๆปก่ถณๅผบๅฏ†็ ่ฆๆฑ‚ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทๆ”ถๅˆฐ้ชŒ่ฏ็ ๅŽ๏ผŒ่พ“ๅ…ฅ้ชŒ่ฏ็ ๅ’Œๆ–ฐๅฏ†็ ้‡็ฝฎๅฏ†็  + * + * ๆณจๆ„๏ผš + * - ้ชŒ่ฏ็  10 ๅˆ†้’Ÿๆœ‰ๆ•ˆ + * - ้ชŒ่ฏ็ ๆœ€ๅคšๅฐ่ฏ• 5 ๆฌก๏ผŒ่ถ…่ฟ‡ๅŽๅคฑๆ•ˆ + * - ้ชŒ่ฏ็ ไฝฟ็”จๅŽ็ซ‹ๅณๅคฑๆ•ˆ + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "้‡็ฝฎๅฏ†็ ็กฎ่ฎค่ฏทๆฑ‚") public class PasswordResetConfirmDto { + /** + * ้‚ฎ็ฎฑๅœฐๅ€ + * - ไธ่ƒฝไธบ็ฉบ + * - ๅฟ…้กป็ฌฆๅˆ้‚ฎ็ฎฑๆ ผๅผ + * - ็คบไพ‹๏ผšuser@example.com + */ @NotBlank(message = "้‚ฎ็ฎฑไธ่ƒฝไธบ็ฉบ") @Email(message = "้‚ฎ็ฎฑๆ ผๅผไธๆญฃ็กฎ") - @Schema(description = "้‚ฎ็ฎฑ") + @Schema(description = "้‚ฎ็ฎฑ", example = "user@example.com") private String email; + /** + * ้ชŒ่ฏ็  + * - ไธ่ƒฝไธบ็ฉบ + * - ๅฟ…้กปๆ˜ฏ 6 ไฝๆ•ฐๅญ— + * - ็คบไพ‹๏ผš123456 + */ @NotBlank(message = "้ชŒ่ฏ็ ไธ่ƒฝไธบ็ฉบ") @Pattern(regexp = "\\d{6}", message = "้ชŒ่ฏ็ ๅฟ…้กปไธบ 6 ไฝๆ•ฐๅญ—") - @Schema(description = "6ไฝ้ชŒ่ฏ็ ") + @Schema(description = "6ไฝ้ชŒ่ฏ็ ", example = "123456") private String code; + /** + * ๆ–ฐๅฏ†็  + * - ไธ่ƒฝไธบ็ฉบ + * - ้œ€ๆปก่ถณๅผบๅฏ†็ ่ฆๆฑ‚๏ผšๅŒ…ๅซๅคงๅฐๅ†™ๅญ—ๆฏใ€ๆ•ฐๅญ—ใ€็‰นๆฎŠๅญ—็ฌฆ + * - ็คบไพ‹๏ผšNewPassword123! + */ @NotBlank(message = "ๆ–ฐๅฏ†็ ไธ่ƒฝไธบ็ฉบ") - @Size(min = 6, max = 64, message = "ๅฏ†็ ้•ฟๅบฆๅฟ…้กปๅœจ 6-64 ไฝไน‹้—ด") - @Schema(description = "ๆ–ฐๅฏ†็ ") + @StrongPassword + @Schema(description = "ๆ–ฐๅฏ†็ ", example = "NewPassword123!") private String newPassword; } diff --git a/src/main/java/com/aisi/template/domain/dto/PasswordResetRequestDto.java b/src/main/java/com/aisi/template/domain/dto/PasswordResetRequestDto.java index 393d398..1bb48a9 100644 --- a/src/main/java/com/aisi/template/domain/dto/PasswordResetRequestDto.java +++ b/src/main/java/com/aisi/template/domain/dto/PasswordResetRequestDto.java @@ -5,12 +5,36 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Data; +/** + * ๅฏ†็ ้‡็ฝฎ่ฏทๆฑ‚ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽ่ฏทๆฑ‚ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ้‚ฎ็ฎฑ๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒๅฟ…้กป็ฌฆๅˆ้‚ฎ็ฎฑๆ ผๅผ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทๅฟ˜่ฎฐๅฏ†็ ๆ—ถ๏ผŒ่พ“ๅ…ฅ้‚ฎ็ฎฑ่ฏทๆฑ‚้‡็ฝฎ้ชŒ่ฏ็  + * + * ๆณจๆ„๏ผš + * - ไธบ้˜ฒๆญข็”จๆˆทๅๆžšไธพ๏ผŒๆ— ่ฎบ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ้ƒฝ่ฟ”ๅ›ž็›ธๅŒๆถˆๆฏ + * - ๅŒไธ€้‚ฎ็ฎฑ 60 ็ง’ๅ†…ๅช่ƒฝ่ฏทๆฑ‚ไธ€ๆฌก + * - ๅŒไธ€้‚ฎ็ฎฑ 1 ๅฐๆ—ถๅ†…ๆœ€ๅคš่ฏทๆฑ‚ 5 ๆฌก + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "ๆ‰พๅ›žๅฏ†็ ่ฏทๆฑ‚") public class PasswordResetRequestDto { + /** + * ้‚ฎ็ฎฑๅœฐๅ€ + * - ไธ่ƒฝไธบ็ฉบ + * - ๅฟ…้กป็ฌฆๅˆ้‚ฎ็ฎฑๆ ผๅผ + * - ็คบไพ‹๏ผšuser@example.com + */ @NotBlank(message = "้‚ฎ็ฎฑไธ่ƒฝไธบ็ฉบ") @Email(message = "้‚ฎ็ฎฑๆ ผๅผไธๆญฃ็กฎ") - @Schema(description = "้‚ฎ็ฎฑ") + @Schema(description = "้‚ฎ็ฎฑ", example = "user@example.com") private String email; } diff --git a/src/main/java/com/aisi/template/domain/dto/RefreshTokenDto.java b/src/main/java/com/aisi/template/domain/dto/RefreshTokenDto.java new file mode 100644 index 0000000..46ed989 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/dto/RefreshTokenDto.java @@ -0,0 +1,14 @@ +package com.aisi.template.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +@Schema(description = "ๅˆทๆ–ฐไปค็‰Œ่ฏทๆฑ‚") +public class RefreshTokenDto { + + @NotBlank(message = "ๅˆทๆ–ฐไปค็‰Œไธ่ƒฝไธบ็ฉบ") + @Schema(description = "ๅˆทๆ–ฐไปค็‰Œ") + private String refreshToken; +} diff --git a/src/main/java/com/aisi/template/domain/dto/RoleDto.java b/src/main/java/com/aisi/template/domain/dto/RoleDto.java new file mode 100644 index 0000000..122416f --- /dev/null +++ b/src/main/java/com/aisi/template/domain/dto/RoleDto.java @@ -0,0 +1,65 @@ +package com.aisi.template.domain.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * ่ง’่‰ฒๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽๅˆ›ๅปบๅ’Œๆ›ดๆ–ฐ่ง’่‰ฒ + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ่ง’่‰ฒ็ผ–็ ๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒๆ ผๅผๅฆ‚ ROLE_USER + * - ่ง’่‰ฒๅ็งฐ๏ผšไธ่ƒฝไธบ็ฉบ + * - ๆŽ’ๅบๅญ—ๆฎต๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้ป˜่ฎค 0 + * - ็Šถๆ€๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้ป˜่ฎค 1๏ผˆๅฏ็”จ๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Data +public class RoleDto { + + /** + * ่ง’่‰ฒ็ผ–็  + * - ไธ่ƒฝไธบ็ฉบ + * - ๆ ผๅผ๏ผšROLE_ + ๅ็งฐ๏ผŒๅฆ‚ ROLE_USER, ROLE_ADMIN + * - ็คบไพ‹๏ผšROLE_USER + */ + @NotBlank(message = "่ง’่‰ฒ็ผ–็ ไธ่ƒฝไธบ็ฉบ") + private String roleCode; + + /** + * ่ง’่‰ฒๅ็งฐ + * - ไธ่ƒฝไธบ็ฉบ + * - ็”จไบŽ็•Œ้ขๅฑ•็คบ + * - ็คบไพ‹๏ผšๆ™ฎ้€š็”จๆˆทใ€็ฎก็†ๅ‘˜ + */ + @NotBlank(message = "่ง’่‰ฒๅ็งฐไธ่ƒฝไธบ็ฉบ") + private String roleName; + + /** + * ่ง’่‰ฒๆ่ฟฐ + * - ๅฏ้€‰ + * - ๆ่ฟฐ่ง’่‰ฒ็š„็”จ้€”ๅ’Œๆƒ้™่Œƒๅ›ด + */ + private String description; + + /** + * ๆŽ’ๅบๅญ—ๆฎต + * - ไธ่ƒฝไธบ็ฉบ + * - ๆ•ฐๅ€ผ่ถŠๅฐ่ถŠ้ ๅ‰ + * - ้ป˜่ฎคๅ€ผ๏ผš0 + */ + @NotNull(message = "ๆŽ’ๅบๅญ—ๆฎตไธ่ƒฝไธบ็ฉบ") + private Integer sortOrder = 0; + + /** + * ่ง’่‰ฒ็Šถๆ€ + * - ไธ่ƒฝไธบ็ฉบ + * - 1๏ผšๅฏ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ + @NotNull(message = "็Šถๆ€ไธ่ƒฝไธบ็ฉบ") + private Integer status = 1; +} diff --git a/src/main/java/com/aisi/template/domain/dto/RoleQueryDto.java b/src/main/java/com/aisi/template/domain/dto/RoleQueryDto.java new file mode 100644 index 0000000..d6e0d2c --- /dev/null +++ b/src/main/java/com/aisi/template/domain/dto/RoleQueryDto.java @@ -0,0 +1,41 @@ +package com.aisi.template.domain.dto; + +import lombok.Data; + +/** + * ่ง’่‰ฒๆŸฅ่ฏขๅ‚ๆ•ฐๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽๆŸฅ่ฏข่ง’่‰ฒๅˆ—่กจ + * + * ๆŸฅ่ฏขๆกไปถ๏ผš + * - ่ง’่‰ฒ็ผ–็ ๏ผšๆจก็ณŠๅŒน้… + * - ่ง’่‰ฒๅ็งฐ๏ผšๆจก็ณŠๅŒน้… + * - ็Šถๆ€๏ผš็ฒพ็กฎๅŒน้… + * + * @author Claude + * @since 2024-04-09 + */ +@Data +public class RoleQueryDto { + + /** + * ่ง’่‰ฒ็ผ–็  + * - ๆจก็ณŠๅŒน้… + * - ็คบไพ‹๏ผšROLE_USER ไผšๅŒน้… role_code LIKE '%ROLE_USER%' + */ + private String roleCode; + + /** + * ่ง’่‰ฒๅ็งฐ + * - ๆจก็ณŠๅŒน้… + * - ็คบไพ‹๏ผš็ฎก็†ๅ‘˜ ไผšๅŒน้… role_name LIKE '%็ฎก็†ๅ‘˜%' + */ + private String roleName; + + /** + * ่ง’่‰ฒ็Šถๆ€ + * - ็ฒพ็กฎๅŒน้… + * - 1๏ผšๅฏ็”จ + * - 0๏ผš็ฆ็”จ + */ + private Integer status; +} diff --git a/src/main/java/com/aisi/template/domain/dto/UserDto.java b/src/main/java/com/aisi/template/domain/dto/UserDto.java index bbf1f67..428747c 100644 --- a/src/main/java/com/aisi/template/domain/dto/UserDto.java +++ b/src/main/java/com/aisi/template/domain/dto/UserDto.java @@ -1,24 +1,59 @@ package com.aisi.template.domain.dto; +import com.aisi.template.annotation.StrongPassword; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.Data; +/** + * ็”จๆˆทๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽ็”จๆˆทๆณจๅ†Œๅ’Œ็™ปๅฝ•่ฏทๆฑ‚ + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ็”จๆˆทๅ๏ผš2-50 ไฝๅญ—็ฌฆ๏ผŒไธ่ƒฝไธบ็ฉบ + * - ๅฏ†็ ๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒ้œ€ๆปก่ถณๅผบๅฏ†็ ่ฆๆฑ‚๏ผˆ@StrongPassword๏ผ‰ + * - ้‚ฎ็ฎฑ๏ผšๅฏ้€‰๏ผŒๆณจๅ†Œๆ—ถๅปบ่ฎฎๆไพ› + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทๆณจๅ†Œ๏ผš้œ€่ฆ็”จๆˆทๅใ€ๅฏ†็ ใ€้‚ฎ็ฎฑ + * - ็”จๆˆท็™ปๅฝ•๏ผš้œ€่ฆ็”จๆˆทๅใ€ๅฏ†็  + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "็”จๆˆทๆณจๅ†Œ/็™ปๅฝ•่ฏทๆฑ‚") public class UserDto { + /** + * ็”จๆˆทๅ + * - ้•ฟๅบฆ๏ผš2-50 ไฝๅญ—็ฌฆ + * - ไธ่ƒฝไธบ็ฉบ + * - ็คบไพ‹๏ผšadmin, user01 + */ @NotBlank(message = "็”จๆˆทๅไธ่ƒฝไธบ็ฉบ") @Size(min = 2, max = 50, message = "็”จๆˆทๅ้•ฟๅบฆๅฟ…้กปๅœจ 2-50 ไฝไน‹้—ด") @Schema(description = "็”จๆˆทๅ", example = "admin") private String username; + /** + * ๅฏ†็  + * - ไธ่ƒฝไธบ็ฉบ + * - ้œ€ๆปก่ถณๅผบๅฏ†็ ่ฆๆฑ‚๏ผšๅŒ…ๅซๅคงๅฐๅ†™ๅญ—ๆฏใ€ๆ•ฐๅญ—ใ€็‰นๆฎŠๅญ—็ฌฆ + * - ็คบไพ‹๏ผšPassword123! + */ @NotBlank(message = "ๅฏ†็ ไธ่ƒฝไธบ็ฉบ") - @Size(min = 6, max = 64, message = "ๅฏ†็ ้•ฟๅบฆๅฟ…้กปๅœจ 6-64 ไฝไน‹้—ด") - @Schema(description = "ๅฏ†็ ", example = "123456") + @StrongPassword + @Schema(description = "ๅฏ†็ ", example = "Password123!") private String password; + /** + * ้‚ฎ็ฎฑๅœฐๅ€ + * - ๅฏ้€‰ๅญ—ๆฎต + * - ๆณจๅ†Œๆ—ถๅปบ่ฎฎๆไพ›๏ผŒ็”จไบŽๆ‰พๅ›žๅฏ†็  + * - ็คบไพ‹๏ผšuser@example.com + */ @Schema(description = "้‚ฎ็ฎฑ๏ผˆๆณจๅ†Œๆ—ถๅฏ้€‰๏ผ‰", example = "user@example.com") private String email; } diff --git a/src/main/java/com/aisi/template/domain/dto/UserQueryDto.java b/src/main/java/com/aisi/template/domain/dto/UserQueryDto.java index d84d240..369b978 100644 --- a/src/main/java/com/aisi/template/domain/dto/UserQueryDto.java +++ b/src/main/java/com/aisi/template/domain/dto/UserQueryDto.java @@ -3,22 +3,52 @@ package com.aisi.template.domain.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +/** + * ็”จๆˆทๆŸฅ่ฏขๅ‚ๆ•ฐๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽๅˆ†้กตๆŸฅ่ฏข็”จๆˆทๅˆ—่กจ + * + * ๆŸฅ่ฏขๆกไปถ๏ผš + * - ๅˆ†้กตๅ‚ๆ•ฐ๏ผš้กต็ ใ€ๆฏ้กตๅคงๅฐ + * - ๅ…ณ้”ฎ่ฏ๏ผšๅŒน้…็”จๆˆทๅๆˆ–้‚ฎ็ฎฑ๏ผˆๆจก็ณŠๆœ็ดข๏ผ‰ + * - ็Šถๆ€๏ผš็ฒพ็กฎๅŒน้…๏ผˆ1=ๆญฃๅธธ๏ผŒ0=็ฆ็”จ๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "็”จๆˆทๆŸฅ่ฏขๅ‚ๆ•ฐ") public class UserQueryDto { + /** + * ้กต็  + * - ้ป˜่ฎคๅ€ผ๏ผš1 + * - ไปŽ 1 ๅผ€ๅง‹่ฎกๆ•ฐ + */ @Schema(description = "้กต็ ", example = "1") private Integer page = 1; + /** + * ๆฏ้กตๅคงๅฐ + * - ้ป˜่ฎคๅ€ผ๏ผš10 + * - ๆœ€ๅฐๅ€ผ๏ผš1 + */ @Schema(description = "ๆฏ้กตๅคงๅฐ", example = "10") private Integer size = 10; - @Schema(description = "ๅ…ณ้”ฎ่ฏ๏ผˆ็”จๆˆทๅ/้‚ฎ็ฎฑ๏ผ‰") + /** + * ๅ…ณ้”ฎ่ฏ + * - ๆจก็ณŠๅŒน้…็”จๆˆทๅๆˆ–้‚ฎ็ฎฑ + * - ็คบไพ‹๏ผšadmin ไผšๅŒน้… username LIKE '%admin%' OR email LIKE '%admin%' + */ + @Schema(description = "ๅ…ณ้”ฎ่ฏ๏ผˆ็”จๆˆทๅ/้‚ฎ็ฎฑ๏ผ‰", example = "admin") private String keyword; - @Schema(description = "่ง’่‰ฒ๏ผˆUSER/ADMIN๏ผ‰") - private String role; - - @Schema(description = "็Šถๆ€๏ผˆ1=ๆญฃๅธธ 0=็ฆ็”จ๏ผ‰") + /** + * ็”จๆˆท็Šถๆ€ + * - 1๏ผšๆญฃๅธธ๏ผˆๅฏ็”จ๏ผ‰ + * - 0๏ผš็ฆ็”จ + * - ็ฒพ็กฎๅŒน้… + */ + @Schema(description = "็Šถๆ€๏ผˆ1=ๆญฃๅธธ 0=็ฆ็”จ๏ผ‰", example = "1") private Integer status; } diff --git a/src/main/java/com/aisi/template/domain/dto/UserRoleUpdateDto.java b/src/main/java/com/aisi/template/domain/dto/UserRoleUpdateDto.java index e1d5109..2033d22 100644 --- a/src/main/java/com/aisi/template/domain/dto/UserRoleUpdateDto.java +++ b/src/main/java/com/aisi/template/domain/dto/UserRoleUpdateDto.java @@ -1,14 +1,40 @@ package com.aisi.template.domain.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; import lombok.Data; +import java.util.Set; + +/** + * ็”จๆˆท่ง’่‰ฒๆ›ดๆ–ฐๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽไธบ็”จๆˆทๅˆ†้…ๆˆ–ไฟฎๆ”น่ง’่‰ฒ + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ่ง’่‰ฒIDๅˆ—่กจ๏ผšไธ่ƒฝไธบ็ฉบ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็ฎก็†ๅ‘˜ไธบ็”จๆˆทๅˆ†้…่ง’่‰ฒ + * - ็ฎก็†ๅ‘˜ไฟฎๆ”น็”จๆˆท็š„่ง’่‰ฒ + * + * ๆณจๆ„๏ผš + * - ไธ่ƒฝไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ + * - ่ง’่‰ฒๅ˜ๆ›ดๅŽ็”จๆˆท้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ•ๆ‰่ƒฝ็”Ÿๆ•ˆ + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "็”จๆˆท่ง’่‰ฒๆ›ดๆ–ฐ่ฏทๆฑ‚") public class UserRoleUpdateDto { - @NotBlank(message = "่ง’่‰ฒไธ่ƒฝไธบ็ฉบ") - @Schema(description = "่ง’่‰ฒ๏ผˆUSER/ADMIN๏ผ‰", example = "ADMIN") - private String role; + /** + * ่ง’่‰ฒIDๅˆ—่กจ + * - ไธ่ƒฝไธบ็ฉบ + * - ไผšๅฎŒๅ…จๆ›ฟๆข็”จๆˆท็š„่ง’่‰ฒๅˆ—่กจ + * - ็คบไพ‹๏ผš[1, 2] ่กจ็คบๅˆ†้…่ง’่‰ฒIDไธบ1ๅ’Œ2็š„่ง’่‰ฒ + */ + @NotEmpty(message = "่ง’่‰ฒไธ่ƒฝไธบ็ฉบ") + @Schema(description = "่ง’่‰ฒIDๅˆ—่กจ", example = "[1, 2]") + private Set roleIds; } diff --git a/src/main/java/com/aisi/template/domain/dto/UserStatusUpdateDto.java b/src/main/java/com/aisi/template/domain/dto/UserStatusUpdateDto.java index c20c837..bd77bb3 100644 --- a/src/main/java/com/aisi/template/domain/dto/UserStatusUpdateDto.java +++ b/src/main/java/com/aisi/template/domain/dto/UserStatusUpdateDto.java @@ -6,10 +6,34 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; +/** + * ็”จๆˆท็Šถๆ€ๆ›ดๆ–ฐๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * ็”จไบŽๆ›ดๆ–ฐ็”จๆˆท็š„ๅฏ็”จ/็ฆ็”จ็Šถๆ€ + * + * ้ชŒ่ฏ่ง„ๅˆ™๏ผš + * - ็Šถๆ€๏ผšไธ่ƒฝไธบ็ฉบ๏ผŒๅช่ƒฝๆ˜ฏ 0 ๆˆ– 1 + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็ฎก็†ๅ‘˜ๅฏ็”จๆˆ–็ฆ็”จ็”จๆˆท่ดฆๆˆท + * + * ๆณจๆ„๏ผš + * - ไธ่ƒฝ็ฆ็”จๅฝ“ๅ‰็™ปๅฝ•็š„็”จๆˆท + * - ็Šถๆ€ๅ˜ๆ›ดๅŽ็”จๆˆท้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ• + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "็”จๆˆท็Šถๆ€ๆ›ดๆ–ฐ่ฏทๆฑ‚") public class UserStatusUpdateDto { + /** + * ็”จๆˆท็Šถๆ€ + * - ไธ่ƒฝไธบ็ฉบ + * - ๅช่ƒฝๆ˜ฏ 0 ๆˆ– 1 + * - 1๏ผšๆญฃๅธธ๏ผˆๅฏ็”จ๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ @NotNull(message = "็Šถๆ€ไธ่ƒฝไธบ็ฉบ") @Min(value = 0, message = "็Šถๆ€ๅช่ƒฝไธบ 0 ๆˆ– 1") @Max(value = 1, message = "็Šถๆ€ๅช่ƒฝไธบ 0 ๆˆ– 1") diff --git a/src/main/java/com/aisi/template/domain/entity/PasswordResetCode.java b/src/main/java/com/aisi/template/domain/entity/PasswordResetCode.java index 13a50f0..d350d1e 100644 --- a/src/main/java/com/aisi/template/domain/entity/PasswordResetCode.java +++ b/src/main/java/com/aisi/template/domain/entity/PasswordResetCode.java @@ -6,6 +6,26 @@ import lombok.Data; import java.time.LocalDateTime; +/** + * ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ๅฎžไฝ“็ฑป + * ๅญ˜ๅ‚จๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ไฟกๆฏ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ้‚ฎ็ฎฑ๏ผš็”จๆˆท้‚ฎ็ฎฑ + * 2. ้ชŒ่ฏ็ ๏ผš้ชŒ่ฏ็ ็š„ๅ“ˆๅธŒๅ€ผ๏ผˆไธๅญ˜ๅ‚จๆ˜Žๆ–‡๏ผ‰ + * 3. ๆœ‰ๆ•ˆๆœŸ๏ผš่ฟ‡ๆœŸๆ—ถ้—ด + * 4. ไฝฟ็”จ็Šถๆ€๏ผšๆ˜ฏๅฆๅทฒไฝฟ็”จ + * 5. ๅฐ่ฏ•ๆฌกๆ•ฐ๏ผš้ชŒ่ฏๅคฑ่ดฅๆฌกๆ•ฐ + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ้ชŒ่ฏ็ ไฝฟ็”จ SHA-256 ๅ“ˆๅธŒๅญ˜ๅ‚จ + * - ้ชŒ่ฏ็ ้ป˜่ฎค 10 ๅˆ†้’Ÿๆœ‰ๆ•ˆ + * - ๆœ€ๅคšๅฐ่ฏ• 5 ๆฌก๏ผŒ่ถ…่ฟ‡ๅŽๅคฑๆ•ˆ + * - ๅŒไธ€้‚ฎ็ฎฑ 60 ็ง’ๅ†…ๅช่ƒฝ่ฏทๆฑ‚ไธ€ๆฌก + * + * @author Claude + * @since 2024-04-09 + */ @Entity @Table(name = "password_reset_codes", indexes = { @Index(name = "idx_password_reset_email", columnList = "email"), @@ -14,30 +34,61 @@ import java.time.LocalDateTime; @Data public class PasswordResetCode { + /** + * ้ชŒ่ฏ็ ID๏ผˆไธป้”ฎ๏ผ‰ + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + /** + * ้‚ฎ็ฎฑๅœฐๅ€๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + */ @Column(nullable = false, length = 255) private String email; + /** + * ้ชŒ่ฏ็ ๅ“ˆๅธŒๅ€ผ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - ๅญ˜ๅ‚จ SHA-256 ๅ“ˆๅธŒๅ€ผ๏ผŒไธๅญ˜ๅ‚จๆ˜Žๆ–‡้ชŒ่ฏ็  + * - ้˜ฒๆญขๆ•ฐๆฎๅบ“ๆณ„้œฒๅฏผ่‡ด้ชŒ่ฏ็ ๆณ„้œฒ + */ @Column(name = "code_hash", nullable = false, length = 64) private String codeHash; + /** + * ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * ้ป˜่ฎค๏ผšๅฝ“ๅ‰ๆ—ถ้—ด + 10 ๅˆ†้’Ÿ + */ @Column(name = "expires_at", nullable = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private LocalDateTime expiresAt; + /** + * ๆ˜ฏๅฆๅทฒไฝฟ็”จ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - false๏ผšๆœชไฝฟ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - true๏ผšๅทฒไฝฟ็”จ + */ @Column(name = "used", nullable = false) private Boolean used = false; + /** + * ๅฐ่ฏ•ๆฌกๆ•ฐ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - ้ชŒ่ฏๅคฑ่ดฅๆ—ถ้€’ๅขž + * - ่ถ…่ฟ‡ 5 ๆฌกๅŽ้ชŒ่ฏ็ ๅคฑๆ•ˆ + */ @Column(name = "attempt_count", nullable = false) private Integer attemptCount = 0; + /** + * ๅˆ›ๅปบๆ—ถ้—ด๏ผˆไธๅฏไธบ็ฉบ๏ผŒไธๅฏๆ›ดๆ–ฐ๏ผ‰ + */ @Column(name = "created_at", nullable = false, updatable = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private LocalDateTime createdAt; + /** + * ๅˆ›ๅปบๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ @PrePersist protected void onCreate() { if (createdAt == null) { diff --git a/src/main/java/com/aisi/template/domain/entity/RefreshToken.java b/src/main/java/com/aisi/template/domain/entity/RefreshToken.java new file mode 100644 index 0000000..4ceed8c --- /dev/null +++ b/src/main/java/com/aisi/template/domain/entity/RefreshToken.java @@ -0,0 +1,126 @@ +package com.aisi.template.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * Refresh Token ๅฎžไฝ“็ฑป + * ๅฎšไน‰ๅˆทๆ–ฐไปค็‰Œ็š„ๅญ˜ๅ‚จๅ’Œ็ฎก็† + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ็”จๆˆทๅ…ณ่”๏ผš็”จๆˆทID + * 2. Token ไฟกๆฏ๏ผšToken ๅ“ˆๅธŒๅ€ผ + * 3. ่ฎพๅค‡ไฟกๆฏ๏ผš่ฎพๅค‡ไฟกๆฏใ€IPๅœฐๅ€ + * 4. ๆœ‰ๆ•ˆๆœŸ๏ผš่ฟ‡ๆœŸๆ—ถ้—ดใ€ๆ’ค้”€็Šถๆ€ + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ๆ•ฐๆฎๅบ“ๅชๅญ˜ๅ‚จ Token ็š„ SHA-256 ๅ“ˆๅธŒๅ€ผ๏ผŒไธๅญ˜ๅ‚จๅŽŸๅง‹ๅ€ผ + * - ๆ”ฏๆŒ Token ่ฝฎๆข๏ผˆๆฏๆฌกๅˆทๆ–ฐ็”Ÿๆˆๆ–ฐ Token๏ผ‰ + * - ๆ”ฏๆŒ Token ๆ’ค้”€๏ผˆ็™ปๅ‡บใ€ไฟฎๆ”นๅฏ†็ ๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Entity +@Table(name = "refresh_tokens") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RefreshToken { + + /** + * Token ID๏ผˆไธป้”ฎ๏ผ‰ + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * ็”จๆˆทID๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + */ + @Column(name = "user_id", nullable = false) + private Long userId; + + /** + * Token ๅ“ˆๅธŒๅ€ผ๏ผˆๅ”ฏไธ€๏ผŒไธๅฏไธบ็ฉบ๏ผ‰ + * - ๅญ˜ๅ‚จ SHA-256 ๅ“ˆๅธŒๅ€ผ๏ผŒไธๅญ˜ๅ‚จๅŽŸๅง‹ Token + * - ้˜ฒๆญขๆ•ฐๆฎๅบ“ๆณ„้œฒๅฏผ่‡ด Token ๆณ„้œฒ + */ + @Column(name = "token_hash", nullable = false, unique = true, length = 128) + private String tokenHash; + + /** + * ่ฎพๅค‡ไฟกๆฏ + * ๅฆ‚๏ผšiPhone 14 Pro, Windows PC + * ็”จไบŽๅ‘็”จๆˆทๅฑ•็คบ็™ปๅฝ•่ฎพๅค‡ๅˆ—่กจ + */ + @Column(name = "device_info", length = 255) + private String deviceInfo; + + /** + * IP ๅœฐๅ€ + * ๆ”ฏๆŒ IPv4 ๅ’Œ IPv6 + * ็”จไบŽๅฎ‰ๅ…จๅฎก่ฎก + */ + @Column(name = "ip_address", length = 45) + private String ipAddress; + + /** + * ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * ้ป˜่ฎค๏ผšๅฝ“ๅ‰ๆ—ถ้—ด + 7 ๅคฉ + */ + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt; + + /** + * ๆ˜ฏๅฆๅทฒๆ’ค้”€ + * - false๏ผšๆœ‰ๆ•ˆ๏ผˆ้ป˜่ฎค๏ผ‰ + * - true๏ผšๅทฒๆ’ค้”€ + */ + @Column(name = "revoked", nullable = false) + private Boolean revoked = false; + + /** + * ๆ’ค้”€ๆ—ถ้—ด + * ่ฎฐๅฝ• Token ่ขซๆ’ค้”€็š„ๆ—ถ้—ด + */ + @Column(name = "revoked_at") + private LocalDateTime revokedAt; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด๏ผˆไธๅฏไธบ็ฉบ๏ผŒไธๅฏๆ›ดๆ–ฐ๏ผ‰ + */ + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * ๅˆ›ๅปบๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } + + /** + * ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * + * @return true ่กจ็คบ Token ๅทฒ่ฟ‡ๆœŸ + */ + public boolean isExpired() { + return LocalDateTime.now().isAfter(expiresAt); + } + + /** + * ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๆœ‰ๆ•ˆ + * ๆœ‰ๆ•ˆ = ๆœช่ฟ‡ๆœŸ ไธ” ๆœชๆ’ค้”€ + * + * @return true ่กจ็คบ Token ๆœ‰ๆ•ˆ + */ + public boolean isValid() { + return !revoked && !isExpired(); + } +} diff --git a/src/main/java/com/aisi/template/domain/entity/SysAuditLog.java b/src/main/java/com/aisi/template/domain/entity/SysAuditLog.java new file mode 100644 index 0000000..0df232a --- /dev/null +++ b/src/main/java/com/aisi/template/domain/entity/SysAuditLog.java @@ -0,0 +1,150 @@ +package com.aisi.template.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * ็ณป็ปŸๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“็ฑป + * ่ฎฐๅฝ•็”จๆˆทๆ“ไฝœๅ’Œ็ณป็ปŸ่กŒไธบ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๆ“ไฝœ็”จๆˆท๏ผš็”จๆˆทIDใ€็”จๆˆทๅ + * 2. ๆ“ไฝœไฟกๆฏ๏ผšๆ“ไฝœ็ฑปๅž‹ใ€่ต„ๆบใ€่ต„ๆบID + * 3. ่ฏทๆฑ‚ไฟกๆฏ๏ผš่ฏทๆฑ‚ๆ–นๆณ•ใ€่ฏทๆฑ‚URIใ€IPๅœฐๅ€ใ€User-Agent + * 4. ๆ“ไฝœ็ป“ๆžœ๏ผš็Šถๆ€ใ€้”™่ฏฏไฟกๆฏ + * + * ๆ“ไฝœ็ฑปๅž‹๏ผˆaction๏ผ‰๏ผš + * - LOGIN๏ผš็™ปๅฝ• + * - LOGOUT๏ผš็™ปๅ‡บ + * - CREATE๏ผšๅˆ›ๅปบ + * - UPDATE๏ผšๆ›ดๆ–ฐ + * - DELETE๏ผšๅˆ ้™ค + * + * ่ต„ๆบ็ฑปๅž‹๏ผˆresource๏ผ‰๏ผš + * - user๏ผš็”จๆˆท + * - role๏ผš่ง’่‰ฒ + * - menu๏ผš่œๅ• + * - permission๏ผšๆƒ้™ + * + * @author Claude + * @since 2024-04-09 + */ +@Entity +@Table(name = "sys_audit_log") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SysAuditLog { + + /** + * ๆ—ฅๅฟ—ID๏ผˆไธป้”ฎ๏ผ‰ + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * ๆ“ไฝœ็”จๆˆทID + */ + @Column(name = "user_id") + private Long userId; + + /** + * ๆ“ไฝœ็”จๆˆทๅ + */ + @Column(name = "username", length = 50) + private String username; + + /** + * ๆ“ไฝœ็ฑปๅž‹๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - LOGIN๏ผš็™ปๅฝ• + * - LOGOUT๏ผš็™ปๅ‡บ + * - CREATE๏ผšๅˆ›ๅปบ + * - UPDATE๏ผšๆ›ดๆ–ฐ + * - DELETE๏ผšๅˆ ้™ค + */ + @Column(name = "action", nullable = false, length = 50) + private String action; + + /** + * ๆ“ไฝœ่ต„ๆบ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * ๅฆ‚๏ผšuser, role, menu, permission + */ + @Column(name = "resource", nullable = false, length = 100) + private String resource; + + /** + * ่ต„ๆบID + * ่ขซๆ“ไฝœ่ต„ๆบ็š„ID + */ + @Column(name = "resource_id", length = 50) + private String resourceId; + + /** + * ๆ“ไฝœๆ่ฟฐ + * ไบบ็ฑปๅฏ่ฏป็š„ๆ“ไฝœๆ่ฟฐ + */ + @Column(name = "description", length = 500) + private String description; + + /** + * ่ฏทๆฑ‚ๆ–นๆณ• + * - GET, POST, PUT, DELETE ็ญ‰ + */ + @Column(name = "request_method", length = 10) + private String requestMethod; + + /** + * ่ฏทๆฑ‚URI + * ่ฏทๆฑ‚็š„ๅฎŒๆ•ด่ทฏๅพ„ + */ + @Column(name = "request_uri", length = 500) + private String requestUri; + + /** + * IP ๅœฐๅ€ + * ๆ“ไฝœ่€…็š„ IP ๅœฐๅ€๏ผŒๆ”ฏๆŒ IPv4 ๅ’Œ IPv6 + */ + @Column(name = "ip_address", length = 45) + private String ipAddress; + + /** + * User-Agent + * ๆต่งˆๅ™จๆˆ–ๅฎขๆˆท็ซฏไฟกๆฏ + */ + @Column(name = "user_agent", length = 500) + private String userAgent; + + /** + * ๆ“ไฝœ็Šถๆ€๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - 1๏ผšๆˆๅŠŸ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผšๅคฑ่ดฅ + */ + @Column(name = "status", nullable = false, columnDefinition = "TINYINT") + private Integer status = 1; + + /** + * ้”™่ฏฏไฟกๆฏ + * ๆ“ไฝœๅคฑ่ดฅๆ—ถ็š„้”™่ฏฏๆ่ฟฐ + */ + @Column(name = "error_message", length = 1000) + private String errorMessage; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด๏ผˆไธๅฏไธบ็ฉบ๏ผŒไธๅฏๆ›ดๆ–ฐ๏ผ‰ + */ + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * ๅˆ›ๅปบๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/aisi/template/domain/entity/SysMenu.java b/src/main/java/com/aisi/template/domain/entity/SysMenu.java new file mode 100644 index 0000000..341bb24 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/entity/SysMenu.java @@ -0,0 +1,172 @@ +package com.aisi.template.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * ็ณป็ปŸ่œๅ•ๅฎžไฝ“็ฑป + * ๅฎšไน‰็ณป็ปŸ็š„่œๅ•็ป“ๆž„ๅ’Œๅฏผ่ˆช + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผš่œๅ•ๅ็งฐใ€่œๅ•็ฑปๅž‹ + * 2. ๆ ‘ๅฝข็ป“ๆž„๏ผš็ˆถ่œๅ•ID + * 3. ่œๅ•้…็ฝฎ๏ผš่ทฏๅพ„ใ€็ป„ไปถใ€ๅ›พๆ ‡ใ€ๆŽ’ๅบ + * 4. ๆ˜พ็คบๆŽงๅˆถ๏ผšๅฏ่งๆ€งใ€ๅฏ็”จ็Šถๆ€ + * 5. ๆƒ้™ๆŽงๅˆถ๏ผšๅ…ณ่”็š„ๆƒ้™็ผ–็  + * 6. ่ง’่‰ฒๅ…ณ็ณป๏ผšๅคšๅฏนๅคšๅ…ณ่”่ง’่‰ฒ + * + * ่œๅ•็ฑปๅž‹๏ผš + * - DIRECTORY(1)๏ผš็›ฎๅฝ•๏ผŒไป…็”จไบŽๅˆ†็ป„๏ผŒไธๅฏนๅบ”ๅ…ทไฝ“้กต้ข + * - MENU(2)๏ผš่œๅ•้กน๏ผŒๅฏนๅบ”ๅ…ทไฝ“้กต้ข + * - BUTTON(3)๏ผšๆŒ‰้’ฎ๏ผŒ้กต้ขๅ†…็š„ๆ“ไฝœๆŒ‰้’ฎ + * + * @author Claude + * @since 2024-04-09 + */ +@Entity +@Table(name = "sys_menu") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class SysMenu { + + /** + * ่œๅ•ID๏ผˆไธป้”ฎ๏ผ‰ + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + private Long id; + + /** + * ็ˆถ่œๅ•ID + * - 0 ๆˆ– null๏ผšๆ น่œๅ• + * - ๅ…ถไป–ๅ€ผ๏ผšๅญ่œๅ• + */ + @Column(name = "parent_id", nullable = false) + private Long parentId = 0L; + + /** + * ่œๅ•ๅ็งฐ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + */ + @Column(name = "menu_name", nullable = false, length = 50) + private String menuName; + + /** + * ่œๅ•็ฑปๅž‹๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - 1๏ผš็›ฎๅฝ•๏ผˆDIRECTORY๏ผ‰ + * - 2๏ผš่œๅ•๏ผˆMENU๏ผ‰ + * - 3๏ผšๆŒ‰้’ฎ๏ผˆBUTTON๏ผ‰ + */ + @Column(name = "menu_type", nullable = false) + private Integer menuType; + + /** + * ่œๅ•่ทฏๅพ„ + * ็”จไบŽๅ‰็ซฏ่ทฏ็”ฑ๏ผŒๅฆ‚๏ผš/user, /role + */ + @Column(name = "menu_path", length = 200) + private String menuPath; + + /** + * ็ป„ไปถ่ทฏๅพ„ + * ๅ‰็ซฏ็ป„ไปถๆ–‡ไปถ่ทฏๅพ„๏ผŒๅฆ‚๏ผšviews/user/List.vue + */ + @Column(name = "component", length = 200) + private String component; + + /** + * ่œๅ•ๅ›พๆ ‡ + * ๅ›พๆ ‡ๅ็งฐ๏ผŒๅฆ‚๏ผšuser, role, setting + */ + @Column(name = "icon", length = 100) + private String icon; + + /** + * ๆŽ’ๅบๅญ—ๆฎต + * ๆ•ฐๅ€ผ่ถŠๅฐ่ถŠ้ ๅ‰ + */ + @Column(name = "sort_order", nullable = false) + private Integer sortOrder = 0; + + /** + * ๅฏ่งๆ€ง + * - 1๏ผšๅฏ่ง๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผšไธๅฏ่ง + */ + @Column(name = "visible", nullable = false) + private Integer visible = 1; + + /** + * ่œๅ•็Šถๆ€ + * - 1๏ผšๅฏ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ + @Column(name = "status", nullable = false) + private Integer status = 1; + + /** + * ๆƒ้™็ผ–็  + * ็”จไบŽๆŽงๅˆถ่œๅ•็š„่ฎฟ้—ฎๆƒ้™ + */ + @Column(name = "permission_code", length = 100) + private String permissionCode; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + /** + * ๆ‹ฅๆœ‰ๆญค่œๅ•็š„่ง’่‰ฒๅˆ—่กจ๏ผˆๅคšๅฏนๅคšๅ…ณ็ณป็š„ๅๅ‘็ปดๆŠค๏ผ‰ + * - mappedBy = "menus"๏ผš็”ฑ SysRole ็š„ menus ๅญ—ๆฎต็ปดๆŠคๅ…ณ็ณป + * - ๆ‡’ๅŠ ่ฝฝ๏ผš้ฟๅ…ๆŸฅ่ฏข่œๅ•ๆ—ถๅŠ ่ฝฝๆ‰€ๆœ‰่ง’่‰ฒ + */ + @ManyToMany(mappedBy = "menus", fetch = FetchType.LAZY) + @ToString.Exclude + private Set roles = new HashSet<>(); + + /** + * ๅญ่œๅ•ๅˆ—่กจ๏ผˆ้žๆŒไน…ๅŒ–ๅญ—ๆฎต๏ผ‰ + * - @Transient๏ผšไธๆ˜ ๅฐ„ๅˆฐๆ•ฐๆฎๅบ“ + * - ็”จไบŽๆž„ๅปบ่œๅ•ๆ ‘ๅฝข็ป“ๆž„ + */ + @Transient + @ToString.Exclude + private List children = new ArrayList<>(); + + /** + * ๅˆ›ๅปบๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + /** + * ๆ›ดๆ–ฐๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/aisi/template/domain/entity/SysPermission.java b/src/main/java/com/aisi/template/domain/entity/SysPermission.java new file mode 100644 index 0000000..b7b29d7 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/entity/SysPermission.java @@ -0,0 +1,133 @@ +package com.aisi.template.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +/** + * ็ณป็ปŸๆƒ้™ๅฎžไฝ“็ฑป + * ๅฎšไน‰็ณป็ปŸ็š„่ต„ๆบๆ“ไฝœๆƒ้™ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผšๆƒ้™็ผ–็ ใ€ๆƒ้™ๅ็งฐ + * 2. ่ต„ๆบๆ“ไฝœ๏ผš่ต„ๆบๅ็งฐใ€ๆ“ไฝœ็ฑปๅž‹ + * 3. ๆ่ฟฐๅ’Œ็Šถๆ€๏ผšๆ่ฟฐไฟกๆฏใ€ๅฏ็”จ็Šถๆ€ + * 4. ่ง’่‰ฒๅ…ณ็ณป๏ผšๅคšๅฏนๅคšๅ…ณ่”่ง’่‰ฒ + * + * ๆƒ้™ๆ ผๅผ๏ผš + * - ่ต„ๆบ:ๆ“ไฝœ๏ผŒๅฆ‚ user:read, user:write + * - ่ต„ๆบ๏ผš็ณป็ปŸไธญ็š„ๅฎžไฝ“๏ผŒๅฆ‚ user, role, permission, menu + * - ๆ“ไฝœ๏ผšread๏ผˆ่ฏปๅ–๏ผ‰ใ€write๏ผˆๅ†™ๅ…ฅ๏ผ‰ใ€delete๏ผˆๅˆ ้™ค๏ผ‰ + * + * ่ฎพ่ฎก่ฏดๆ˜Ž๏ผš + * - ๆƒ้™ๆ˜ฏ็ณป็ปŸ้ข„ๅฎšไน‰็š„๏ผŒ้€š่ฟ‡ๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌๅˆๅง‹ๅŒ– + * - ๆƒ้™้€š่ฟ‡่ง’่‰ฒๅˆ†้…็ป™็”จๆˆท + * + * @author Claude + * @since 2024-04-09 + */ +@Entity +@Table(name = "sys_permission") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class SysPermission { + + /** + * ๆƒ้™ID๏ผˆไธป้”ฎ๏ผ‰ + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + private Long id; + + /** + * ๆƒ้™็ผ–็ ๏ผˆๅ”ฏไธ€๏ผŒไธๅฏไธบ็ฉบ๏ผ‰ + * ๆ ผๅผ๏ผš่ต„ๆบ:ๆ“ไฝœ๏ผŒๅฆ‚ user:read, role:write + */ + @Column(name = "permission_code", nullable = false, unique = true, length = 100) + private String permissionCode; + + /** + * ๆƒ้™ๅ็งฐ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * ็”จไบŽ็•Œ้ขๅฑ•็คบ๏ผŒๅฆ‚๏ผšๆŸฅ็œ‹็”จๆˆทใ€็ผ–่พ‘็”จๆˆท + */ + @Column(name = "permission_name", nullable = false, length = 100) + private String permissionName; + + /** + * ่ต„ๆบๅ็งฐ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * ๅฆ‚๏ผšuser๏ผˆ็”จๆˆท๏ผ‰ใ€role๏ผˆ่ง’่‰ฒ๏ผ‰ใ€menu๏ผˆ่œๅ•๏ผ‰ + */ + @Column(name = "resource", nullable = false, length = 50) + private String resource; + + /** + * ๆ“ไฝœ็ฑปๅž‹๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * - read๏ผš่ฏปๅ–๏ผˆๆŸฅ็œ‹๏ผ‰ + * - write๏ผšๅ†™ๅ…ฅ๏ผˆๅˆ›ๅปบ/็ผ–่พ‘๏ผ‰ + * - delete๏ผšๅˆ ้™ค + */ + @Column(name = "action", nullable = false, length = 50) + private String action; + + /** + * ๆƒ้™ๆ่ฟฐ + */ + @Column(name = "description", length = 500) + private String description; + + /** + * ๆƒ้™็Šถๆ€ + * - 1๏ผšๅฏ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ + @Column(name = "status", nullable = false) + private Integer status = 1; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + /** + * ๆ‹ฅๆœ‰ๆญคๆƒ้™็š„่ง’่‰ฒๅˆ—่กจ๏ผˆๅคšๅฏนๅคšๅ…ณ็ณป็š„ๅๅ‘็ปดๆŠค๏ผ‰ + * - mappedBy = "permissions"๏ผš็”ฑ SysRole ็š„ permissions ๅญ—ๆฎต็ปดๆŠคๅ…ณ็ณป + * - ๆ‡’ๅŠ ่ฝฝ๏ผš้ฟๅ…ๆŸฅ่ฏขๆƒ้™ๆ—ถๅŠ ่ฝฝๆ‰€ๆœ‰่ง’่‰ฒ + */ + @ManyToMany(mappedBy = "permissions", fetch = FetchType.LAZY) + @ToString.Exclude + private Set roles = new HashSet<>(); + + /** + * ๅˆ›ๅปบๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + /** + * ๆ›ดๆ–ฐๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/aisi/template/domain/entity/SysRole.java b/src/main/java/com/aisi/template/domain/entity/SysRole.java new file mode 100644 index 0000000..b1dea30 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/entity/SysRole.java @@ -0,0 +1,139 @@ +package com.aisi.template.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +/** + * ็ณป็ปŸ่ง’่‰ฒๅฎžไฝ“็ฑป + * ๅฎšไน‰่ง’่‰ฒ็š„ๅŸบๆœฌไฟกๆฏๅ’Œๆƒ้™/่œๅ•ๅ…ณ็ณป + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผš่ง’่‰ฒ็ผ–็ ใ€่ง’่‰ฒๅ็งฐใ€ๆ่ฟฐ + * 2. ๆŽ’ๅบๅ’Œ็Šถๆ€๏ผšๆŽ’ๅบๅญ—ๆฎตใ€ๅฏ็”จ็Šถๆ€ + * 3. ๆƒ้™ๅ…ณ็ณป๏ผšๅคšๅฏนๅคšๅ…ณ่”ๆƒ้™ + * 4. ่œๅ•ๅ…ณ็ณป๏ผšๅคšๅฏนๅคšๅ…ณ่”่œๅ• + * + * ่ง’่‰ฒ่ฏดๆ˜Ž๏ผš + * - ่ง’่‰ฒๆ˜ฏๆƒ้™ๅ’Œ่œๅ•็š„้›†ๅˆ + * - ็”จๆˆท้€š่ฟ‡่ง’่‰ฒ่Žทๅพ—ๆƒ้™ๅ’Œ่œๅ•่ฎฟ้—ฎๆƒ้™ + * - ้ข„ๅฎšไน‰่ง’่‰ฒ๏ผšSUPER_ADMIN๏ผˆ่ถ…็บง็ฎก็†ๅ‘˜๏ผ‰ใ€ADMIN๏ผˆ็ฎก็†ๅ‘˜๏ผ‰ใ€USER๏ผˆๆ™ฎ้€š็”จๆˆท๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Entity +@Table(name = "sys_role") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +public class SysRole { + + /** + * ่ง’่‰ฒID๏ผˆไธป้”ฎ๏ผ‰ + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include + private Long id; + + /** + * ่ง’่‰ฒ็ผ–็ ๏ผˆๅ”ฏไธ€๏ผŒไธๅฏไธบ็ฉบ๏ผ‰ + * ๆ ผๅผ๏ผšROLE_ + ๅ็งฐ๏ผŒๅฆ‚๏ผšROLE_USER, ROLE_ADMIN + */ + @Column(name = "role_code", nullable = false, unique = true, length = 50) + private String roleCode; + + /** + * ่ง’่‰ฒๅ็งฐ๏ผˆไธๅฏไธบ็ฉบ๏ผ‰ + * ็”จไบŽ็•Œ้ขๅฑ•็คบ๏ผŒๅฆ‚๏ผšๆ™ฎ้€š็”จๆˆทใ€็ฎก็†ๅ‘˜ + */ + @Column(name = "role_name", nullable = false, length = 100) + private String roleName; + + /** + * ่ง’่‰ฒๆ่ฟฐ + */ + @Column(name = "description", length = 500) + private String description; + + /** + * ๆŽ’ๅบๅญ—ๆฎต + * ๆ•ฐๅ€ผ่ถŠๅฐ่ถŠ้ ๅ‰ + */ + @Column(name = "sort_order", nullable = false) + private Integer sortOrder = 0; + + /** + * ่ง’่‰ฒ็Šถๆ€ + * - 1๏ผšๅฏ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ + @Column(name = "status", nullable = false) + private Integer status = 1; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + /** + * ่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆƒ้™ๅˆ—่กจ๏ผˆๅคšๅฏนๅคšๅ…ณ็ณป๏ผ‰ + * - ๆ‡’ๅŠ ่ฝฝ๏ผš้ฟๅ…ๆŸฅ่ฏข่ง’่‰ฒๆ—ถๅŠ ่ฝฝๆ‰€ๆœ‰ๆƒ้™ + * - JoinTable๏ผšๅ…ณ่”่กจ sys_role_permission + */ + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_role_permission", + joinColumns = @JoinColumn(name = "role_id"), + inverseJoinColumns = @JoinColumn(name = "permission_id") + ) + @ToString.Exclude + private Set permissions = new HashSet<>(); + + /** + * ่ง’่‰ฒๆ‹ฅๆœ‰็š„่œๅ•ๅˆ—่กจ๏ผˆๅคšๅฏนๅคšๅ…ณ็ณป๏ผ‰ + * - ๆ‡’ๅŠ ่ฝฝ๏ผš้ฟๅ…ๆŸฅ่ฏข่ง’่‰ฒๆ—ถๅŠ ่ฝฝๆ‰€ๆœ‰่œๅ• + * - JoinTable๏ผšๅ…ณ่”่กจ sys_role_menu + */ + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_role_menu", + joinColumns = @JoinColumn(name = "role_id"), + inverseJoinColumns = @JoinColumn(name = "menu_id") + ) + @ToString.Exclude + private Set menus = new HashSet<>(); + + /** + * ๅˆ›ๅปบๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + /** + * ๆ›ดๆ–ฐๅ‰่‡ชๅŠจ่ฎพ็ฝฎๆ—ถ้—ด + */ + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/aisi/template/domain/entity/User.java b/src/main/java/com/aisi/template/domain/entity/User.java index fc0af4d..28de263 100644 --- a/src/main/java/com/aisi/template/domain/entity/User.java +++ b/src/main/java/com/aisi/template/domain/entity/User.java @@ -1,50 +1,129 @@ package com.aisi.template.domain.entity; -import com.aisi.template.domain.enums.Role; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.persistence.*; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.ToString; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +/** + * ็”จๆˆทๅฎžไฝ“็ฑป + * ๅฎšไน‰็”จๆˆท็š„ๅŸบๆœฌไฟกๆฏๅ’Œ่ดฆๆˆท็Šถๆ€ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผš็”จๆˆทๅใ€ๅฏ†็ ใ€้‚ฎ็ฎฑ + * 2. ่ดฆๆˆท็Šถๆ€๏ผšๅฏ็”จ/็ฆ็”จใ€้”ๅฎš็Šถๆ€ + * 3. ๅฎ‰ๅ…จไฟกๆฏ๏ผšๅคฑ่ดฅๆฌกๆ•ฐใ€้”ๅฎšๆ—ถ้—ดใ€ๅฏ†็ ไฟฎๆ”นๆ—ถ้—ด + * 4. ่ง’่‰ฒๅ…ณ็ณป๏ผšๅคšๅฏนๅคšๅ…ณ่”่ง’่‰ฒ + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ๅฏ†็ ไฝฟ็”จ BCrypt ๅŠ ๅฏ†ๅญ˜ๅ‚จ + * - ่ฟž็ปญๅคฑ่ดฅ 5 ๆฌก้”ๅฎš่ดฆๆˆท 30 ๅˆ†้’Ÿ + * - ๆ”ฏๆŒๆฐธไน…้”ๅฎš๏ผˆ็ฆ็”จ่ดฆๆˆท๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ @Entity @Table(name = "users") @Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @EntityListeners(AuditingEntityListener.class) public class User { + /** + * ็”จๆˆทID๏ผˆไธป้”ฎ๏ผ‰ + */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @EqualsAndHashCode.Include private Long id; + /** + * ็”จๆˆทๅ๏ผˆๅ”ฏไธ€๏ผŒไธๅฏไธบ็ฉบ๏ผ‰ + */ @Column(nullable = false, unique = true, length = 50) private String username; + /** + * ๅฏ†็ ๏ผˆBCrypt ๅŠ ๅฏ†๏ผŒไธๅฏไธบ็ฉบ๏ผ‰ + * ไฝฟ็”จ @JsonIgnore ้˜ฒๆญขๅบๅˆ—ๅŒ–ๆ—ถๆณ„้œฒๅฏ†็  + * ไฝฟ็”จ @ToString.Exclude ้˜ฒๆญข toString ๆ—ถๆณ„้œฒๅฏ†็  + */ @ToString.Exclude @JsonIgnore @Column(nullable = false) private String password; + /** + * ้‚ฎ็ฎฑๅœฐๅ€๏ผˆๅ”ฏไธ€๏ผ‰ + */ @Column(unique = true) private String email; + /** + * ็”จๆˆท็Šถๆ€ + * - 1๏ผšๅฏ็”จ๏ผˆ้ป˜่ฎค๏ผ‰ + * - 0๏ผš็ฆ็”จ๏ผˆๆฐธไน…้”ๅฎš๏ผ‰ + */ @Column(name = "status", nullable = false, columnDefinition = "TINYINT DEFAULT 1 COMMENT '1=ๆญฃๅธธ 0=็ฆ็”จ'") private Integer status = 1; - @Column(name = "role", nullable = false, length = 20, columnDefinition = "VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT '็”จๆˆท่ง’่‰ฒ๏ผšUSER=ๆ™ฎ้€š็”จๆˆท๏ผŒADMIN=็ฎก็†ๅ‘˜'") - @Enumerated(EnumType.STRING) - private Role role = Role.USER; + /** + * ็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ + * ่ฟž็ปญๅคฑ่ดฅ 5 ๆฌกๅฐ†้”ๅฎš่ดฆๆˆท + */ + @Column(name = "failed_login_count", nullable = false) + private Integer failedLoginCount = 0; + /** + * ่ดฆๆˆท้”ๅฎšๅˆฐๆœŸๆ—ถ้—ด + * - null๏ผšๆœช้”ๅฎš + * - ๆœ‰ๅ€ผ๏ผš้”ๅฎšๅˆฐๆญคๆ—ถ้—ด + */ + @Column(name = "locked_until") + private LocalDateTime lockedUntil; + + /** + * ๅฏ†็ ๆœ€ๅŽไฟฎๆ”นๆ—ถ้—ด + * ็”จไบŽๅฏ†็ ่ฟ‡ๆœŸ็ญ–็•ฅ + */ + @Column(name = "password_changed_at") + private LocalDateTime passwordChangedAt; + + /** + * ็”จๆˆท่ง’่‰ฒๅˆ—่กจ๏ผˆๅคšๅฏนๅคšๅ…ณ็ณป๏ผ‰ + * - ๆ‡’ๅŠ ่ฝฝ๏ผš้ฟๅ…ๆŸฅ่ฏข็”จๆˆทๆ—ถๅŠ ่ฝฝๆ‰€ๆœ‰่ง’่‰ฒ + * - JoinTable๏ผšๅ…ณ่”่กจ sys_user_role + */ + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable( + name = "sys_user_role", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + @ToString.Exclude + private Set roles = new HashSet<>(); + + /** + * ๅˆ›ๅปบๆ—ถ้—ด๏ผˆ่‡ชๅŠจๅกซๅ……๏ผ‰ + */ @CreatedDate @Column(name = "created_at", nullable = false, updatable = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private LocalDateTime createdAt; + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด๏ผˆ่‡ชๅŠจๅกซๅ……๏ผ‰ + */ @LastModifiedDate @Column(name = "updated_at", nullable = false) @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") @@ -52,8 +131,38 @@ public class User { /** * ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅฏ็”จ + * + * @return true ่กจ็คบ่ดฆๆˆทๅฏ็”จ */ public boolean isEnabled() { return status != null && status == 1; } + + /** + * ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + * ่ฏดๆ˜Ž๏ผš + * - ๅฆ‚ๆžœ้”ๅฎšๆ—ถ้—ดไธบ็ฉบ๏ผŒ่กจ็คบๆœช้”ๅฎš + * - ๅฆ‚ๆžœๅฝ“ๅ‰ๆ—ถ้—ดๅœจ้”ๅฎšๆ—ถ้—ดไน‹ๅ‰๏ผŒ่กจ็คบไป็„ถ้”ๅฎš + * - ้”ๅฎšไผš่‡ชๅŠจ่ฟ‡ๆœŸ๏ผˆไธดๆ—ถ้”ๅฎš๏ผ‰ + * + * @return true ่กจ็คบ่ดฆๆˆท่ขซ้”ๅฎš + */ + public boolean isLocked() { + if (lockedUntil == null) { + return false; + } + return LocalDateTime.now().isBefore(lockedUntil); + } + + /** + * ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซๆฐธไน…้”ๅฎš๏ผˆ็ฆ็”จ๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - status = 0 ่กจ็คบ่ดฆๆˆท่ขซ็ฎก็†ๅ‘˜็ฆ็”จ + * - ไธๅŒไบŽไธดๆ—ถ้”ๅฎš๏ผŒๆฐธไน…้”ๅฎš้œ€่ฆ็ฎก็†ๅ‘˜ๆ‰‹ๅŠจ่งฃ้™ค + * + * @return true ่กจ็คบ่ดฆๆˆท่ขซๆฐธไน…้”ๅฎš + */ + public boolean isPermanentlyLocked() { + return status != null && status == 0; + } } diff --git a/src/main/java/com/aisi/template/domain/enums/ErrorCode.java b/src/main/java/com/aisi/template/domain/enums/ErrorCode.java deleted file mode 100644 index ae1e723..0000000 --- a/src/main/java/com/aisi/template/domain/enums/ErrorCode.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.aisi.template.domain.enums; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum ErrorCode { - - // ้€š็”จ้”™่ฏฏ - PARAM_ERROR(400, "ๅ‚ๆ•ฐ้”™่ฏฏ"), - SYSTEM_ERROR(500, "็ณป็ปŸ็นๅฟ™๏ผŒ่ฏท็จๅŽๅ†่ฏ•"), - - // ็”จๆˆท็›ธๅ…ณ (1000 - 1999) - USER_NOT_FOUND(1001, "็”จๆˆทไธๅญ˜ๅœจ"), - PASSWORD_ERROR(1002, "ๅฏ†็ ้”™่ฏฏ"), - USER_LOCKED(1003, "่ดฆๅทๅทฒ่ขซ้”ๅฎš"), - - // ไธšๅŠก็›ธๅ…ณ (2000 - 2999) - ้ข„็•™็ป™ๅ…ทไฝ“ไธšๅŠกๆจกๅ— - ; - - private final int code; - private final String message; -} \ No newline at end of file diff --git a/src/main/java/com/aisi/template/domain/enums/Role.java b/src/main/java/com/aisi/template/domain/enums/Role.java deleted file mode 100644 index 0d5c0b9..0000000 --- a/src/main/java/com/aisi/template/domain/enums/Role.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.aisi.template.domain.enums; - -import lombok.Getter; - -/** - * ็”จๆˆท่ง’่‰ฒๆžšไธพ - */ -@Getter -public enum Role { - - /** - * ๆ™ฎ้€š็”จๆˆท - */ - USER("ROLE_USER", "ๆ™ฎ้€š็”จๆˆท"), - - /** - * ็ฎก็†ๅ‘˜ - */ - ADMIN("ROLE_ADMIN", "็ฎก็†ๅ‘˜"); - - private final String authority; - private final String description; - - Role(String authority, String description) { - this.authority = authority; - this.description = description; - } - - /** - * ไปŽๆ•ฐๆฎๅบ“ๅญ—็ฌฆไธฒ่ฝฌๆขไธบๆžšไธพ - */ - public static Role fromString(String role) { - if (role == null) { - return USER; - } - try { - return Role.valueOf(role.toUpperCase()); - } catch (IllegalArgumentException e) { - return USER; - } - } - - /** - * ่ฝฌๆขไธบๆ•ฐๆฎๅบ“ๅญ˜ๅ‚จๆ ผๅผ - */ - public String toDbValue() { - return this.name(); - } -} diff --git a/src/main/java/com/aisi/template/domain/vo/LoginResponseVo.java b/src/main/java/com/aisi/template/domain/vo/LoginResponseVo.java new file mode 100644 index 0000000..965c547 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/vo/LoginResponseVo.java @@ -0,0 +1,94 @@ +package com.aisi.template.domain.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * ็™ปๅฝ•ๅ“ๅบ”่ง†ๅ›พๅฏน่ฑก + * ็”จไบŽ่ฟ”ๅ›ž็™ปๅฝ•ๆˆๅŠŸๅŽ็š„ๅ“ๅบ”ๆ•ฐๆฎ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. Token ไฟกๆฏ๏ผš่ฎฟ้—ฎไปค็‰Œใ€ๅˆทๆ–ฐไปค็‰Œใ€่ฟ‡ๆœŸๆ—ถ้—ด + * 2. ็”จๆˆทไฟกๆฏ๏ผš็”จๆˆทIDใ€็”จๆˆทๅใ€้‚ฎ็ฎฑ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆท็™ปๅฝ•ๆˆๅŠŸๅŽ่ฟ”ๅ›ž + * - ็”จๆˆทๆณจๅ†ŒๆˆๅŠŸๅŽ่ฟ”ๅ›ž๏ผˆ่‡ชๅŠจ็™ปๅฝ•๏ผ‰ + * - Token ๅˆทๆ–ฐๅŽ่ฟ”ๅ›ž + * + * @author Claude + * @since 2024-04-09 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "็™ปๅฝ•ๅ“ๅบ”") +public class LoginResponseVo { + + /** + * ่ฎฟ้—ฎไปค็‰Œ๏ผˆAccess Token๏ผ‰ + * - ็”จไบŽ API ่ฎฟ้—ฎ่ฎค่ฏ + * - ๆ”พๅœจ่ฏทๆฑ‚ๅคด๏ผšAuthorization: Bearer {accessToken} + * - ้ป˜่ฎคๆœ‰ๆ•ˆๆœŸ๏ผš1 ๅฐๆ—ถ + */ + @Schema(description = "่ฎฟ้—ฎไปค็‰Œ") + private String accessToken; + + /** + * ๅˆทๆ–ฐไปค็‰Œ๏ผˆRefresh Token๏ผ‰ + * - ็”จไบŽ่Žทๅ–ๆ–ฐ็š„ Access Token + * - ๅฝ“ Access Token ่ฟ‡ๆœŸๆ—ถไฝฟ็”จ + * - ้ป˜่ฎคๆœ‰ๆ•ˆๆœŸ๏ผš7 ๅคฉ + */ + @Schema(description = "ๅˆทๆ–ฐไปค็‰Œ") + private String refreshToken; + + /** + * ่ฎฟ้—ฎไปค็‰Œ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * - ้ป˜่ฎค๏ผš3600 ็ง’๏ผˆ1 ๅฐๆ—ถ๏ผ‰ + * - ๅ‰็ซฏๅฏๆ นๆฎๆญคๆ—ถ้—ดๆๅ‰ๅˆทๆ–ฐ Token + */ + @Schema(description = "่ฎฟ้—ฎไปค็‰Œ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰") + private Long expiresIn; + + /** + * ็”จๆˆทไฟกๆฏ + * - ๅŒ…ๅซ็”จๆˆท็š„ๅŸบๆœฌไฟกๆฏ + * - ๅ‰็ซฏๅฏๆ˜พ็คบ็”จๆˆทๅ็ญ‰ไฟกๆฏ + */ + @Schema(description = "็”จๆˆทไฟกๆฏ") + private UserInfoVo userInfo; + + /** + * ็”จๆˆทไฟกๆฏ่ง†ๅ›พๅฏน่ฑก + * ๅŒ…ๅซ็”จๆˆท็š„ๅŸบๆœฌไฟกๆฏ + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "็”จๆˆทไฟกๆฏ") + public static class UserInfoVo { + /** + * ็”จๆˆทID + */ + @Schema(description = "็”จๆˆทID") + private Long id; + + /** + * ็”จๆˆทๅ + */ + @Schema(description = "็”จๆˆทๅ") + private String username; + + /** + * ้‚ฎ็ฎฑๅœฐๅ€ + */ + @Schema(description = "้‚ฎ็ฎฑ") + private String email; + } +} diff --git a/src/main/java/com/aisi/template/domain/vo/MenuVo.java b/src/main/java/com/aisi/template/domain/vo/MenuVo.java new file mode 100644 index 0000000..2afda37 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/vo/MenuVo.java @@ -0,0 +1,228 @@ +package com.aisi.template.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * ่œๅ•่ง†ๅ›พๅฏน่ฑก + * ็”จไบŽ่ฟ”ๅ›ž่œๅ•ไฟกๆฏ็ป™ๅ‰็ซฏ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผš่œๅ•IDใ€็ˆถ่œๅ•IDใ€่œๅ•ๅ็งฐใ€่œๅ•็ฑปๅž‹ + * 2. ่œๅ•้…็ฝฎ๏ผš่ทฏๅพ„ใ€็ป„ไปถใ€ๅ›พๆ ‡ใ€ๆŽ’ๅบ + * 3. ๆ˜พ็คบๆŽงๅˆถ๏ผšๅฏ่งๆ€งใ€ๅฏ็”จ็Šถๆ€ + * 4. ๆƒ้™ๆŽงๅˆถ๏ผšๆƒ้™็ผ–็  + * 5. ๅญ่œๅ•๏ผšๅญ่œๅ•ๅˆ—่กจ๏ผˆๆ ‘ๅฝข็ป“ๆž„๏ผ‰ + * 6. ๆ—ถ้—ดไฟกๆฏ๏ผšๅˆ›ๅปบๆ—ถ้—ดใ€ๆ›ดๆ–ฐๆ—ถ้—ด + * + * @author Claude + * @since 2024-04-09 + */ +public class MenuVo { + + /** + * ่œๅ•ID + */ + private Long id; + + /** + * ็ˆถ่œๅ•ID + * - 0 ๆˆ– null๏ผšๆ น่œๅ• + * - ๅ…ถไป–ๅ€ผ๏ผšๅญ่œๅ• + */ + private Long parentId; + + /** + * ่œๅ•ๅ็งฐ + */ + private String menuName; + + /** + * ่œๅ•็ฑปๅž‹ + * - 1๏ผš็›ฎๅฝ•๏ผˆDIRECTORY๏ผ‰ + * - 2๏ผš่œๅ•๏ผˆMENU๏ผ‰ + * - 3๏ผšๆŒ‰้’ฎ๏ผˆBUTTON๏ผ‰ + */ + private Integer menuType; + + /** + * ่œๅ•่ทฏๅพ„ + * - ็”จไบŽๅ‰็ซฏ่ทฏ็”ฑ + */ + private String menuPath; + + /** + * ็ป„ไปถ่ทฏๅพ„ + * - ๅ‰็ซฏ็ป„ไปถๆ–‡ไปถ่ทฏๅพ„ + */ + private String component; + + /** + * ่œๅ•ๅ›พๆ ‡ + * - ๅ›พๆ ‡ๅ็งฐ + */ + private String icon; + + /** + * ๆŽ’ๅบๅญ—ๆฎต + * - ๆ•ฐๅ€ผ่ถŠๅฐ่ถŠ้ ๅ‰ + */ + private Integer sortOrder; + + /** + * ๅฏ่งๆ€ง + * - 1๏ผšๅฏ่ง + * - 0๏ผšไธๅฏ่ง + */ + private Integer visible; + + /** + * ่œๅ•็Šถๆ€ + * - 1๏ผšๅฏ็”จ + * - 0๏ผš็ฆ็”จ + */ + private Integer status; + + /** + * ๆƒ้™็ผ–็  + * - ็”จไบŽๆŽงๅˆถ่œๅ•็š„่ฎฟ้—ฎๆƒ้™ + */ + private String permissionCode; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private LocalDateTime createdAt; + + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private LocalDateTime updatedAt; + + /** + * ๅญ่œๅ•ๅˆ—่กจ + * - ็”จไบŽๆž„ๅปบ่œๅ•ๆ ‘ๅฝข็ป“ๆž„ + * - ้€’ๅฝ’็ป“ๆž„ + */ + private List children; + + public MenuVo() { + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getParentId() { + return parentId; + } + + public void setParentId(Long parentId) { + this.parentId = parentId; + } + + public String getMenuName() { + return menuName; + } + + public void setMenuName(String menuName) { + this.menuName = menuName; + } + + public Integer getMenuType() { + return menuType; + } + + public void setMenuType(Integer menuType) { + this.menuType = menuType; + } + + public String getMenuPath() { + return menuPath; + } + + public void setMenuPath(String menuPath) { + this.menuPath = menuPath; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public Integer getVisible() { + return visible; + } + + public void setVisible(Integer visible) { + this.visible = visible; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getPermissionCode() { + return permissionCode; + } + + public void setPermissionCode(String permissionCode) { + this.permissionCode = permissionCode; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } +} diff --git a/src/main/java/com/aisi/template/domain/vo/PermissionVo.java b/src/main/java/com/aisi/template/domain/vo/PermissionVo.java new file mode 100644 index 0000000..7d9b897 --- /dev/null +++ b/src/main/java/com/aisi/template/domain/vo/PermissionVo.java @@ -0,0 +1,154 @@ +package com.aisi.template.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +/** + * ๆƒ้™่ง†ๅ›พๅฏน่ฑก + * ็”จไบŽ่ฟ”ๅ›žๆƒ้™ไฟกๆฏ็ป™ๅ‰็ซฏ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผšๆƒ้™IDใ€ๆƒ้™็ผ–็ ใ€ๆƒ้™ๅ็งฐ + * 2. ่ต„ๆบๆ“ไฝœ๏ผš่ต„ๆบๅ็งฐใ€ๆ“ไฝœ็ฑปๅž‹ + * 3. ๆ่ฟฐๅ’Œ็Šถๆ€๏ผšๆ่ฟฐไฟกๆฏใ€ๅฏ็”จ็Šถๆ€ + * 4. ๆ—ถ้—ดไฟกๆฏ๏ผšๅˆ›ๅปบๆ—ถ้—ดใ€ๆ›ดๆ–ฐๆ—ถ้—ด + * + * @author Claude + * @since 2024-04-09 + */ +public class PermissionVo { + + /** + * ๆƒ้™ID + */ + private Long id; + + /** + * ๆƒ้™็ผ–็  + * - ๆ ผๅผ๏ผš่ต„ๆบ:ๆ“ไฝœ + * - ็คบไพ‹๏ผšuser:read, role:write + */ + private String permissionCode; + + /** + * ๆƒ้™ๅ็งฐ + * - ็”จไบŽ็•Œ้ขๅฑ•็คบ + * - ็คบไพ‹๏ผšๆŸฅ็œ‹็”จๆˆทใ€็ผ–่พ‘่ง’่‰ฒ + */ + private String permissionName; + + /** + * ่ต„ๆบๅ็งฐ + * - ็คบไพ‹๏ผšuser, role, menu + */ + private String resource; + + /** + * ๆ“ไฝœ็ฑปๅž‹ + * - read๏ผš่ฏปๅ–๏ผˆๆŸฅ็œ‹๏ผ‰ + * - write๏ผšๅ†™ๅ…ฅ๏ผˆๅˆ›ๅปบ/็ผ–่พ‘๏ผ‰ + * - delete๏ผšๅˆ ้™ค + */ + private String action; + + /** + * ๆƒ้™ๆ่ฟฐ + */ + private String description; + + /** + * ๆƒ้™็Šถๆ€ + * - 1๏ผšๅฏ็”จ + * - 0๏ผš็ฆ็”จ + */ + private Integer status; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private LocalDateTime createdAt; + + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private LocalDateTime updatedAt; + + public PermissionVo() { + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getPermissionCode() { + return permissionCode; + } + + public void setPermissionCode(String permissionCode) { + this.permissionCode = permissionCode; + } + + public String getPermissionName() { + return permissionName; + } + + public void setPermissionName(String permissionName) { + this.permissionName = permissionName; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/src/main/java/com/aisi/template/domain/vo/RoleVo.java b/src/main/java/com/aisi/template/domain/vo/RoleVo.java new file mode 100644 index 0000000..b4c32bf --- /dev/null +++ b/src/main/java/com/aisi/template/domain/vo/RoleVo.java @@ -0,0 +1,152 @@ +package com.aisi.template.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * ่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + * ็”จไบŽ่ฟ”ๅ›ž่ง’่‰ฒไฟกๆฏ็ป™ๅ‰็ซฏ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผš่ง’่‰ฒIDใ€่ง’่‰ฒ็ผ–็ ใ€่ง’่‰ฒๅ็งฐใ€ๆ่ฟฐ + * 2. ๆŽ’ๅบๅ’Œ็Šถๆ€๏ผšๆŽ’ๅบๅญ—ๆฎตใ€ๅฏ็”จ็Šถๆ€ + * 3. ๆƒ้™ไฟกๆฏ๏ผšๅ…ณ่”็š„ๆƒ้™ๅˆ—่กจ + * 4. ๆ—ถ้—ดไฟกๆฏ๏ผšๅˆ›ๅปบๆ—ถ้—ดใ€ๆ›ดๆ–ฐๆ—ถ้—ด + * + * @author Claude + * @since 2024-04-09 + */ +public class RoleVo { + + /** + * ่ง’่‰ฒID + */ + private Long id; + + /** + * ่ง’่‰ฒ็ผ–็  + * - ๆ ผๅผ๏ผšROLE_ + ๅ็งฐ + * - ็คบไพ‹๏ผšROLE_USER, ROLE_ADMIN + */ + private String roleCode; + + /** + * ่ง’่‰ฒๅ็งฐ + * - ็”จไบŽ็•Œ้ขๅฑ•็คบ + * - ็คบไพ‹๏ผšๆ™ฎ้€š็”จๆˆทใ€็ฎก็†ๅ‘˜ + */ + private String roleName; + + /** + * ่ง’่‰ฒๆ่ฟฐ + */ + private String description; + + /** + * ๆŽ’ๅบๅญ—ๆฎต + * - ๆ•ฐๅ€ผ่ถŠๅฐ่ถŠ้ ๅ‰ + */ + private Integer sortOrder; + + /** + * ่ง’่‰ฒ็Šถๆ€ + * - 1๏ผšๅฏ็”จ + * - 0๏ผš็ฆ็”จ + */ + private Integer status; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private LocalDateTime createdAt; + + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private LocalDateTime updatedAt; + + /** + * ่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆƒ้™ๅˆ—่กจ + */ + private Set permissions; + + public RoleVo() { + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getRoleCode() { + return roleCode; + } + + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; + } + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Integer getSortOrder() { + return sortOrder; + } + + public void setSortOrder(Integer sortOrder) { + this.sortOrder = sortOrder; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public Set getPermissions() { + return permissions; + } + + public void setPermissions(Set permissions) { + this.permissions = permissions; + } +} diff --git a/src/main/java/com/aisi/template/domain/vo/UserVo.java b/src/main/java/com/aisi/template/domain/vo/UserVo.java index e9d5828..515bd7e 100644 --- a/src/main/java/com/aisi/template/domain/vo/UserVo.java +++ b/src/main/java/com/aisi/template/domain/vo/UserVo.java @@ -5,30 +5,73 @@ import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import java.time.LocalDateTime; +import java.util.Set; +/** + * ็”จๆˆท่ง†ๅ›พๅฏน่ฑก + * ็”จไบŽ่ฟ”ๅ›ž็”จๆˆทไฟกๆฏ็ป™ๅ‰็ซฏ + * + * ไธป่ฆๅญ—ๆฎต๏ผš + * 1. ๅŸบๆœฌไฟกๆฏ๏ผš็”จๆˆทIDใ€็”จๆˆทๅใ€้‚ฎ็ฎฑ + * 2. ็Šถๆ€ไฟกๆฏ๏ผšๅฏ็”จ/็ฆ็”จ็Šถๆ€ + * 3. ่ง’่‰ฒไฟกๆฏ๏ผš่ง’่‰ฒ็ผ–็ ๅˆ—่กจ + * 4. ๆ—ถ้—ดไฟกๆฏ๏ผšๅˆ›ๅปบๆ—ถ้—ดใ€ๆ›ดๆ–ฐๆ—ถ้—ด + * + * ๆณจๆ„๏ผš + * - ไธๅŒ…ๅซๅฏ†็ ็ญ‰ๆ•ๆ„Ÿไฟกๆฏ + * - ่ง’่‰ฒๅช่ฟ”ๅ›ž่ง’่‰ฒ็ผ–็ ๏ผŒไธ่ฟ”ๅ›žๅฎŒๆ•ด่ง’่‰ฒๅฏน่ฑก + * + * @author Claude + * @since 2024-04-09 + */ @Data @Schema(description = "็”จๆˆท่ง†ๅ›พๅฏน่ฑก") public class UserVo { + /** + * ็”จๆˆทID + */ @Schema(description = "็”จๆˆทID") private Long id; + /** + * ็”จๆˆทๅ + */ @Schema(description = "็”จๆˆทๅ") private String username; + /** + * ้‚ฎ็ฎฑๅœฐๅ€ + */ @Schema(description = "้‚ฎ็ฎฑ") private String email; + /** + * ็”จๆˆท็Šถๆ€ + * - 1๏ผšๆญฃๅธธ๏ผˆๅฏ็”จ๏ผ‰ + * - 0๏ผš็ฆ็”จ + */ @Schema(description = "็Šถๆ€๏ผˆ1=ๆญฃๅธธ 0=็ฆ็”จ๏ผ‰") private Integer status; - @Schema(description = "่ง’่‰ฒ๏ผˆUSER=ๆ™ฎ้€š็”จๆˆท๏ผŒADMIN=็ฎก็†ๅ‘˜๏ผ‰") - private String role; + /** + * ่ง’่‰ฒ็ผ–็ ๅˆ—่กจ + * - ็คบไพ‹๏ผš["ROLE_USER", "ROLE_ADMIN"] + * - ๅช่ฟ”ๅ›ž่ง’่‰ฒ็ผ–็ ๏ผŒไธ่ฟ”ๅ›žๅฎŒๆ•ด่ง’่‰ฒๅฏน่ฑก + */ + @Schema(description = "่ง’่‰ฒๅˆ—่กจ") + private Set roles; + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ @Schema(description = "ๅˆ›ๅปบๆ—ถ้—ด") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private LocalDateTime createdAt; + /** + * ๆ›ดๆ–ฐๆ—ถ้—ด + */ @Schema(description = "ๆ›ดๆ–ฐๆ—ถ้—ด") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private LocalDateTime updatedAt; diff --git a/src/main/java/com/aisi/template/examples/RedisUsageExample.java b/src/main/java/com/aisi/template/examples/RedisUsageExample.java new file mode 100644 index 0000000..f91d0c3 --- /dev/null +++ b/src/main/java/com/aisi/template/examples/RedisUsageExample.java @@ -0,0 +1,536 @@ +package com.aisi.template.examples; + +import com.aisi.template.domain.entity.User; +import com.aisi.template.utils.RedisCache; +import com.aisi.template.utils.RedisLock; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Redis ไฝฟ็”จ็คบไพ‹ + * ๅฑ•็คบๅ„็งๅœบๆ™ฏไธ‹็š„ Redis ไฝฟ็”จๆ–นๆณ• + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisUsageExample { + + private final RedisCache redisCache; + private final RedisLock redisLock; + + // ==================== ๅœบๆ™ฏ 1: ๅฏน่ฑก็ผ“ๅญ˜ ==================== + + /** + * ๅœบๆ™ฏ๏ผš็ผ“ๅญ˜็”จๆˆทๅฏน่ฑก + * ๆญฅ้ชค๏ผš + * 1. ๅ…ˆไปŽ็ผ“ๅญ˜่Žทๅ– + * 2. ๅฆ‚ๆžœ็ผ“ๅญ˜ไธๅญ˜ๅœจ๏ผŒไปŽๆ•ฐๆฎๅบ“ๆŸฅ่ฏข + * 3. ๅฐ†ๆŸฅ่ฏข็ป“ๆžœๅญ˜ๅ…ฅ็ผ“ๅญ˜ + * + * @param userId ็”จๆˆทID + * @return ็”จๆˆทๅฏน่ฑก + */ + public User getUserWithCache(Long userId) { + // 1. ๅฎšไน‰็ผ“ๅญ˜้”ฎ + String cacheKey = "user:" + userId; + + // 2. ๅ…ˆไปŽ็ผ“ๅญ˜่Žทๅ– + User cachedUser = redisCache.get(cacheKey, User.class); + if (cachedUser != null) { + log.info("ไปŽ็ผ“ๅญ˜่Žทๅ–็”จๆˆท - userId: {}", userId); + return cachedUser; + } + + // 3. ็ผ“ๅญ˜ไธๅญ˜ๅœจ๏ผŒไปŽๆ•ฐๆฎๅบ“ๆŸฅ่ฏข๏ผˆๆจกๆ‹Ÿ๏ผ‰ + log.info("ไปŽๆ•ฐๆฎๅบ“ๆŸฅ่ฏข็”จๆˆท - userId: {}", userId); + User user = getUserFromDatabase(userId); + + // 4. ๅญ˜ๅ…ฅ็ผ“ๅญ˜๏ผˆ30ๅˆ†้’Ÿ่ฟ‡ๆœŸ๏ผ‰ + if (user != null) { + redisCache.set(cacheKey, user, 30, TimeUnit.MINUTES); + } + + return user; + } + + /** + * ๅœบๆ™ฏ๏ผšๆ›ดๆ–ฐ็”จๆˆทๅŽๅˆ ้™ค็ผ“ๅญ˜ + * ๆญฅ้ชค๏ผš + * 1. ๆ›ดๆ–ฐๆ•ฐๆฎๅบ“ + * 2. ๅˆ ้™ค็ผ“ๅญ˜๏ผˆไธ‹ๆฌกๆŸฅ่ฏขๆ—ถ้‡ๆ–ฐๅŠ ่ฝฝ๏ผ‰ + * + * @param user ็”จๆˆทๅฏน่ฑก + */ + public void updateUser(User user) { + // 1. ๆ›ดๆ–ฐๆ•ฐๆฎๅบ“๏ผˆๆจกๆ‹Ÿ๏ผ‰ + log.info("ๆ›ดๆ–ฐๆ•ฐๆฎๅบ“ - userId: {}", user.getId()); + updateUserInDatabase(user); + + // 2. ๅˆ ้™ค็ผ“ๅญ˜ + String cacheKey = "user:" + user.getId(); + redisCache.delete(cacheKey); + log.info("ๅˆ ้™ค็”จๆˆท็ผ“ๅญ˜ - userId: {}", user.getId()); + } + + // ==================== ๅœบๆ™ฏ 2: ่ฎกๆ•ฐๅ™จ ==================== + + /** + * ๅœบๆ™ฏ๏ผšๆ–‡็ซ ้˜…่ฏปๆ•ฐ่ฎกๆ•ฐ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ Redis ๅŽŸๅญ่‡ชๅขž + * 2. ๅผ‚ๆญฅๆ‰น้‡ๆ›ดๆ–ฐๅˆฐๆ•ฐๆฎๅบ“ + * + * @param articleId ๆ–‡็ซ ID + * @return ๅฝ“ๅ‰้˜…่ฏปๆ•ฐ + */ + public long incrementArticleViewCount(Long articleId) { + // 1. ๅฎšไน‰่ฎกๆ•ฐๅ™จ้”ฎ + String counterKey = "article:view:" + articleId; + + // 2. ่‡ชๅขž๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰ + long count = redisCache.increment(counterKey, 1); + + // 3. ๆฏ100ๆฌกๆ›ดๆ–ฐไธ€ๆฌกๆ•ฐๆฎๅบ“๏ผˆๆจกๆ‹Ÿ๏ผ‰ + if (count % 100 == 0) { + log.info("ๅŒๆญฅ้˜…่ฏปๆ•ฐๅˆฐๆ•ฐๆฎๅบ“ - articleId: {}, count: {}", articleId, count); + syncViewCountToDatabase(articleId, count); + } + + return count; + } + + /** + * ๅœบๆ™ฏ๏ผš็‚น่ตž/ๅ–ๆถˆ็‚น่ตž + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ Set ๅญ˜ๅ‚จ็‚น่ตž็”จๆˆทID + * 2. ไฝฟ็”จ่ฎกๆ•ฐๅ™จ่ฎฐๅฝ•็‚น่ตžๆ•ฐ + * + * @param articleId ๆ–‡็ซ ID + * @param userId ็”จๆˆทID + * @param liked ๆ˜ฏๅฆ็‚น่ตž + */ + public void likeArticle(Long articleId, Long userId, boolean liked) { + // 1. ๅฎšไน‰็‚น่ตž้›†ๅˆ้”ฎๅ’Œ่ฎกๆ•ฐๅ™จ้”ฎ + String likeSetKey = "article:like:" + articleId; + String likeCountKey = "article:like:count:" + articleId; + + if (liked) { + // 2. ็‚น่ตž๏ผšๆทปๅŠ ็”จๆˆทๅˆฐ้›†ๅˆ๏ผŒ่ฎกๆ•ฐๅ™จ+1 + redisCache.sAdd(likeSetKey, userId); + redisCache.increment(likeCountKey, 1); + log.info("็‚น่ตžๆˆๅŠŸ - articleId: {}, userId: {}", articleId, userId); + } else { + // 3. ๅ–ๆถˆ็‚น่ตž๏ผšไปŽ้›†ๅˆ็งป้™ค็”จๆˆท๏ผŒ่ฎกๆ•ฐๅ™จ-1 + redisCache.sRemove(likeSetKey, userId); + redisCache.decrement(likeCountKey, 1); + log.info("ๅ–ๆถˆ็‚น่ตž - articleId: {}, userId: {}", articleId, userId); + } + } + + /** + * ๅœบๆ™ฏ๏ผšๆฃ€ๆŸฅๆ˜ฏๅฆๅทฒ็‚น่ตž + * + * @param articleId ๆ–‡็ซ ID + * @param userId ็”จๆˆทID + * @return ๆ˜ฏๅฆๅทฒ็‚น่ตž + */ + public boolean isArticleLiked(Long articleId, Long userId) { + String likeSetKey = "article:like:" + articleId; + return redisCache.sIsMember(likeSetKey, userId); + } + + // ==================== ๅœบๆ™ฏ 3: ๅˆ†ๅธƒๅผ้” ==================== + + /** + * ๅœบๆ™ฏ๏ผš้˜ฒๆญข้‡ๅคๆไบค + * ๆญฅ้ชค๏ผš + * 1. ๅฐ่ฏ•่Žทๅ–้” + * 2. ๅฆ‚ๆžœ่Žทๅ–ๆˆๅŠŸ๏ผŒๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + * 3. ๆœ€ๅŽ้‡Šๆ”พ้” + * + * @param orderId ่ฎขๅ•ID + */ + public void processOrder(Long orderId) { + // 1. ๅฎšไน‰้”้”ฎ + String lockKey = "order:process:" + orderId; + + // 2. ๅฐ่ฏ•่Žทๅ–้”๏ผˆ30็ง’่ฟ‡ๆœŸ๏ผ‰ + String lockValue = redisLock.tryLock(lockKey, 30); + if (lockValue == null) { + log.warn("่ฎขๅ•ๆญฃๅœจๅค„็†ไธญ๏ผŒ่ฏทๅ‹ฟ้‡ๅคๆไบค - orderId: {}", orderId); + throw new RuntimeException("่ฎขๅ•ๆญฃๅœจๅค„็†ไธญ"); + } + + try { + // 3. ๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + log.info("ๅค„็†่ฎขๅ• - orderId: {}", orderId); + processOrderBusiness(orderId); + + } finally { + // 4. ้‡Šๆ”พ้” + redisLock.unlock(lockKey, lockValue); + log.info("้‡Šๆ”พ่ฎขๅ•้” - orderId: {}", orderId); + } + } + + /** + * ๅœบๆ™ฏ๏ผšๅบ“ๅญ˜ๆ‰ฃๅ‡๏ผˆ้˜ฒๆญข่ถ…ๅ–๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅˆ†ๅธƒๅผ้” + * 2. ๆฃ€ๆŸฅๅบ“ๅญ˜ + * 3. ๆ‰ฃๅ‡ๅบ“ๅญ˜ + * 4. ้‡Šๆ”พ้” + * + * @param productId ๅ•†ๅ“ID + * @param quantity ๆ•ฐ้‡ + * @return ๆ˜ฏๅฆๆ‰ฃๅ‡ๆˆๅŠŸ + */ + public boolean deductStock(Long productId, int quantity) { + // 1. ๅฎšไน‰้”้”ฎ + String lockKey = "product:stock:" + productId; + + // 2. ๅฐ่ฏ•่Žทๅ–้”๏ผˆ10็ง’่ฟ‡ๆœŸ๏ผŒ็ญ‰ๅพ…5็ง’๏ผ‰ + String lockValue = redisLock.tryLock(lockKey, 10, 5000); + if (lockValue == null) { + log.warn("่Žทๅ–ๅบ“ๅญ˜้”ๅคฑ่ดฅ๏ผŒ่ฏท็จๅŽ้‡่ฏ• - productId: {}", productId); + return false; + } + + try { + // 3. ่Žทๅ–ๅฝ“ๅ‰ๅบ“ๅญ˜ + String stockKey = "product:stock:" + productId; + Object stockObj = redisCache.get(stockKey); + int currentStock = stockObj != null ? (Integer) stockObj : getStockFromDatabase(productId); + + // 4. ๆฃ€ๆŸฅๅบ“ๅญ˜ๆ˜ฏๅฆๅ……่ถณ + if (currentStock < quantity) { + log.warn("ๅบ“ๅญ˜ไธ่ถณ - productId: {}, current: {}, required: {}", + productId, currentStock, quantity); + return false; + } + + // 5. ๆ‰ฃๅ‡ๅบ“ๅญ˜ + int newStock = currentStock - quantity; + redisCache.set(stockKey, newStock, 1, TimeUnit.HOURS); + + // 6. ๅผ‚ๆญฅๆ›ดๆ–ฐๆ•ฐๆฎๅบ“ + updateStockInDatabase(productId, newStock); + + log.info("ๅบ“ๅญ˜ๆ‰ฃๅ‡ๆˆๅŠŸ - productId: {}, old: {}, new: {}", + productId, currentStock, newStock); + return true; + + } finally { + // 7. ้‡Šๆ”พ้” + redisLock.unlock(lockKey, lockValue); + } + } + + // ==================== ๅœบๆ™ฏ 4: ๆŽ’่กŒๆฆœ ==================== + + /** + * ๅœบๆ™ฏ๏ผšๆทปๅŠ ็งฏๅˆ†ๅˆฐๆŽ’่กŒๆฆœ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ ZSet ๅญ˜ๅ‚จ็”จๆˆท็งฏๅˆ† + * 2. ่‡ชๅŠจๆŒ‰ๅˆ†ๆ•ฐๆŽ’ๅบ + * + * @param userId ็”จๆˆทID + * @param score ็งฏๅˆ† + */ + public void addUserScore(Long userId, double score) { + // 1. ๅฎšไน‰ๆŽ’่กŒๆฆœ้”ฎ + String rankKey = "leaderboard:user:score"; + + // 2. ๆทปๅŠ ๅˆฐๆœ‰ๅบ้›†ๅˆ + // ๅฆ‚ๆžœ็”จๆˆทๅทฒๅญ˜ๅœจ๏ผŒๅˆ†ๆ•ฐไผš็ดฏๅŠ  + redisCache.zIncrementScore(rankKey, userId, score); + log.info("็”จๆˆท็งฏๅˆ†ๆ›ดๆ–ฐ - userId: {}, score: {}", userId, score); + } + + /** + * ๅœบๆ™ฏ๏ผš่Žทๅ–ๆŽ’่กŒๆฆœ TOP N + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅˆ†ๆ•ฐๆœ€้ซ˜็š„ๅ‰ N ไธช็”จๆˆท + * 2. ่ฟ”ๅ›žๅ€’ๅบ็ป“ๆžœ + * + * @param topN ๅ‰Nๅ + * @return ็”จๆˆทID้›†ๅˆ๏ผˆๆŒ‰ๅˆ†ๆ•ฐๅ€’ๅบ๏ผ‰ + */ + public Set getTopUsers(int topN) { + // 1. ๅฎšไน‰ๆŽ’่กŒๆฆœ้”ฎ + String rankKey = "leaderboard:user:score"; + + // 2. ่Žทๅ–ๅ€’ๅบๆŽ’ๅๅ‰ N ็š„็”จๆˆท + Set topUsers = redisCache.zRangeByScore(rankKey, Double.MIN_VALUE, Double.MAX_VALUE); + + // 3. ็”ฑไบŽ Redis ่ฟ”ๅ›ž็š„ๆ˜ฏๆ— ๅบ้›†ๅˆ๏ผŒ่ฟ™้‡Œ็ฎ€ๅŒ–ๅค„็† + // ๅฎž้™…ๅบ”่ฏฅไฝฟ็”จ zReverseRange ๆŒ‰ๆŽ’ๅ่Œƒๅ›ด่Žทๅ– + log.info("่Žทๅ–ๆŽ’่กŒๆฆœ TOP {}", topN); + return topUsers; + } + + /** + * ๅœบๆ™ฏ๏ผš่Žทๅ–็”จๆˆทๆŽ’ๅ + * + * @param userId ็”จๆˆทID + * @return ๆŽ’ๅ๏ผˆไปŽ0ๅผ€ๅง‹๏ผ‰ + */ + public Long getUserRank(Long userId) { + String rankKey = "leaderboard:user:score"; + Long rank = redisCache.zReverseRank(rankKey, userId); + return rank != null ? rank + 1 : null; // ่ฝฌๆขไธบไปŽ1ๅผ€ๅง‹ + } + + // ==================== ๅœบๆ™ฏ 5: ๆถˆๆฏ้˜Ÿๅˆ— ==================== + + /** + * ๅœบๆ™ฏ๏ผšๅ‘้€ๆถˆๆฏๅˆฐ้˜Ÿๅˆ— + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ List ไฝœไธบ้˜Ÿๅˆ— + * 2. ไปŽๅณไพงๆŽจๅ…ฅๆถˆๆฏ + * + * @param queueName ้˜Ÿๅˆ—ๅ็งฐ + * @param message ๆถˆๆฏๅ†…ๅฎน + */ + public void sendMessage(String queueName, String message) { + String queueKey = "queue:" + queueName; + redisCache.lRightPush(queueKey, message); + log.info("ๅ‘้€ๆถˆๆฏๅˆฐ้˜Ÿๅˆ— - queue: {}, message: {}", queueName, message); + } + + /** + * ๅœบๆ™ฏ๏ผšไปŽ้˜Ÿๅˆ—ๆถˆ่ดนๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ไปŽๅทฆไพงๅผนๅ‡บๆถˆๆฏ + * 2. ๅค„็†ๆถˆๆฏ + * + * @param queueName ้˜Ÿๅˆ—ๅ็งฐ + * @return ๆถˆๆฏๅ†…ๅฎน + */ + public String consumeMessage(String queueName) { + String queueKey = "queue:" + queueName; + Object message = redisCache.lLeftPop(queueKey); + if (message != null) { + log.info("ไปŽ้˜Ÿๅˆ—ๆถˆ่ดนๆถˆๆฏ - queue: {}, message: {}", queueName, message); + return message.toString(); + } + return null; + } + + // ==================== ๅœบๆ™ฏ 6: ๅ…ฑๅŒๅฅฝๅ‹ ==================== + + /** + * ๅœบๆ™ฏ๏ผš่ฎก็ฎ—ไธคไธช็”จๆˆท็š„ๅ…ฑๅŒๅฅฝๅ‹ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ Set ๅญ˜ๅ‚จๆฏไธช็”จๆˆท็š„ๅฅฝๅ‹ๅˆ—่กจ + * 2. ่ฎก็ฎ—ไบค้›† + * + * @param userId1 ็”จๆˆท1 ID + * @param userId2 ็”จๆˆท2 ID + * @return ๅ…ฑๅŒๅฅฝๅ‹ID้›†ๅˆ + */ + public Set getCommonFriends(Long userId1, Long userId2) { + // 1. ๅฎšไน‰ๅฅฝๅ‹้›†ๅˆ้”ฎ + String friendsKey1 = "user:friends:" + userId1; + String friendsKey2 = "user:friends:" + userId2; + + // 2. ่ฎก็ฎ—ไบค้›† + Set commonFriends = redisCache.sIntersect(friendsKey1, friendsKey2); + + log.info("ๅ…ฑๅŒๅฅฝๅ‹ - userId1: {}, userId2: {}, count: {}", + userId1, userId2, commonFriends.size()); + return commonFriends; + } + + /** + * ๅœบๆ™ฏ๏ผšๆทปๅŠ ๅฅฝๅ‹ + * + * @param userId ็”จๆˆทID + * @param friendId ๅฅฝๅ‹ID + */ + public void addFriend(Long userId, Long friendId) { + String friendsKey = "user:friends:" + userId; + redisCache.sAdd(friendsKey, friendId); + log.info("ๆทปๅŠ ๅฅฝๅ‹ - userId: {}, friendId: {}", userId, friendId); + } + + // ==================== ๅœบๆ™ฏ 7: ็”จๆˆทๆ ‡็ญพ ==================== + + /** + * ๅœบๆ™ฏ๏ผšๆทปๅŠ ็”จๆˆทๆ ‡็ญพ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ Set ๅญ˜ๅ‚จ็”จๆˆทๆ ‡็ญพ + * 2. ่‡ชๅŠจๅŽป้‡ + * + * @param userId ็”จๆˆทID + * @param tags ๆ ‡็ญพๅˆ—่กจ + */ + public void addUserTags(Long userId, String... tags) { + String tagsKey = "user:tags:" + userId; + redisCache.sAdd(tagsKey, (Object[]) tags); + log.info("ๆทปๅŠ ็”จๆˆทๆ ‡็ญพ - userId: {}, tags: {}", userId, List.of(tags)); + } + + /** + * ๅœบๆ™ฏ๏ผš่Žทๅ–็”จๆˆทๆ ‡็ญพ + * + * @param userId ็”จๆˆทID + * @return ๆ ‡็ญพ้›†ๅˆ + */ + public Set getUserTags(Long userId) { + String tagsKey = "user:tags:" + userId; + return redisCache.sMembers(tagsKey); + } + + // ==================== ๅœบๆ™ฏ 8: ็”จๆˆทSession ==================== + + /** + * ๅœบๆ™ฏ๏ผšๅญ˜ๅ‚จ็”จๆˆทไผš่ฏไฟกๆฏ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ Hash ๅญ˜ๅ‚จไผš่ฏๅญ—ๆฎต + * 2. ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param userId ็”จๆˆทID + * @param sessionId ไผš่ฏID + * @param loginIp ็™ปๅฝ•IP + */ + public void storeUserSession(Long userId, String sessionId, String loginIp) { + String sessionKey = "session:user:" + userId; + + // 1. ๅญ˜ๅ‚จไผš่ฏไฟกๆฏ + redisCache.hSet(sessionKey, "sessionId", sessionId); + redisCache.hSet(sessionKey, "loginIp", loginIp); + redisCache.hSet(sessionKey, "loginTime", System.currentTimeMillis()); + + // 2. ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ24ๅฐๆ—ถ๏ผ‰ + redisCache.expire(sessionKey, 24, TimeUnit.HOURS); + + log.info("ๅญ˜ๅ‚จ็”จๆˆทไผš่ฏ - userId: {}, sessionId: {}", userId, sessionId); + } + + /** + * ๅœบๆ™ฏ๏ผš่Žทๅ–็”จๆˆทไผš่ฏไฟกๆฏ + * + * @param userId ็”จๆˆทID + * @return ไผš่ฏไฟกๆฏ + */ + public Map getUserSession(Long userId) { + String sessionKey = "session:user:" + userId; + return redisCache.hGetAll(sessionKey); + } + + // ==================== ๅœบๆ™ฏ 9: ็ƒญ็‚นๆ•ฐๆฎ็ผ“ๅญ˜ ==================== + + /** + * ๅœบๆ™ฏ๏ผš็ผ“ๅญ˜็ƒญ็‚นๆ•ฐๆฎ๏ผˆๅฆ‚้ฆ–้กตๆŽจ่๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ็ผ“ๅญ˜่Žทๅ– + * 2. ๅฆ‚ๆžœไธๅญ˜ๅœจ๏ผŒๅŠ ่ฝฝๆ•ฐๆฎ + * 3. ่ฎพ็ฝฎ่พƒ็Ÿญ็š„่ฟ‡ๆœŸๆ—ถ้—ด + * + * @return ๆŽจ่ๅ†…ๅฎนๅˆ—่กจ + */ + public List getHotContent() { + String cacheKey = "hot:content:home"; + + // 1. ไปŽ็ผ“ๅญ˜่Žทๅ– + Object cached = redisCache.get(cacheKey); + if (cached != null) { + return (List) cached; + } + + // 2. ๅŠ ่ฝฝ็ƒญ็‚นๆ•ฐๆฎ๏ผˆๆจกๆ‹Ÿ๏ผ‰ + List hotContent = loadHotContentFromDatabase(); + + // 3. ๅญ˜ๅ…ฅ็ผ“ๅญ˜๏ผˆ5ๅˆ†้’Ÿ่ฟ‡ๆœŸ๏ผŒ็ƒญ็‚นๆ•ฐๆฎๆ›ดๆ–ฐๅฟซ๏ผ‰ + redisCache.set(cacheKey, hotContent, 5, TimeUnit.MINUTES); + + return hotContent; + } + + // ==================== ๅœบๆ™ฏ 10: ้™ๆต๏ผˆๅŸบไบŽ่ฎกๆ•ฐๅ™จ๏ผ‰==================== + + /** + * ๅœบๆ™ฏ๏ผšAPI ้™ๆต + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ่ฎกๆ•ฐๅ™จ่ฎฐๅฝ•่ฏทๆฑ‚ๆฌกๆ•ฐ + * 2. ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + * 3. ่ถ…่ฟ‡้™ๅˆถๅˆ™ๆ‹’็ป + * + * @param userId ็”จๆˆทID + * @param limit ้™ๅˆถๆฌกๆ•ฐ + * @param windowSeconds ๆ—ถ้—ด็ช—ๅฃ๏ผˆ็ง’๏ผ‰ + * @return ๆ˜ฏๅฆๅ…่ฎธ่ฏทๆฑ‚ + */ + public boolean checkRateLimit(Long userId, int limit, int windowSeconds) { + String limitKey = "ratelimit:user:" + userId; + + // 1. ่Žทๅ–ๅฝ“ๅ‰่ฎกๆ•ฐ + Object countObj = redisCache.get(limitKey); + int currentCount = countObj != null ? (Integer) countObj : 0; + + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆ่ถ…่ฟ‡้™ๅˆถ + if (currentCount >= limit) { + log.warn("็”จๆˆท่ฏทๆฑ‚่ถ…้™ - userId: {}, count: {}, limit: {}", + userId, currentCount, limit); + return false; + } + + // 3. ๅขžๅŠ ่ฎกๆ•ฐ + if (currentCount == 0) { + // ็ฌฌไธ€ๆฌก่ฏทๆฑ‚๏ผŒ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + redisCache.set(limitKey, 1, windowSeconds, TimeUnit.SECONDS); + } else { + // ้ž็ฌฌไธ€ๆฌก๏ผŒ็›ดๆŽฅ่‡ชๅขž + redisCache.increment(limitKey, 1); + } + + return true; + } + + // ==================== ไปฅไธ‹ๆ˜ฏๆจกๆ‹Ÿ็š„่พ…ๅŠฉๆ–นๆณ• ==================== + + private User getUserFromDatabase(Long userId) { + // ๆจกๆ‹Ÿๆ•ฐๆฎๅบ“ๆŸฅ่ฏข + User user = new User(); + user.setId(userId); + user.setUsername("user" + userId); + return user; + } + + private void updateUserInDatabase(User user) { + // ๆจกๆ‹Ÿๆ•ฐๆฎๅบ“ๆ›ดๆ–ฐ + } + + private void syncViewCountToDatabase(Long articleId, long count) { + // ๆจกๆ‹ŸๅŒๆญฅๅˆฐๆ•ฐๆฎๅบ“ + } + + private void processOrderBusiness(Long orderId) { + // ๆจกๆ‹Ÿ่ฎขๅ•ๅค„็† + } + + private int getStockFromDatabase(Long productId) { + // ๆจกๆ‹Ÿ่Žทๅ–ๅบ“ๅญ˜ + return 1000; + } + + private void updateStockInDatabase(Long productId, int stock) { + // ๆจกๆ‹Ÿๆ›ดๆ–ฐๅบ“ๅญ˜ + } + + private List loadHotContentFromDatabase() { + // ๆจกๆ‹ŸๅŠ ่ฝฝ็ƒญ็‚นๆ•ฐๆฎ + return List.of("content1", "content2", "content3"); + } +} diff --git a/src/main/java/com/aisi/template/examples/TransactionExample.java b/src/main/java/com/aisi/template/examples/TransactionExample.java new file mode 100644 index 0000000..9ccb12d --- /dev/null +++ b/src/main/java/com/aisi/template/examples/TransactionExample.java @@ -0,0 +1,540 @@ +package com.aisi.template.examples; + +import com.aisi.template.domain.entity.SysRole; +import com.aisi.template.domain.entity.User; +import com.aisi.template.repository.SysRoleRepository; +import com.aisi.template.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.Set; + +/** + * ไบ‹ๅŠกไฝฟ็”จ็คบไพ‹ + * ๅฑ•็คบ Spring ไบ‹ๅŠก็š„ๅ„็งไผ ๆ’ญ่กŒไธบๅ’Œไฝฟ็”จๅœบๆ™ฏ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TransactionExample { + + private final UserRepository userRepository; + private final SysRoleRepository roleRepository; + + // ==================== ๅœบๆ™ฏ 1: ๅŸบๆœฌไบ‹ๅŠกไฝฟ็”จ ==================== + + /** + * ๅœบๆ™ฏ๏ผšๅˆ›ๅปบ็”จๆˆทๅนถๅˆ†้…่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๅผ€ๅฏไบ‹ๅŠก + * 2. ๅˆ›ๅปบ็”จๆˆท + * 3. ๅˆ†้…่ง’่‰ฒ + * 4. ๆไบคไบ‹ๅŠก + * ๆณจๆ„๏ผšไปปไฝ•ๆญฅ้ชคๅคฑ่ดฅ้ƒฝไผšๅ›žๆปšๆ•ดไธชไบ‹ๅŠก + * + * @param username ็”จๆˆทๅ + * @param password ๅฏ†็  + * @param roleIds ่ง’่‰ฒID้›†ๅˆ + * @return ๅˆ›ๅปบ็š„็”จๆˆทID + */ + @Transactional(rollbackFor = Exception.class) + public Long createUserWithRoles(String username, String password, Set roleIds) { + log.info("ๅผ€ๅง‹ๅˆ›ๅปบ็”จๆˆทๅนถๅˆ†้…่ง’่‰ฒ - username: {}", username); + + try { + // 1. ๅˆ›ๅปบ็”จๆˆท + User user = new User(); + user.setUsername(username); + user.setPassword(password); + User savedUser = userRepository.save(user); + log.info("็”จๆˆทๅˆ›ๅปบๆˆๅŠŸ - userId: {}", savedUser.getId()); + + // 2. ๅˆ†้…่ง’่‰ฒ + for (Long roleId : roleIds) { + SysRole role = roleRepository.findById(roleId) + .orElseThrow(() -> new RuntimeException("่ง’่‰ฒไธๅญ˜ๅœจ: " + roleId)); + user.getRoles().add(role); + } + userRepository.save(user); + log.info("่ง’่‰ฒๅˆ†้…ๆˆๅŠŸ - userId: {}, roleIds: {}", savedUser.getId(), roleIds); + + // 3. ไบ‹ๅŠกๆไบค๏ผˆๆ–นๆณ•ๆญฃๅธธ็ป“ๆŸๆ—ถ่‡ชๅŠจๆไบค๏ผ‰ + return savedUser.getId(); + + } catch (Exception e) { + // 4. ๅ‘็”Ÿๅผ‚ๅธธ๏ผŒไบ‹ๅŠกๅ›žๆปš + log.error("ๅˆ›ๅปบ็”จๆˆทๅคฑ่ดฅ๏ผŒไบ‹ๅŠกๅ›žๆปš - username: {}", username, e); + throw e; + } + } + + // ==================== ๅœบๆ™ฏ 2: REQUIRED ไผ ๆ’ญ่กŒไธบ ==================== + + /** + * ๅœบๆ™ฏ๏ผšREQUIRED ไผ ๆ’ญ่กŒไธบ๏ผˆ้ป˜่ฎค๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - ๅฆ‚ๆžœๅฝ“ๅ‰ๆฒกๆœ‰ไบ‹ๅŠก๏ผŒๅฐฑๆ–ฐๅปบไธ€ไธชไบ‹ๅŠก + * - ๅฆ‚ๆžœๅฝ“ๅ‰ๅทฒ็ปๅญ˜ๅœจไธ€ไธชไบ‹ๅŠก๏ผŒๅฐฑๅŠ ๅ…ฅๅˆฐ่ฟ™ไธชไบ‹ๅŠกไธญ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๅคงๅคšๆ•ฐไธšๅŠกๅœบๆ™ฏ + * + * @param userId ็”จๆˆทID + */ + @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) + public void requiredExample(Long userId) { + log.info("REQUIRED ไผ ๆ’ญ่กŒไธบ็คบไพ‹ - userId: {}", userId); + // ไธšๅŠก้€ป่พ‘ + } + + /** + * ๅœบๆ™ฏ๏ผšๅตŒๅฅ—ไบ‹ๅŠก็คบไพ‹ + * ๆญฅ้ชค๏ผš + * 1. ๅค–ๅฑ‚ๆ–นๆณ•ๅˆ›ๅปบไบ‹ๅŠก + * 2. ่ฐƒ็”จๅ†…ๅฑ‚ๆ–นๆณ•๏ผˆๅŠ ๅ…ฅๅค–ๅฑ‚ไบ‹ๅŠก๏ผ‰ + * 3. ๅ†…ๅค–ๅฑ‚ไปปไธ€ๅผ‚ๅธธ๏ผŒๆ•ดไฝ“ๅ›žๆปš + * + * @param userId ็”จๆˆทID + */ + @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) + public void nestedTransactionExample(Long userId) { + log.info("ๅค–ๅฑ‚ไบ‹ๅŠกๅผ€ๅง‹ - userId: {}", userId); + + // 1. ๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("new@example.com"); + userRepository.save(user); + + // 2. ่ฐƒ็”จๅ†…ๅฑ‚ๆ–นๆณ•๏ผˆไผšๅŠ ๅ…ฅๅฝ“ๅ‰ไบ‹ๅŠก๏ผ‰ + innerMethod(userId); + + // 3. ๅค–ๅฑ‚ไบ‹ๅŠกๆไบค + log.info("ๅค–ๅฑ‚ไบ‹ๅŠกๆไบค - userId: {}", userId); + } + + /** + * ๅ†…ๅฑ‚ๆ–นๆณ•๏ผˆๅŠ ๅ…ฅๅค–ๅฑ‚ไบ‹ๅŠก๏ผ‰ + */ + @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) + public void innerMethod(Long userId) { + log.info("ๅ†…ๅฑ‚ๆ–นๆณ•ๆ‰ง่กŒ - userId: {}", userId); + User user = userRepository.findById(userId).orElseThrow(); + user.setUsername("updated_username"); + userRepository.save(user); + } + + // ==================== ๅœบๆ™ฏ 3: REQUIRES_NEW ไผ ๆ’ญ่กŒไธบ ==================== + + /** + * ๅœบๆ™ฏ๏ผšREQUIRES_NEW ไผ ๆ’ญ่กŒไธบ + * ่ฏดๆ˜Ž๏ผš + * - ๆ— ่ฎบๅฝ“ๅ‰ๆ˜ฏๅฆๆœ‰ไบ‹ๅŠก๏ผŒ้ƒฝไผšๅˆ›ๅปบๆ–ฐไบ‹ๅŠก + * - ๅฆ‚ๆžœๅฝ“ๅ‰ๅญ˜ๅœจไบ‹ๅŠก๏ผŒๅฐ†ๅฝ“ๅ‰ไบ‹ๅŠกๆŒ‚่ตท + * ไฝฟ็”จๅœบๆ™ฏ๏ผš็‹ฌ็ซ‹่ฎฐๅฝ•ๆ—ฅๅฟ—๏ผŒไธๅ—ไธปไบ‹ๅŠกๅฝฑๅ“ + * + * @param userId ็”จๆˆทID + */ + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public void requiresNewExample(Long userId) { + log.info("REQUIRES_NEW ไผ ๆ’ญ่กŒไธบ็คบไพ‹ - userId: {}", userId); + // ็‹ฌ็ซ‹ไบ‹ๅŠก๏ผŒๅณไฝฟๅค–ๅฑ‚ไบ‹ๅŠกๅ›žๆปšไนŸไธๅฝฑๅ“ + } + + /** + * ๅœบๆ™ฏ๏ผšไธปไบ‹ๅŠก + ็‹ฌ็ซ‹ไบ‹ๅŠก + * ๆญฅ้ชค๏ผš + * 1. ไธปไบ‹ๅŠกๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + * 2. ็‹ฌ็ซ‹ไบ‹ๅŠก่ฎฐๅฝ•ๆ—ฅๅฟ— + * 3. ไธปไบ‹ๅŠกๅคฑ่ดฅไธๅฝฑๅ“ๆ—ฅๅฟ—่ฎฐๅฝ• + * + * @param userId ็”จๆˆทID + */ + @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) + public void mainTransactionWithLog(Long userId) { + log.info("ไธปไบ‹ๅŠกๅผ€ๅง‹ - userId: {}", userId); + + try { + // 1. ๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("main@example.com"); + userRepository.save(user); + + // 2. ่ฎฐๅฝ•ๆ“ไฝœๆ—ฅๅฟ—๏ผˆ็‹ฌ็ซ‹ไบ‹ๅŠก๏ผ‰ + recordOperationLog(userId, "ๆ›ดๆ–ฐ็”จๆˆท้‚ฎ็ฎฑ"); + + // 3. ๆจกๆ‹Ÿๅผ‚ๅธธ + // throw new RuntimeException("ๆจกๆ‹Ÿๅผ‚ๅธธ"); + + } catch (Exception e) { + log.error("ไธปไบ‹ๅŠกๅผ‚ๅธธ", e); + throw e; + } + } + + /** + * ่ฎฐๅฝ•ๆ“ไฝœๆ—ฅๅฟ—๏ผˆ็‹ฌ็ซ‹ไบ‹ๅŠก๏ผ‰ + * ๅณไฝฟไธปไบ‹ๅŠกๅ›žๆปš๏ผŒๆ—ฅๅฟ—ไนŸไผš่ขซ่ฎฐๅฝ• + */ + @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) + public void recordOperationLog(Long userId, String operation) { + log.info("่ฎฐๅฝ•ๆ“ไฝœๆ—ฅๅฟ—๏ผˆ็‹ฌ็ซ‹ไบ‹ๅŠก๏ผ‰- userId: {}, operation: {}", userId, operation); + // ๆ—ฅๅฟ—่ฎฐๅฝ•้€ป่พ‘ + } + + // ==================== ๅœบๆ™ฏ 4: NESTED ไผ ๆ’ญ่กŒไธบ ==================== + + /** + * ๅœบๆ™ฏ๏ผšNESTED ไผ ๆ’ญ่กŒไธบ + * ่ฏดๆ˜Ž๏ผš + * - ๅฆ‚ๆžœๅฝ“ๅ‰ๅญ˜ๅœจไบ‹ๅŠก๏ผŒๅˆ™ๅตŒๅฅ—ๅœจ่ฏฅไบ‹ๅŠกไธญๆ‰ง่กŒ + * - ๅตŒๅฅ—ไบ‹ๅŠกๆ˜ฏๅค–ๅฑ‚ไบ‹ๅŠก็š„ไธ€้ƒจๅˆ†๏ผŒๅฏไปฅ็‹ฌ็ซ‹ๆไบค + * - ๅค–ๅฑ‚ไบ‹ๅŠกๅคฑ่ดฅๆ—ถ๏ผŒๅตŒๅฅ—ไบ‹ๅŠกไนŸไผšๅ›žๆปš + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆ‰น้‡ๆ“ไฝœไธญ้ƒจๅˆ†ๅฏ็‹ฌ็ซ‹ๅ›žๆปš + * + * @param userId ็”จๆˆทID + */ + @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class) + public void nestedExample(Long userId) { + log.info("NESTED ไผ ๆ’ญ่กŒไธบ็คบไพ‹ - userId: {}", userId); + } + + /** + * ๅœบๆ™ฏ๏ผšๅตŒๅฅ—ไบ‹ๅŠกไฝฟ็”จ + * ๆญฅ้ชค๏ผš + * 1. ๅค–ๅฑ‚ไบ‹ๅŠกๆ›ดๆ–ฐ็”จๆˆทๅŸบๆœฌไฟกๆฏ + * 2. ๅตŒๅฅ—ไบ‹ๅŠกๆ›ดๆ–ฐๆ‰ฉๅฑ•ไฟกๆฏ + * 3. ๅตŒๅฅ—ไบ‹ๅŠกๅฏไปฅ็‹ฌ็ซ‹ๅ›žๆปš๏ผˆsavepoint๏ผ‰ + * + * @param userId ็”จๆˆทID + */ + @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) + public void nestedTransactionUsage(Long userId) { + log.info("ๅค–ๅฑ‚ไบ‹ๅŠกๅผ€ๅง‹ - userId: {}", userId); + + // 1. ๆ›ดๆ–ฐๅŸบๆœฌไฟกๆฏ + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("basic@example.com"); + userRepository.save(user); + + try { + // 2. ๅตŒๅฅ—ไบ‹ๅŠกๆ›ดๆ–ฐๆ‰ฉๅฑ•ไฟกๆฏ๏ผˆๅˆ›ๅปบ savepoint๏ผ‰ + updateExtendedInfo(userId); + + } catch (Exception e) { + // 3. ๅตŒๅฅ—ไบ‹ๅŠกๅผ‚ๅธธไธๅฝฑๅ“ๅค–ๅฑ‚ไบ‹ๅŠก + log.warn("ๆ‰ฉๅฑ•ไฟกๆฏๆ›ดๆ–ฐๅคฑ่ดฅ๏ผŒ็ปง็ปญๆ‰ง่กŒ", e); + } + + log.info("ๅค–ๅฑ‚ไบ‹ๅŠก็ปง็ปญ - userId: {}", userId); + } + + @Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class) + public void updateExtendedInfo(Long userId) { + log.info("ๅตŒๅฅ—ไบ‹ๅŠกๆ‰ง่กŒ - userId: {}", userId); + // ๆ‰ฉๅฑ•ไฟกๆฏๆ›ดๆ–ฐ + } + + // ==================== ๅœบๆ™ฏ 5: ไบ‹ๅŠกๅ›žๆปš ==================== + + /** + * ๅœบๆ™ฏ๏ผšๅผ‚ๅธธๆ—ถๅ›žๆปšไบ‹ๅŠก + * ๆญฅ้ชค๏ผš + * 1. ๆ‰ง่กŒๅคšไธชๆ•ฐๆฎๅบ“ๆ“ไฝœ + * 2. ๅ‘็”Ÿๅผ‚ๅธธๆ—ถ่‡ชๅŠจๅ›žๆปš + * 3. rollbackFor ๆŒ‡ๅฎšๅ“ชไบ›ๅผ‚ๅธธ่งฆๅ‘ๅ›žๆปš + * + * @param userId ็”จๆˆทID + */ + @Transactional(rollbackFor = {RuntimeException.class, Exception.class}) + public void rollbackExample(Long userId) { + log.info("ไบ‹ๅŠกๅ›žๆปš็คบไพ‹ - userId: {}", userId); + + // 1. ๆ‰ง่กŒ็ฌฌไธ€ไธชๆ“ไฝœ + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("rollback@example.com"); + userRepository.save(user); + + // 2. ๆจกๆ‹Ÿๅผ‚ๅธธ๏ผŒ่งฆๅ‘ๅ›žๆปš + if (true) { + throw new RuntimeException("ๆจกๆ‹Ÿๅผ‚ๅธธ๏ผŒ่งฆๅ‘ไบ‹ๅŠกๅ›žๆปš"); + } + + // 3. ่ฟ™่กŒไปฃ็ ไธไผšๆ‰ง่กŒ + user.setUsername("never_reached"); + } + + /** + * ๅœบๆ™ฏ๏ผšๆŒ‡ๅฎšๅ›žๆปšๅผ‚ๅธธ + * ่ฏดๆ˜Ž๏ผš + * - ้ป˜่ฎคๅชๅฏน RuntimeException ๅ’Œ Error ๅ›žๆปš + * - ้œ€่ฆๅฏน checked exception ๅ›žๆปšๆ—ถ๏ผŒๆŒ‡ๅฎš rollbackFor + * + * @param userId ็”จๆˆทID + * @throws Exception ไธšๅŠกๅผ‚ๅธธ + */ + @Transactional(rollbackFor = Exception.class) + public void rollbackForCheckedException(Long userId) throws Exception { + log.info("ๆŒ‡ๅฎšๅ›žๆปšๅผ‚ๅธธ็คบไพ‹ - userId: {}", userId); + + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("checked@example.com"); + userRepository.save(user); + + // ๆŠ›ๅ‡บ checked exception ไนŸไผšๅ›žๆปš + throw new Exception("ไธšๅŠกๅผ‚ๅธธ"); + } + + /** + * ๅœบๆ™ฏ๏ผšๆŽ’้™คๅ›žๆปšๅผ‚ๅธธ + * ่ฏดๆ˜Ž๏ผš + * - ๆŸไบ›ๅผ‚ๅธธไธ่งฆๅ‘ๅ›žๆปš + * - ไฝฟ็”จ noRollbackFor ๆŒ‡ๅฎš + * + * @param userId ็”จๆˆทID + */ + @Transactional(noRollbackFor = {BusinessException.class}, rollbackFor = Exception.class) + public void noRollbackForExample(Long userId) { + log.info("ๆŽ’้™คๅ›žๆปšๅผ‚ๅธธ็คบไพ‹ - userId: {}", userId); + + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("no_rollback@example.com"); + userRepository.save(user); + + // BusinessException ไธไผš่งฆๅ‘ๅ›žๆปš + throw new BusinessException("ไธšๅŠกๅผ‚ๅธธ๏ผŒไฝ†ไธๅ›žๆปš"); + } + + // ==================== ๅœบๆ™ฏ 6: ๅช่ฏปไบ‹ๅŠก ==================== + + /** + * ๅœบๆ™ฏ๏ผšๅช่ฏปไบ‹ๅŠก + * ่ฏดๆ˜Ž๏ผš + * - ่ฎพ็ฝฎไบ‹ๅŠกไธบๅช่ฏป๏ผŒไผ˜ๅŒ–ๆŸฅ่ฏขๆ€ง่ƒฝ + * - ๆ•ฐๆฎๅบ“ๅฏไปฅ่ฟ›่กŒๆŸฅ่ฏขไผ˜ๅŒ– + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆŸฅ่ฏขๆ–นๆณ•ใ€ๆŠฅ่กจ็ปŸ่ฎก + * + * @param userId ็”จๆˆทID + * @return ็”จๆˆทๅฏน่ฑก + */ + @Transactional(readOnly = true) + public User readOnlyExample(Long userId) { + log.info("ๅช่ฏปไบ‹ๅŠก็คบไพ‹ - userId: {}", userId); + return userRepository.findById(userId).orElseThrow(); + } + + /** + * ๅœบๆ™ฏ๏ผšๆ‰น้‡ๆŸฅ่ฏข๏ผˆๅช่ฏปไบ‹ๅŠก๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅผ€ๅฏๅช่ฏปไบ‹ๅŠก + * 2. ๆ‰ง่กŒๅคšไธชๆŸฅ่ฏข + * 3. ๅ…ณ้—ญไบ‹ๅŠก + * + * @param userId ็”จๆˆทID + * @return ็”จๆˆทไฟกๆฏ + */ + @Transactional(readOnly = true) + public UserInfoDTO getUserInfo(Long userId) { + log.info("ๆ‰น้‡ๆŸฅ่ฏข็คบไพ‹ - userId: {}", userId); + + // 1. ๆŸฅ่ฏขๅŸบๆœฌไฟกๆฏ + User user = userRepository.findById(userId).orElseThrow(); + + // 2. ๆŸฅ่ฏข่ง’่‰ฒไฟกๆฏ + Set roles = user.getRoles(); + + // 3. ็ป„่ฃ…่ฟ”ๅ›ž + UserInfoDTO dto = new UserInfoDTO(); + dto.setUserId(user.getId()); + dto.setUsername(user.getUsername()); + dto.setRoleCount(roles.size()); + + return dto; + } + + // ==================== ๅœบๆ™ฏ 7: ไบ‹ๅŠก่ถ…ๆ—ถ ==================== + + /** + * ๅœบๆ™ฏ๏ผš่ฎพ็ฝฎไบ‹ๅŠก่ถ…ๆ—ถ + * ่ฏดๆ˜Ž๏ผš + * - ่ถ…่ฟ‡ๆŒ‡ๅฎšๆ—ถ้—ดๅŽ่‡ชๅŠจๅ›žๆปš + * - ๅ•ไฝ๏ผš็ง’ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš้•ฟๆ—ถ้—ด่ฟ่กŒ็š„ไบ‹ๅŠก + * + * @param userId ็”จๆˆทID + */ + @Transactional(timeout = 30, rollbackFor = Exception.class) + public void timeoutExample(Long userId) throws InterruptedException { + log.info("ไบ‹ๅŠก่ถ…ๆ—ถ็คบไพ‹ - userId: {}", userId); + + // ๆจกๆ‹Ÿ้•ฟๆ—ถ้—ดๆ“ไฝœ + Thread.sleep(1000); + + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("timeout@example.com"); + userRepository.save(user); + } + + // ==================== ๅœบๆ™ฏ 8: ็ผ–็จ‹ๅผไบ‹ๅŠก ==================== + + /** + * ๅœบๆ™ฏ๏ผš็ผ–็จ‹ๅผไบ‹ๅŠก๏ผˆๆ‰‹ๅŠจๆŽงๅˆถไบ‹ๅŠก๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๆ‰‹ๅŠจๅผ€ๅฏไบ‹ๅŠก + * 2. ๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + * 3. ๆ‰‹ๅŠจๆไบคๆˆ–ๅ›žๆปš + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๅคๆ‚็š„ไบ‹ๅŠกๆŽงๅˆถ้€ป่พ‘ + * + * @param userId ็”จๆˆทID + */ + public void programmaticTransaction(Long userId) { + log.info("็ผ–็จ‹ๅผไบ‹ๅŠก็คบไพ‹ - userId: {}", userId); + + // ้€š่ฟ‡ TransactionTemplate ๆ‰‹ๅŠจๆŽงๅˆถไบ‹ๅŠก + // ๅฎž้™…ไฝฟ็”จๆ—ถๆณจๅ…ฅ TransactionTemplate + // transactionTemplate.execute(status -> { + // try { + // User user = userRepository.findById(userId).orElseThrow(); + // user.setEmail("programmatic@example.com"); + // userRepository.save(user); + // return true; + // } catch (Exception e) { + // status.setRollbackOnly(); + // throw e; + // } + // }); + } + + // ==================== ๅœบๆ™ฏ 9: ไบ‹ๅŠกๅŽ็ฝฎๅค„็† ==================== + + /** + * ๅœบๆ™ฏ๏ผšไบ‹ๅŠกๆไบคๅŽๆ‰ง่กŒๆ“ไฝœ + * ๆญฅ้ชค๏ผš + * 1. ๆ‰ง่กŒไธšๅŠก้€ป่พ‘ + * 2. ๆณจๅ†Œไบ‹ๅŠกๅŒๆญฅๅ›ž่ฐƒ + * 3. ไบ‹ๅŠกๆไบคๅŽ่‡ชๅŠจๆ‰ง่กŒๅ›ž่ฐƒ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๅ‘้€ๆถˆๆฏใ€ๆธ…้™ค็ผ“ๅญ˜ใ€ๅ‘ๅธƒไบ‹ไปถ + * + * @param userId ็”จๆˆทID + */ + @Transactional(rollbackFor = Exception.class) + public void afterCommitExample(Long userId) { + log.info("ไบ‹ๅŠกๅŽ็ฝฎๅค„็†็คบไพ‹ - userId: {}", userId); + + // 1. ๆ›ดๆ–ฐๆ•ฐๆฎๅบ“ + User user = userRepository.findById(userId).orElseThrow(); + user.setEmail("after_commit@example.com"); + userRepository.save(user); + + // 2. ๆณจๅ†Œไบ‹ๅŠกๆไบคๅŽๅ›ž่ฐƒ + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + // 3. ไบ‹ๅŠกๆไบคๅŽๆ‰ง่กŒ๏ผˆๅฆ‚ๅ‘้€ๆถˆๆฏ๏ผ‰ + log.info("ไบ‹ๅŠกๅทฒๆไบค๏ผŒๅ‘้€้€š็Ÿฅ - userId: {}", userId); + sendMessage(userId); + } + + @Override + public void afterCompletion(int status) { + // 4. ไบ‹ๅŠกๅฎŒๆˆๅŽๆ‰ง่กŒ๏ผˆๆ— ่ฎบๆˆๅŠŸๆˆ–ๅคฑ่ดฅ๏ผ‰ + log.info("ไบ‹ๅŠกๅทฒๅฎŒๆˆ - userId: {}, status: {}", userId, status); + } + }); + } + + /** + * ๅœบๆ™ฏ๏ผšไบ‹ๅŠกๅ›žๆปšๅŽๆ‰ง่กŒๆ“ไฝœ + * + * @param userId ็”จๆˆทID + */ + @Transactional(rollbackFor = Exception.class) + public void afterRollbackExample(Long userId) { + log.info("ไบ‹ๅŠกๅ›žๆปšๅŽๅค„็†็คบไพ‹ - userId: {}", userId); + + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCompletion(int status) { + if (STATUS_ROLLED_BACK == status) { + // ไบ‹ๅŠกๅ›žๆปšๅŽๆ‰ง่กŒ + log.error("ไบ‹ๅŠกๅทฒๅ›žๆปš - userId: {}", userId); + handleRollback(userId); + } + } + }); + + // ๆจกๆ‹Ÿๅผ‚ๅธธ + throw new RuntimeException("ๆจกๆ‹Ÿๅผ‚ๅธธ"); + } + + // ==================== ๅœบๆ™ฏ 10: ้š”็ฆป็บงๅˆซ ==================== + + /** + * ๅœบๆ™ฏ๏ผš่ฎพ็ฝฎไบ‹ๅŠก้š”็ฆป็บงๅˆซ + * ่ฏดๆ˜Ž๏ผš + * - READ_UNCOMMITTED: ่ฏปๆœชๆไบค๏ผˆๅฏ่ƒฝๅ‡บ็Žฐ่„่ฏป๏ผ‰ + * - READ_COMMITTED: ่ฏปๅทฒๆไบค๏ผˆ้˜ฒๆญข่„่ฏป๏ผŒๅฏ่ƒฝๅ‡บ็Žฐไธๅฏ้‡ๅค่ฏป๏ผ‰ + * - REPEATABLE_READ: ๅฏ้‡ๅค่ฏป๏ผˆ้˜ฒๆญข่„่ฏปๅ’Œไธๅฏ้‡ๅค่ฏป๏ผŒๅฏ่ƒฝๅ‡บ็Žฐๅนป่ฏป๏ผ‰ + * - SERIALIZABLE: ไธฒ่กŒๅŒ–๏ผˆๆœ€้ซ˜้š”็ฆป็บงๅˆซ๏ผŒๆ€ง่ƒฝๆœ€ๅทฎ๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆ นๆฎไธšๅŠก้œ€ๆฑ‚้€‰ๆ‹ฉๅˆ้€‚็š„้š”็ฆป็บงๅˆซ + * + * @param userId ็”จๆˆทID + */ + @Transactional(isolation = org.springframework.transaction.annotation.Isolation.READ_COMMITTED, + rollbackFor = Exception.class) + public void isolationLevelExample(Long userId) { + log.info("้š”็ฆป็บงๅˆซ็คบไพ‹ - userId: {}", userId); + // ไฝฟ็”จ READ_COMMITTED ้š”็ฆป็บงๅˆซ + } + + // ==================== ่พ…ๅŠฉ็ฑปๅ’Œๆ–นๆณ• ==================== + + /** + * ็”จๆˆทไฟกๆฏ DTO + */ + public static class UserInfoDTO { + private Long userId; + private String username; + private Integer roleCount; + + public void setUserId(Long userId) { + this.userId = userId; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setRoleCount(Integer roleCount) { + this.roleCount = roleCount; + } + } + + /** + * ไธšๅŠกๅผ‚ๅธธ + */ + public static class BusinessException extends RuntimeException { + public BusinessException(String message) { + super(message); + } + } + + /** + * ๅ‘้€ๆถˆๆฏ๏ผˆๆจกๆ‹Ÿ๏ผ‰ + */ + private void sendMessage(Long userId) { + log.info("ๅ‘้€ๆถˆๆฏ - userId: {}", userId); + } + + /** + * ๅค„็†ๅ›žๆปš๏ผˆๆจกๆ‹Ÿ๏ผ‰ + */ + private void handleRollback(Long userId) { + log.info("ๅค„็†ๅ›žๆปš - userId: {}", userId); + } +} diff --git a/src/main/java/com/aisi/template/exception/BusinessException.java b/src/main/java/com/aisi/template/exception/BusinessException.java index 46efefb..63e1c7d 100644 --- a/src/main/java/com/aisi/template/exception/BusinessException.java +++ b/src/main/java/com/aisi/template/exception/BusinessException.java @@ -1,21 +1,51 @@ package com.aisi.template.exception; -import com.aisi.template.domain.enums.ErrorCode; +import com.aisi.template.domain.RestCode; import lombok.Getter; /** * ่‡ชๅฎšไน‰ไธšๅŠกๅผ‚ๅธธ * ็”จไบŽๅœจ Service ๅฑ‚ไธญๆ–ญ้€ป่พ‘๏ผŒๅนถ่ฟ”ๅ›žๅ…ทไฝ“็š„้”™่ฏฏ็ ๅ’Œ้”™่ฏฏไฟกๆฏ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ไธšๅŠก้€ป่พ‘ๆ ก้ชŒๅคฑ่ดฅ + * - ่ต„ๆบไธๅญ˜ๅœจ + * - ๆƒ้™ไธ่ถณ + * - ๅ‚ๆ•ฐ้”™่ฏฏ + * + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+ * // ๆŒ‡ๅฎš้”™่ฏฏ็ ๅ’Œๆถˆๆฏ
+ * throw new BusinessException(404, "็”จๆˆทไธๅญ˜ๅœจ");
+ *
+ * // ไฝฟ็”จ้€š็”จ้”™่ฏฏ็ ๏ผˆ้ป˜่ฎค 400๏ผ‰
+ * throw new BusinessException("ๆ“ไฝœๅคฑ่ดฅ");
+ *
+ * // ไฝฟ็”จ้”™่ฏฏ็ ๆžšไธพ
+ * throw new BusinessException(RestCode.USER_NOT_FOUND);
+ * 
+ * + * @author Claude + * @since 2024-04-09 */ @Getter // ไฝฟ็”จ Lombok ่‡ชๅŠจ็”Ÿๆˆ getCode() ๆ–นๆณ• public class BusinessException extends RuntimeException { - - // ้”™่ฏฏ็  (ไพ‹ๅฆ‚ 400, 403, 1001 ็ญ‰) + + /** + * ้”™่ฏฏ็  + * - ไพ‹ๅฆ‚๏ผš400, 403, 1001 ็ญ‰ + */ private final int code; /** * ๆž„้€ ๆ–นๆณ• 1๏ผšๆ‰‹ๅŠจๆŒ‡ๅฎš code ๅ’Œ message - * ไฝฟ็”จ๏ผšthrow new BusinessException(404, "ๆ‰พไธๅˆฐ่ฏฅๆ–ฐ้—ป"); + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+     * throw new BusinessException(404, "ๆ‰พไธๅˆฐ่ฏฅๆ–ฐ้—ป");
+     * 
+ * + * @param code ้”™่ฏฏ็  + * @param message ้”™่ฏฏๆถˆๆฏ */ public BusinessException(int code, String message) { super(message); // ๆŠŠ message ไผ ็ป™็ˆถ็ฑป๏ผŒๆ–นไพฟ log ๆ‰“ๅฐ @@ -23,17 +53,30 @@ public class BusinessException extends RuntimeException { } /** - * ๆž„้€ ๆ–นๆณ• 2๏ผšไฝฟ็”จ้€š็”จ้”™่ฏฏ็  (้ป˜่ฎคไธบ 400 ๆˆ– 500) - * ไฝฟ็”จ๏ผšthrow new BusinessException("ๆ“ไฝœๅคฑ่ดฅ"); + * ๆž„้€ ๆ–นๆณ• 2๏ผšไฝฟ็”จ้€š็”จ้”™่ฏฏ็  + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+     * throw new BusinessException("ๆ“ไฝœๅคฑ่ดฅ");
+     * 
+ * + * @param message ้”™่ฏฏๆถˆๆฏ */ public BusinessException(String message) { super(message); - this.code = 400; // ้ป˜่ฎค็ป™ไธช 400 + this.code = 400; // ้ป˜่ฎค็ป™ไธช 400๏ผˆๅฎขๆˆท็ซฏ้”™่ฏฏ๏ผ‰ } - // ๅœจ BusinessException ็ฑป้‡ŒๆทปๅŠ ่ฟ™ไธชๆž„้€ ๆ–นๆณ• - public BusinessException(ErrorCode errorCode) { - super(errorCode.getMessage()); - this.code = errorCode.getCode(); + /** + * ๆž„้€ ๆ–นๆณ• 3๏ผšไฝฟ็”จ้”™่ฏฏ็ ๆžšไธพ + * ไฝฟ็”จ็คบไพ‹๏ผš + *
+     * throw new BusinessException(ErrorCode.USER_NOT_FOUND);
+     * 
+ * + * @param restCode ้”™่ฏฏ็ ๆžšไธพ + */ + public BusinessException(RestCode restCode) { + super(restCode.getMessage()); + this.code = restCode.getCode(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/aisi/template/exception/RateLimitExceededException.java b/src/main/java/com/aisi/template/exception/RateLimitExceededException.java new file mode 100644 index 0000000..ee7d444 --- /dev/null +++ b/src/main/java/com/aisi/template/exception/RateLimitExceededException.java @@ -0,0 +1,35 @@ +package com.aisi.template.exception; + +/** + * ้™ๆต่ถ…ๅ‡บๅผ‚ๅธธ + * ๅฝ“่ฏทๆฑ‚้ข‘็އ่ถ…่ฟ‡้™ๅˆถๆ—ถๆŠ›ๅ‡บๆญคๅผ‚ๅธธ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็™ปๅฝ•ๆŽฅๅฃ๏ผš้˜ฒๆญขๆšดๅŠ›็ ด่งฃ + * - API ๆŽฅๅฃ๏ผš้˜ฒๆญขๆถๆ„ๅˆทๆŽฅๅฃ + * - ๆŠข่ดญๆดปๅŠจ๏ผš้˜ฒๆญขๅˆทๅ• + * + * HTTP ็Šถๆ€็ ๏ผš429 Too Many Requests + * + * @author Claude + * @since 2024-04-09 + */ +public class RateLimitExceededException extends RuntimeException { + + /** + * ๆž„้€ ๆ–นๆณ•๏ผš่‡ชๅฎšไน‰้”™่ฏฏๆถˆๆฏ + * + * @param message ้”™่ฏฏๆถˆๆฏ + */ + public RateLimitExceededException(String message) { + super(message); + } + + /** + * ๆž„้€ ๆ–นๆณ•๏ผš้ป˜่ฎค้”™่ฏฏๆถˆๆฏ + * ้”™่ฏฏๆถˆๆฏ๏ผš"่ฏทๆฑ‚่ฟ‡ไบŽ้ข‘็น๏ผŒ่ฏท็จๅŽๅ†่ฏ•" + */ + public RateLimitExceededException() { + super("่ฏทๆฑ‚่ฟ‡ไบŽ้ข‘็น๏ผŒ่ฏท็จๅŽๅ†่ฏ•"); + } +} diff --git a/src/main/java/com/aisi/template/filter/JwtAuthenticationFilter.java b/src/main/java/com/aisi/template/filter/JwtAuthenticationFilter.java index 72d10dc..22cf1fc 100644 --- a/src/main/java/com/aisi/template/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/aisi/template/filter/JwtAuthenticationFilter.java @@ -1,7 +1,7 @@ package com.aisi.template.filter; - import com.aisi.template.domain.CustomUserDetails; +import com.aisi.template.service.TokenService; import com.aisi.template.service.impl.CustomUserDetailsService; import com.aisi.template.utils.JwtUtil; import jakarta.servlet.FilterChain; @@ -18,50 +18,133 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +/** + * JWT ่ฎค่ฏ่ฟ‡ๆปคๅ™จ + * ๆ‹ฆๆˆช่ฏทๆฑ‚ๅนถ้ชŒ่ฏ JWT Token + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. Token ๆๅ–๏ผšไปŽ่ฏทๆฑ‚ๅคดไธญๆๅ– JWT Token + * 2. Token ้ชŒ่ฏ๏ผš้ชŒ่ฏ Token ็ญพๅๅ’Œๆœ‰ๆ•ˆๆ€ง + * 3. ้ป‘ๅๅ•ๆฃ€ๆŸฅ๏ผšๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + * 4. ็”จๆˆท่ฎค่ฏ๏ผš่ฎพ็ฝฎ Spring Security ่ฎค่ฏไฟกๆฏ + * + * ๅทฅไฝœๆต็จ‹๏ผš + * 1. ไปŽ Authorization ่ฏทๆฑ‚ๅคดไธญๆๅ– Token + * 2. ้ชŒ่ฏ Token ็ญพๅ + * 3. ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + * 4. ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * 5. ๆๅ–็”จๆˆทไฟกๆฏๅนถ่ฎพ็ฝฎ่ฎค่ฏ + * + * @author Claude + * @since 2024-04-09 + */ @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + /** + * JWT ๅทฅๅ…ท็ฑป + * ็”จไบŽ็”Ÿๆˆๅ’Œ้ชŒ่ฏ JWT Token + */ private final JwtUtil jwtUtil; + /** + * ็”จๆˆท่ฏฆๆƒ…ๆœๅŠก + * ็”จไบŽๅŠ ่ฝฝ็”จๆˆทไฟกๆฏ + */ private final CustomUserDetailsService userDetailsService; + /** + * Token ๆœๅŠก + * ็”จไบŽๆฃ€ๆŸฅ Token ้ป‘ๅๅ• + */ + private final TokenService tokenService; + + /** + * ่ฟ‡ๆปคๅ™จๅ†…้ƒจๅค„็†ๆ–นๆณ• + * ๆญฅ้ชค๏ผš + * 1. ไปŽ่ฏทๆฑ‚ๅคดไธญๆๅ– Token + * 2. ้ชŒ่ฏ Token ็ญพๅ + * 3. ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + * 4. ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * 5. ๆๅ–็”จๆˆทไฟกๆฏๅนถ่ฎพ็ฝฎ่ฎค่ฏ + * + * @param request HTTP ่ฏทๆฑ‚ + * @param response HTTP ๅ“ๅบ” + * @param filterChain ่ฟ‡ๆปคๅ™จ้“พ + */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String token = extractToken(request); - if (token != null && jwtUtil.validateToken(token)) { - // token ๆœ‰ๆ•ˆ + // 1. ไปŽ่ฏทๆฑ‚ๅคดไธญๆๅ– Token + String token = extractToken(request); + + if (token != null && jwtUtil.validateToken(token)) { + // 2. ้ชŒ่ฏ Token ็ญพๅ้€š่ฟ‡๏ผŒๆฃ€ๆŸฅๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + String jti = jwtUtil.extractJti(token); + if (jti != null && tokenService.isTokenBlacklisted(jti)) { + // 2.1 Token ๅทฒ่ขซๆ’ค้”€๏ผˆๅœจ้ป‘ๅๅ•ไธญ๏ผ‰๏ผŒๆธ…้™ค่ฎค่ฏ + SecurityContextHolder.clearContext(); + filterChain.doFilter(request, response); + return; + } + + // 3. ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆ่ฟ‡ๆœŸ + if (jwtUtil.isTokenExpired(token)) { + // 3.1 Token ๅทฒ่ฟ‡ๆœŸ๏ผŒๆธ…้™ค่ฎค่ฏ + SecurityContextHolder.clearContext(); + filterChain.doFilter(request, response); + return; + } + + // 4. Token ๆœ‰ๆ•ˆ๏ผŒๆๅ–็”จๆˆทไฟกๆฏๅนถ่ฎพ็ฝฎ่ฎค่ฏ String username = jwtUtil.extractUsername(token); if (username != null && !username.isEmpty()) { + // 4.1 ๅŠ ่ฝฝ็”จๆˆท่ฏฆๆƒ… CustomUserDetails userDetails = (CustomUserDetails) userDetailsService.loadUserByUsername(username); - if (userDetails == null) throw new UsernameNotFoundException("User not found"); + if (userDetails == null) throw new UsernameNotFoundException("็”จๆˆทไธๅญ˜ๅœจ"); + + // 4.2 ๅˆ›ๅปบ่ฎค่ฏๅฏน่ฑก UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + // 4.3 ่ฎพ็ฝฎ่ฎค่ฏไฟกๆฏๅˆฐ SecurityContext SecurityContextHolder.getContext().setAuthentication(authentication); - } - } else { - // token ็ผบๅคฑๆˆ–ๆ— ๆ•ˆๆ—ถ๏ผŒไธๆŠ›ๅผ‚ๅธธ๏ผŒ็›ดๆŽฅๆ”พ่กŒๅˆฐไธ‹ไธ€ไธช่ฟ‡ๆปคๅ™จ - } + } else { + // 5. Token ็ผบๅคฑๆˆ–ๆ— ๆ•ˆ๏ผŒไธๆŠ›ๅ‡บๅผ‚ๅธธ๏ผŒ็ปง็ปญๅค„็† + // ๅŽŸๅ› ๏ผšๆœ‰ไบ›ๆŽฅๅฃไธ้œ€่ฆ่ฎค่ฏ๏ผˆๅฆ‚็™ปๅฝ•ใ€ๆณจๅ†Œ๏ผ‰ + } + // 6. ็ปง็ปญ่ฟ‡ๆปคๅ™จ้“พ filterChain.doFilter(request, response); } /** - * ๆๅ–ๅ‡บtoken + * ไปŽ Authorization ่ฏทๆฑ‚ๅคดไธญๆๅ– Token + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ– Authorization ่ฏทๆฑ‚ๅคด + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไธบ็ฉบ + * 3. ๆฃ€ๆŸฅๆ˜ฏๅฆไปฅ "Bearer " ๅผ€ๅคด + * 4. ๆๅ– Token ้ƒจๅˆ†๏ผˆๅŽปๆމ "Bearer " ๅ‰็ผ€๏ผ‰ * - * @return token + * @param request HTTP ่ฏทๆฑ‚ + * @return JWT Token๏ผŒๅฆ‚ๆžœๆ— ๆ•ˆๅˆ™่ฟ”ๅ›ž null */ - private String extractToken(HttpServletRequest request) { - String authorization = request.getHeader("Authorization"); - if (authorization == null) { - return null; - } - if (!authorization.startsWith("Bearer ")) { - return null; - } - return authorization.substring(7); - } + private String extractToken(HttpServletRequest request) { + // 1. ่Žทๅ– Authorization ่ฏทๆฑ‚ๅคด + String authorization = request.getHeader("Authorization"); + if (authorization == null) { + return null; + } + + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไปฅ "Bearer " ๅผ€ๅคด + if (!authorization.startsWith("Bearer ")) { + return null; + } + + // 3. ๆๅ– Token ้ƒจๅˆ†๏ผˆๅŽปๆމ "Bearer " ๅ‰็ผ€๏ผŒๅ…ฑ 7 ไธชๅญ—็ฌฆ๏ผ‰ + return authorization.substring(7); + } } diff --git a/src/main/java/com/aisi/template/handler/GlobalExceptionHandler.java b/src/main/java/com/aisi/template/handler/GlobalExceptionHandler.java index 17c3284..43c89a4 100644 --- a/src/main/java/com/aisi/template/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/aisi/template/handler/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ package com.aisi.template.handler; import com.aisi.template.domain.RestBean; import com.aisi.template.domain.RestCode; +import com.aisi.template.exception.RateLimitExceededException; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authorization.AuthorizationDeniedException; @@ -21,124 +22,194 @@ import java.util.stream.Collectors; /** * ๅ…จๅฑ€ๅผ‚ๅธธๅค„็†ๅ™จ + * ็ปŸไธ€ๅค„็†็ณป็ปŸไธญ็š„ๅ„็งๅผ‚ๅธธ๏ผŒ่ฟ”ๅ›žๆ ‡ๅ‡†็š„้”™่ฏฏๅ“ๅบ”ๆ ผๅผ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅฎขๆˆท็ซฏ่ฏทๆฑ‚้”™่ฏฏ๏ผˆ400๏ผ‰๏ผšๅ‚ๆ•ฐๆ ก้ชŒๅคฑ่ดฅใ€JSONๆ ผๅผ้”™่ฏฏใ€็ผบๅฐ‘ๅ‚ๆ•ฐ็ญ‰ + * 2. ไธšๅŠก้€ป่พ‘้”™่ฏฏ๏ผš่‡ชๅฎšไน‰ไธšๅŠกๅผ‚ๅธธใ€ๆ•ฐๆฎๅ†ฒ็ช็ญ‰ + * 3. ็ณป็ปŸ้”™่ฏฏ๏ผˆ500๏ผ‰๏ผš็ฉบๆŒ‡้’ˆใ€ๆœช้ข„ๆœŸ็š„ๅผ‚ๅธธ็ญ‰ + * + * ๅผ‚ๅธธๅˆ†็ฑป๏ผš + * - ๅฎขๆˆท็ซฏ้”™่ฏฏ๏ผš่ฎฐๅฝ• WARN ๆ—ฅๅฟ—๏ผŒไธๆ‰“ๅฐๅ †ๆ ˆ + * - ไธšๅŠกๅผ‚ๅธธ๏ผš่ฎฐๅฝ• WARN ๆ—ฅๅฟ—๏ผŒไธๆ‰“ๅฐๅ †ๆ ˆ + * - ็ณป็ปŸ้”™่ฏฏ๏ผš่ฎฐๅฝ• ERROR ๆ—ฅๅฟ—๏ผŒๆ‰“ๅฐๅ †ๆ ˆ + * + * @author Claude + * @since 2024-04-09 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { - // ========================================== - // 1. ๅฎขๆˆท็ซฏ่ฏทๆฑ‚้”™่ฏฏ (400 Bad Request ็ญ‰) - // ่ฟ™็ฑป้”™่ฏฏๆ˜ฏๅ‰็ซฏไผ ๅ‚ไธๅฏน๏ผŒ่ฎฐๅฝ• WARN ๆ—ฅๅฟ—๏ผŒไธ้œ€่ฆๆ‰“ๅฐๅ †ๆ ˆ - // ========================================== + + // ==================== 1. ๅฎขๆˆท็ซฏ่ฏทๆฑ‚้”™่ฏฏ (400 Bad Request ็ญ‰) ==================== + // ่ฟ™็ฑป้”™่ฏฏๆ˜ฏๅ‰็ซฏไผ ๅ‚ไธๅฏน๏ผŒ่ฎฐๅฝ• WARN ๆ—ฅๅฟ—๏ผŒไธ้œ€่ฆๆ‰“ๅฐๅ †ๆ ˆ /** - * 1. ๅ‚ๆ•ฐๆ ก้ชŒๅคฑ่ดฅๅผ‚ๅธธ (@Valid / @Validated) - * ๅœบๆ™ฏ๏ผšๅ‰็ซฏไผ ็š„ JSON ็ผบๅญ—ๆฎต๏ผŒๆˆ–่€…ๅญ—ๆฎตไธ็ฌฆๅˆ @NotNull, @Size ็ญ‰ๆณจ่งฃ่ฆๆฑ‚ + * ๅค„็†ๅ‚ๆ•ฐๆ ก้ชŒๅคฑ่ดฅๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ๅ‰็ซฏไผ ็š„ JSON ็ผบๅญ—ๆฎต + * - ๅญ—ๆฎตไธ็ฌฆๅˆ @NotNull, @Size ็ญ‰ๆณจ่งฃ่ฆๆฑ‚ + * - ไฝฟ็”จ @Valid ๆˆ– @Validated ่งฆๅ‘ + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class}) public RestBean handleValidationException(Exception e) { + // 1. ่Žทๅ–็ป‘ๅฎš็ป“ๆžœ BindingResult bindingResult = null; if (e instanceof MethodArgumentNotValidException) { bindingResult = ((MethodArgumentNotValidException) e).getBindingResult(); } else if (e instanceof BindException) { bindingResult = ((BindException) e).getBindingResult(); } - - // ๆๅ–ๅ…ทไฝ“็š„้”™่ฏฏไฟกๆฏ๏ผˆไพ‹ๅฆ‚๏ผš"email: ้‚ฎ็ฎฑๆ ผๅผไธๆญฃ็กฎ"๏ผ‰ + + // 2. ๆๅ–ๅ…ทไฝ“็š„้”™่ฏฏไฟกๆฏ๏ผˆไพ‹ๅฆ‚๏ผš"email: ้‚ฎ็ฎฑๆ ผๅผไธๆญฃ็กฎ"๏ผ‰ String msg = "ๅ‚ๆ•ฐๆ ก้ชŒๅคฑ่ดฅ"; if (bindingResult != null) { msg = bindingResult.getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); } - + log.warn("ๅ‚ๆ•ฐๆ ก้ชŒๆœช้€š่ฟ‡: {}", msg); - return RestBean.failure(RestCode.FAILURE, msg); + return RestBean.failure(RestCode.FAILURE.getCode(), msg); } /** - * 2. JSON ๆ ผๅผ่งฃๆž้”™่ฏฏ - * ๅœบๆ™ฏ๏ผšๅ‰็ซฏไผ ็š„ JSON ๅฐ‘ไบ†ๆ‹ฌๅท๏ผŒๆˆ–่€…ๆŠŠ String ไผ ็ป™ไบ† Integer ็ฑปๅž‹็š„ๅญ—ๆฎต + * ๅค„็† JSON ๆ ผๅผ่งฃๆž้”™่ฏฏ + * ๅœบๆ™ฏ๏ผš + * - ๅ‰็ซฏไผ ็š„ JSON ๅฐ‘ไบ†ๆ‹ฌๅท + * - ๆŠŠ String ไผ ็ป™ไบ† Integer ็ฑปๅž‹็š„ๅญ—ๆฎต + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(HttpMessageNotReadableException.class) public RestBean handleJsonParseException(HttpMessageNotReadableException e) { - log.warn("JSON่งฃๆžๅคฑ่ดฅ: {}", e.getMessage()); - return RestBean.failure(RestCode.FAILURE, "่ฏทๆฑ‚Bodyๆ ผๅผ้”™่ฏฏ๏ผŒ่ฏทๆฃ€ๆŸฅJSON่ฏญๆณ•"); + log.warn("JSON ่งฃๆžๅคฑ่ดฅ: {}", e.getMessage()); + return RestBean.failure(RestCode.FAILURE.getCode(), "่ฏทๆฑ‚ Body ๆ ผๅผ้”™่ฏฏ๏ผŒ่ฏทๆฃ€ๆŸฅ JSON ่ฏญๆณ•"); } /** - * 3. ็ผบๅฐ‘ๅฟ…่ฆ็š„ URL ๅ‚ๆ•ฐ - * ๅœบๆ™ฏ๏ผšๆŽฅๅฃๅฎšไน‰ไบ† @RequestParam(required=true) ไฝ†ๅ‰็ซฏๆฒกไผ  + * ๅค„็†็ผบๅฐ‘ๅฟ…่ฆ็š„ URL ๅ‚ๆ•ฐๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ๆŽฅๅฃๅฎšไน‰ไบ† @RequestParam(required=true) ไฝ†ๅ‰็ซฏๆฒกไผ  + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(MissingServletRequestParameterException.class) public RestBean handleMissingParam(MissingServletRequestParameterException e) { log.warn("็ผบๅฐ‘่ฏทๆฑ‚ๅ‚ๆ•ฐ: {}", e.getParameterName()); - return RestBean.failure(RestCode.FAILURE, "็ผบๅฐ‘ๅฟ…่ฆๅ‚ๆ•ฐ: " + e.getParameterName()); + return RestBean.failure(RestCode.FAILURE.getCode(), "็ผบๅฐ‘ๅฟ…่ฆๅ‚ๆ•ฐ: " + e.getParameterName()); } /** - * 4. ่ฏทๆฑ‚ๆ–นๆณ•ไธๆ”ฏๆŒ - * ๅœบๆ™ฏ๏ผšๆŽฅๅฃๅชๅ†™ไบ† @PostMapping๏ผŒๅ‰็ซฏๅด็”จ GET ่ฏทๆฑ‚่ฎฟ้—ฎ + * ๅค„็†่ฏทๆฑ‚ๆ–นๆณ•ไธๆ”ฏๆŒๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ๆŽฅๅฃๅชๅ†™ไบ† @PostMapping๏ผŒๅ‰็ซฏๅด็”จ GET ่ฏทๆฑ‚่ฎฟ้—ฎ + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public RestBean handleMethodNotSupported(HttpRequestMethodNotSupportedException e) { log.warn("่ฏทๆฑ‚ๆ–นๆณ•ไธๆ”ฏๆŒ: method={}, supported={}", e.getMethod(), e.getSupportedHttpMethods()); - return RestBean.failure(RestCode.METHOD_NOT_SUPPORT, "ไธๆ”ฏๆŒ่ฏฅ่ฏทๆฑ‚ๆ–นๆณ•: " + e.getMethod()); + return RestBean.failure(RestCode.METHOD_NOT_SUPPORT.getCode(), "ไธๆ”ฏๆŒ่ฏฅ่ฏทๆฑ‚ๆ–นๆณ•: " + e.getMethod()); } /** - * 5. ๅ‚ๆ•ฐ็ฑปๅž‹ไธๅŒน้… (ๅˆšๆ‰ไฝ ้‡ๅˆฐ็š„้‚ฃไธช) + * ๅค„็†ๅ‚ๆ•ฐ็ฑปๅž‹ไธๅŒน้…ๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ๆŽฅๅฃๅฎšไน‰ไบ† Integer ็ฑปๅž‹๏ผŒๅ‰็ซฏไผ ไบ†ๅญ—็ฌฆไธฒ + * - ๆŽฅๅฃๅฎšไน‰ไบ† Boolean ็ฑปๅž‹๏ผŒๅ‰็ซฏไผ ไบ†้žๅธƒๅฐ”ๅ€ผ + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public RestBean handleTypeMismatch(MethodArgumentTypeMismatchException e) { String msg = String.format("ๅ‚ๆ•ฐ็ฑปๅž‹้”™่ฏฏ: ๅ‚ๆ•ฐ[%s] ้œ€่ฆ [%s]", e.getName(), e.getRequiredType().getSimpleName()); log.warn("ๅ‚ๆ•ฐ็ฑปๅž‹ไธๅŒน้…: {}", msg); - return RestBean.failure(RestCode.FAILURE, msg); + return RestBean.failure(RestCode.FAILURE.getCode(), msg); } - - // ========================================== - // 2. ไธšๅŠก้€ป่พ‘ไธŽๆ•ฐๆฎๅบ“้”™่ฏฏ - // ========================================== + + // ==================== 2. ไธšๅŠก้€ป่พ‘ไธŽๆ•ฐๆฎๅบ“้”™่ฏฏ ==================== /** - * 6. ่‡ชๅฎšไน‰ไธšๅŠกๅผ‚ๅธธ (ๆœ€ๅธธ็”จ๏ผ) - * ๅœบๆ™ฏ๏ผšไฝ ๅœจ Service ๅฑ‚ๆ‰‹ๅŠจๆŠ›ๅ‡บ throw new BusinessException(403, "ๆƒ้™ไธ่ถณ"); + * ๅค„็†่‡ชๅฎšไน‰ไธšๅŠกๅผ‚ๅธธ๏ผˆๆœ€ๅธธ็”จ๏ผ‰ + * ๅœบๆ™ฏ๏ผš + * - ๅœจ Service ๅฑ‚ๆ‰‹ๅŠจๆŠ›ๅ‡บ throw new BusinessException(403, "ๆƒ้™ไธ่ถณ") + * - ไธšๅŠก้€ป่พ‘ๆ ก้ชŒๅคฑ่ดฅ + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(BusinessException.class) public RestBean handleBusinessException(BusinessException e) { // ไธšๅŠกๅผ‚ๅธธ้€šๅธธๆ˜ฏ้ข„ๆœŸๅ†…็š„๏ผŒ่ฎฐๅฝ• WARN ๅณๅฏ log.warn("ไธšๅŠกๅผ‚ๅธธ: code={}, message={}", e.getCode(), e.getMessage()); - return RestBean.failure(e.getCode(), e.getMessage(),""); + return RestBean.failure(e.getCode(), e.getMessage(), ""); } /** - * 7. ๆ•ฐๆฎๅบ“ๅ”ฏไธ€้”ฎๅ†ฒ็ช - * ๅœบๆ™ฏ๏ผšๆณจๅ†Œๆ—ถ็”จๆˆทๅๅทฒๅญ˜ๅœจ๏ผŒๆ’ๅ…ฅๆ•ฐๆฎๅบ“ๆ—ถ่งฆๅ‘ Unique Constraint + * ๅค„็†ๆ•ฐๆฎๅบ“ๅ”ฏไธ€้”ฎๅ†ฒ็ชๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ๆณจๅ†Œๆ—ถ็”จๆˆทๅๅทฒๅญ˜ๅœจ + * - ๆ’ๅ…ฅๆ•ฐๆฎๅบ“ๆ—ถ่งฆๅ‘ Unique Constraint + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(DuplicateKeyException.class) public RestBean handleDuplicateKeyException(DuplicateKeyException e) { log.warn("ๆ•ฐๆฎๅบ“ๆ•ฐๆฎๅ†ฒ็ช: {}", e.getMessage()); - return RestBean.failure(RestCode.DATA_ALREADY_FOUND, "ๆ•ฐๆฎๅทฒๅญ˜ๅœจ๏ผŒ่ฏทๅ‹ฟ้‡ๅคๆ“ไฝœ"); + return RestBean.failure(RestCode.DATA_ALREADY_FOUND.getCode(), "ๆ•ฐๆฎๅทฒๅญ˜ๅœจ๏ผŒ่ฏทๅ‹ฟ้‡ๅคๆ“ไฝœ"); } - // ========================================== - // 3. ่‡ดๅ‘ฝ็ณป็ปŸ้”™่ฏฏ (500) - // ่ฟ™็ฑป้”™่ฏฏๆ˜ฏ Bug๏ผŒๅฟ…้กป่ฎฐๅฝ•ๅ †ๆ ˆไฟกๆฏ (e)๏ผŒๅนถๆŠฅ่ญฆ - // ========================================== + /** + * ๅค„็†้™ๆต่ถ…ๅ‡บๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ็”จๆˆท่ฏทๆฑ‚่ฟ‡ไบŽ้ข‘็น๏ผŒ่งฆๅ‘้™ๆต + * - ่ถ…่ฟ‡ๆŽฅๅฃ่ฐƒ็”จ็š„้ข‘็އ้™ๅˆถ + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ”๏ผˆHTTP 429๏ผ‰ + */ + @ExceptionHandler(RateLimitExceededException.class) + public RestBean handleRateLimitExceeded(RateLimitExceededException e) { + log.warn("่ฏทๆฑ‚้™ๆต: {}", e.getMessage()); + return RestBean.failure(429, "่ฏทๆฑ‚่ฟ‡ไบŽ้ข‘็น๏ผŒ่ฏท็จๅŽๅ†่ฏ•"); + } + + // ==================== 3. ่‡ดๅ‘ฝ็ณป็ปŸ้”™่ฏฏ (500) ==================== + // ่ฟ™็ฑป้”™่ฏฏๆ˜ฏ Bug๏ผŒๅฟ…้กป่ฎฐๅฝ•ๅ †ๆ ˆไฟกๆฏ (e)๏ผŒๅนถๆŠฅ่ญฆ /** - * 8. ็ฉบๆŒ‡้’ˆๅผ‚ๅธธ (NullPointerException) - * ๅœบๆ™ฏ๏ผšไปฃ็ ้‡Œๆฒกๅšๅˆค็ฉบ๏ผŒa.b() ๆ—ถ a ๆ˜ฏ null + * ๅค„็†็ฉบๆŒ‡้’ˆๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ไปฃ็ ้‡Œๆฒกๅšๅˆค็ฉบ๏ผŒa.b() ๆ—ถ a ๆ˜ฏ null + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(NullPointerException.class) public RestBean handleNPE(NullPointerException e) { // ๅฟ…้กปๆ‰“ๅฐๅ †ๆ ˆ๏ผ - log.error("ๅ‘็”Ÿ็ฉบๆŒ‡้’ˆๅผ‚ๅธธ: ", e); - return RestBean.failure(RestCode.SYSTEM_ERROR, "็ณป็ปŸๅ†…้ƒจๆ•ฐๆฎๅผ‚ๅธธ๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜"); + log.error("ๅ‘็”Ÿ็ฉบๆŒ‡้’ˆๅผ‚ๅธธ: ", e); + return RestBean.failure(RestCode.SYSTEM_ERROR.getCode(), "็ณป็ปŸๅ†…้ƒจๆ•ฐๆฎๅผ‚ๅธธ๏ผŒ่ฏท่”็ณป็ฎก็†ๅ‘˜"); } /** * ๅค„็†ๆƒ้™ๆ‹’็ปๅผ‚ๅธธ - */ + * ๅœบๆ™ฏ๏ผš + * - ็”จๆˆท่ฎฟ้—ฎไบ†ๆ— ๆƒ้™็š„ๆŽฅๅฃ + * - @PreAuthorize ๆณจ่งฃๆ ก้ชŒๅคฑ่ดฅ + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” + */ @ExceptionHandler(AuthorizationDeniedException.class) public RestBean handleAuthorizationDenied(AuthorizationDeniedException e) { log.warn("ๆƒ้™ๆ‹’็ป: {}", e.getMessage()); @@ -146,7 +217,13 @@ public class GlobalExceptionHandler { } /** - * ๅค„็†ๅ…ถไป–ๅผ‚ๅธธ + * ๅค„็†ๅ…ถไป–ๆœชๆ•่Žท็š„ๅผ‚ๅธธ + * ๅœบๆ™ฏ๏ผš + * - ็ณป็ปŸไธญๆœช้ข„ๆœŸ็š„ๅผ‚ๅธธ + * - ๅ…œๅบ•็š„ๅผ‚ๅธธๅค„็† + * + * @param e ๅผ‚ๅธธๅฏน่ฑก + * @return ้”™่ฏฏๅ“ๅบ” */ @ExceptionHandler(Exception.class) public RestBean handleException(Exception e) { diff --git a/src/main/java/com/aisi/template/interceptor/LoggingInterceptor.java b/src/main/java/com/aisi/template/interceptor/LoggingInterceptor.java index 2d35ba1..33532f4 100644 --- a/src/main/java/com/aisi/template/interceptor/LoggingInterceptor.java +++ b/src/main/java/com/aisi/template/interceptor/LoggingInterceptor.java @@ -10,51 +10,117 @@ import org.springframework.web.servlet.HandlerInterceptor; import java.util.UUID; +/** + * ๆ—ฅๅฟ—ๆ‹ฆๆˆชๅ™จ + * ่ฎฐๅฝ•ๆ‰€ๆœ‰ API ่ฏทๆฑ‚็š„ๆ—ฅๅฟ—ไฟกๆฏ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่ฏทๆฑ‚ๅ‰่ฎฐๅฝ•๏ผš่ฎฐๅฝ•่ฏทๆฑ‚ๅผ€ๅง‹ไฟกๆฏ + * 2. ่ฏทๆฑ‚ๅŽ่ฎฐๅฝ•๏ผš่ฎฐๅฝ•่ฏทๆฑ‚ๅฎŒๆˆไฟกๆฏๅ’Œ่€—ๆ—ถ + * 3. ็”จๆˆท่ฟฝ่ธช๏ผš่ฎฐๅฝ•ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * + * ๆ—ฅๅฟ—ๅ†…ๅฎน๏ผš + * - ่ฏทๆฑ‚ID๏ผš็”จไบŽ่ฟฝ่ธชๆ•ดไธช่ฏทๆฑ‚้“พ่ทฏ + * - ่ฏทๆฑ‚ๆ–นๆณ•๏ผšGET, POST, PUT, DELETE + * - ่ฏทๆฑ‚่ทฏๅพ„๏ผšAPI ่ทฏๅพ„ + * - ็”จๆˆทไฟกๆฏ๏ผš็”จๆˆทIDใ€็”จๆˆทๅ + * - ๅ“ๅบ”็Šถๆ€๏ผšHTTP ็Šถๆ€็  + * - ่ฏทๆฑ‚่€—ๆ—ถ๏ผšๆฏซ็ง’ + * + * @author Claude + * @since 2024-04-09 + */ @Component public class LoggingInterceptor implements HandlerInterceptor { + /** + * ๆ—ฅๅฟ—่ฎฐๅฝ•ๅ™จ + */ private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class); + /** + * ่ฏทๆฑ‚ๅ‰ๅค„็† + * ๆญฅ้ชค๏ผš + * 1. ่ฎฐๅฝ•่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ถ้—ด + * 2. ็”Ÿๆˆ่ฏทๆฑ‚ID๏ผˆ8ไฝUUID๏ผ‰ + * 3. ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * 4. ่ฎฐๅฝ•่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ฅๅฟ— + * + * @param request HTTP ่ฏทๆฑ‚ + * @param response HTTP ๅ“ๅบ” + * @param handler ๅค„็†ๅ™จ + * @return true ่กจ็คบ็ปง็ปญๆ‰ง่กŒ๏ผŒfalse ่กจ็คบไธญๆ–ญ + */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 1. ่ฎฐๅฝ•่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ถ้—ด long startTime = System.currentTimeMillis(); request.setAttribute("startTime", startTime); + // 2. ็”Ÿๆˆ่ฏทๆฑ‚ID๏ผˆ8ไฝUUID๏ผŒไพฟไบŽๆ—ฅๅฟ—่ฟฝ่ธช๏ผ‰ String requestId = UUID.randomUUID().toString().substring(0, 8); request.setAttribute("requestId", requestId); + // 3. ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ Long userId = SecurityUtils.getUserId(); request.setAttribute("userId", userId); String username = SecurityUtils.getUsername(); request.setAttribute("username", username); + // 4. ่ฎฐๅฝ• API ่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ฅๅฟ— if (isApiRequest(request.getRequestURI())) { - logger.info("[{}] API่ฏทๆฑ‚ๅผ€ๅง‹ -> method: {}, path: {}, userId: {}, username: {}", + logger.info("[{}] API ่ฏทๆฑ‚ๅผ€ๅง‹ -> method: {}, path: {}, userId: {}, username: {}", requestId, request.getMethod(), request.getRequestURI(), userId, username); } - return true; // โœ… ๅฟ…้กป่ฟ”ๅ›žtrueๆ‰่ƒฝ็ปง็ปญๆ‰ง่กŒ่ฏทๆฑ‚ + return true; // ๅฟ…้กป่ฟ”ๅ›ž true ๆ‰่ƒฝ็ปง็ปญๆ‰ง่กŒ่ฏทๆฑ‚ } + /** + * ่ฏทๆฑ‚ๅฎŒๆˆๅŽๅค„็† + * ๆญฅ้ชค๏ผš + * 1. ่ฎก็ฎ—่ฏทๆฑ‚่€—ๆ—ถ + * 2. ่ฎฐๅฝ•่ฏทๆฑ‚ๅฎŒๆˆๆ—ฅๅฟ— + * 3. ๅฆ‚ๆžœๆœ‰ๅผ‚ๅธธ๏ผŒ่ฎฐๅฝ•ๅผ‚ๅธธๆ—ฅๅฟ— + * + * @param request HTTP ่ฏทๆฑ‚ + * @param response HTTP ๅ“ๅบ” + * @param handler ๅค„็†ๅ™จ + * @param ex ๅผ‚ๅธธ๏ผˆๅฆ‚ๆžœๆœ‰๏ผ‰ + */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 1. ่Žทๅ–่ฏทๆฑ‚ๅผ€ๅง‹ๆ—ถ้—ด Long startTime = (Long) request.getAttribute("startTime"); if (startTime != null && isApiRequest(request.getRequestURI())) { + // 2. ่ฎก็ฎ—่ฏทๆฑ‚่€—ๆ—ถ long duration = System.currentTimeMillis() - startTime; + + // 3. ่Žทๅ–่ฏทๆฑ‚ไฟกๆฏ String requestId = (String) request.getAttribute("requestId"); Long userId = (Long) request.getAttribute("userId"); String username = (String) request.getAttribute("username"); - logger.info("[{}] API่ฏทๆฑ‚ๅฎŒๆˆ -> method: {}, path: {}, status: {}, userId: {}, username: {}, duration: {}ms", + // 4. ่ฎฐๅฝ• API ่ฏทๆฑ‚ๅฎŒๆˆๆ—ฅๅฟ— + logger.info("[{}] API ่ฏทๆฑ‚ๅฎŒๆˆ -> method: {}, path: {}, status: {}, userId: {}, username: {}, duration: {}ms", requestId, request.getMethod(), request.getRequestURI(), response.getStatus(), userId, username, duration); + // 5. ๅฆ‚ๆžœๆœ‰ๅผ‚ๅธธ๏ผŒ่ฎฐๅฝ•ๅผ‚ๅธธๆ—ฅๅฟ— if (ex != null) { logger.error("[{}] ่ฏทๆฑ‚ๅผ‚ๅธธ: {}", requestId, ex.getMessage(), ex); } } } + /** + * ๅˆคๆ–ญๆ˜ฏๅฆไธบ API ่ฏทๆฑ‚ + * ่ฏดๆ˜Ž๏ผš + * - API ่ฏทๆฑ‚็š„่ทฏๅพ„ไปฅ /api/ ๅผ€ๅคด + * + * @param path ่ฏทๆฑ‚่ทฏๅพ„ + * @return ๆ˜ฏๅฆไธบ API ่ฏทๆฑ‚ + */ private boolean isApiRequest(String path) { return path.startsWith("/api/"); } diff --git a/src/main/java/com/aisi/template/mq/consumer/UserMessageConsumer.java b/src/main/java/com/aisi/template/mq/consumer/UserMessageConsumer.java new file mode 100644 index 0000000..c3d9b27 --- /dev/null +++ b/src/main/java/com/aisi/template/mq/consumer/UserMessageConsumer.java @@ -0,0 +1,267 @@ +package com.aisi.template.mq.consumer; + +import com.aisi.template.mq.message.UserMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * ็”จๆˆทๆถˆๆฏๆถˆ่ดน่€… + * ไปŽ RocketMQ ๆถˆ่ดน็”จๆˆท็›ธๅ…ณๆถˆๆฏๅนถๅค„็† + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * 1. ็”จๆˆทๆณจๅ†ŒๅŽๅ‘้€ๆฌข่ฟŽ้‚ฎไปถ + * 2. ็”จๆˆท็™ปๅฝ•ๅŽ่ฎฐๅฝ•็™ปๅฝ•ๆ—ฅๅฟ—ๅˆฐๆ•ฐๆฎๅบ“ + * 3. ็”จๆˆทๆ›ดๆ–ฐๅŽๅŒๆญฅๅˆฐๅ…ถไป–็ณป็ปŸ๏ผˆๅฆ‚ๆ•ฐๆฎไป“ๅบ“๏ผ‰ + * 4. ็”จๆˆทๅˆ ้™คๅŽๆธ…็†ๅ…ณ่”ๆ•ฐๆฎ + * + * ๆถˆ่ดน่€…็ป„่ฏดๆ˜Ž๏ผš + * - consumerGroup: ๆถˆ่ดน่€…็ป„ๅ๏ผŒๅŒไธ€ไธช็ป„ๅ†…ๅชๆœ‰ไธ€ไธชๆถˆ่ดน่€…่ƒฝๆถˆ่ดนๆŸๆกๆถˆๆฏ + * - topic: ๆถˆ่ดน็š„ไธป้ข˜ๅ็งฐ + * - consumeMode: ๆถˆ่ดนๆจกๅผ๏ผŒCONCURRENTLY๏ผˆๅนถๅ‘ๆถˆ่ดน๏ผ‰ๆˆ– ORDERLY๏ผˆ้กบๅบๆถˆ่ดน๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Component +@ConditionalOnProperty(name = "rocketmq.name-server") +@RocketMQMessageListener( + // ๆถˆ่ดน่€…็ป„ๅ็งฐ + consumerGroup = "user-consumer-group", + // ่ฎข้˜…็š„ไธป้ข˜ๅ็งฐ + topic = "${rocketmq.producer.user-topic:user-topic}", + // ๆถˆ่ดนๆจกๅผ๏ผšๅนถๅ‘ๆถˆ่ดน๏ผŒๆ้ซ˜ๅžๅ้‡ + consumeMode = org.apache.rocketmq.spring.annotation.ConsumeMode.CONCURRENTLY, + // ๆถˆๆฏๆจกๅž‹๏ผš้›†็พคๆถˆ่ดน๏ผˆๅนฟๆ’ญๆถˆ่ดนไธบ BROADCASTING๏ผ‰ + messageModel = org.apache.rocketmq.spring.annotation.MessageModel.CLUSTERING +) +public class UserMessageConsumer implements org.apache.rocketmq.spring.core.RocketMQListener { + + /** + * ๆถˆ่ดนๆถˆๆฏ็š„ๆ–นๆณ• + * ๆญฅ้ชค๏ผš + * 1. ๆŽฅๆ”ถๆถˆๆฏๅฏน่ฑก + * 2. ๆ นๆฎๆถˆๆฏ็ฑปๅž‹่ฟ›่กŒไธๅŒ็š„ๅค„็† + * 3. ๅค„็†ๆˆๅŠŸ่ฟ”ๅ›ž๏ผŒๅค„็†ๅคฑ่ดฅๆŠ›ๅ‡บๅผ‚ๅธธ + * + * ๆณจๆ„ไบ‹้กน๏ผš + * - ๅฆ‚ๆžœๆ–นๆณ•ๆญฃๅธธ่ฟ”ๅ›ž๏ผŒๆถˆๆฏไผš่ขซ็กฎ่ฎคๆถˆ่ดนๆˆๅŠŸ + * - ๅฆ‚ๆžœๆŠ›ๅ‡บๅผ‚ๅธธ๏ผŒๆถˆๆฏไผš่ขซ้‡ๆ–ฐๆถˆ่ดน๏ผˆๆ นๆฎ้…็ฝฎ็š„้‡่ฏ•ๆฌกๆ•ฐ๏ผ‰ + * - ๆถˆ่ดนๅน‚็ญ‰ๆ€ง๏ผš้œ€่ฆไธšๅŠกๆ–นไฟ่ฏ๏ผŒๅปบ่ฎฎไฝฟ็”จๅ”ฏไธ€IDๅŽป้‡ + * + * @param message ๆถˆๆฏๅฏน่ฑก + */ + @Override + public void onMessage(UserMessage message) { + try { + log.info("ๆ”ถๅˆฐ็”จๆˆทๆถˆๆฏ - messageId: {}, type: {}, userId: {}", + message.getMessageId(), message.getMessageType(), message.getUserId()); + + // 1. ๆ นๆฎๆถˆๆฏ็ฑปๅž‹่ฟ›่กŒไธๅŒ็š„ๅค„็† + switch (message.getMessageType()) { + case "REGISTER": + // ๅค„็†็”จๆˆทๆณจๅ†Œๆถˆๆฏ + handleRegisterMessage(message); + break; + + case "LOGIN": + // ๅค„็†็”จๆˆท็™ปๅฝ•ๆถˆๆฏ + handleLoginMessage(message); + break; + + case "LOGOUT": + // ๅค„็†็”จๆˆท็™ปๅ‡บๆถˆๆฏ + handleLogoutMessage(message); + break; + + case "UPDATE": + // ๅค„็†็”จๆˆทๆ›ดๆ–ฐๆถˆๆฏ + handleUpdateMessage(message); + break; + + case "DELETE": + // ๅค„็†็”จๆˆทๅˆ ้™คๆถˆๆฏ + handleDeleteMessage(message); + break; + + default: + log.warn("ๆœช็Ÿฅๆถˆๆฏ็ฑปๅž‹: {}", message.getMessageType()); + } + + log.info("ๆถˆๆฏๅค„็†ๅฎŒๆˆ - messageId: {}", message.getMessageId()); + + } catch (Exception e) { + // 2. ๅค„็†ๅคฑ่ดฅ๏ผŒ่ฎฐๅฝ•้”™่ฏฏๆ—ฅๅฟ— + // ๆŠ›ๅ‡บๅผ‚ๅธธไผšๅฏผ่‡ดๆถˆๆฏ้‡ๆ–ฐๆถˆ่ดน + log.error("ๆถˆๆฏๅค„็†ๅคฑ่ดฅ - messageId: {}, error: {}", message.getMessageId(), e.getMessage(), e); + throw new RuntimeException("ๆถˆๆฏๅค„็†ๅคฑ่ดฅ: " + e.getMessage(), e); + } + } + + /** + * ๅค„็†็”จๆˆทๆณจๅ†Œๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ๅ‘้€ๆฌข่ฟŽ้‚ฎไปถ + * 2. ๅˆๅง‹ๅŒ–็”จๆˆทๆ•ฐๆฎ + * 3. ่ฎฐๅฝ•ๆณจๅ†Œๆ—ฅๅฟ— + * + * @param message ็”จๆˆทๆถˆๆฏ + */ + private void handleRegisterMessage(UserMessage message) { + log.info("ๅค„็†็”จๆˆทๆณจๅ†Œๆถˆๆฏ - userId: {}, username: {}", message.getUserId(), message.getUsername()); + + // 1. ๅ‘้€ๆฌข่ฟŽ้‚ฎไปถ๏ผˆ็คบไพ‹๏ผ‰ + // ๅฎž้™…้กน็›ฎไธญ่ฟ™้‡Œๅบ”่ฏฅ่ฐƒ็”จ้‚ฎไปถๆœๅŠก + sendWelcomeEmail(message.getUsername(), message.getEmail()); + + // 2. ๅˆๅง‹ๅŒ–็”จๆˆทๆ•ฐๆฎ๏ผˆ็คบไพ‹๏ผ‰ + // ไพ‹ๅฆ‚๏ผšๅˆ›ๅปบ็”จๆˆท้…็ฝฎใ€ๅˆๅง‹ๅŒ–็”จๆˆท้’ฑๅŒ…็ญ‰ + initializeUserData(message.getUserId()); + + // 3. ่ฎฐๅฝ•ๆณจๅ†Œๆ—ฅๅฟ—๏ผˆ็คบไพ‹๏ผ‰ + logRegistration(message); + } + + /** + * ๅค„็†็”จๆˆท็™ปๅฝ•ๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ่ฎฐๅฝ•็™ปๅฝ•ๆ—ฅๅฟ—ๅˆฐๆ•ฐๆฎๅบ“ + * 2. ๆ›ดๆ–ฐๆœ€ๅŽ็™ปๅฝ•ๆ—ถ้—ด + * 3. ๆฃ€ๆŸฅๅผ‚ๅธธ็™ปๅฝ•๏ผˆๅผ‚ๅœฐ็™ปๅฝ•็ญ‰๏ผ‰ + * + * @param message ็”จๆˆทๆถˆๆฏ + */ + private void handleLoginMessage(UserMessage message) { + log.info("ๅค„็†็”จๆˆท็™ปๅฝ•ๆถˆๆฏ - userId: {}, username: {}", message.getUserId(), message.getUsername()); + + // 1. ่ฎฐๅฝ•็™ปๅฝ•ๆ—ฅๅฟ— + saveLoginLog(message); + + // 2. ๆ›ดๆ–ฐๆœ€ๅŽ็™ปๅฝ•ๆ—ถ้—ด + updateLastLoginTime(message.getUserId()); + + // 3. ๆฃ€ๆŸฅๅผ‚ๅธธ็™ปๅฝ• + checkAbnormalLogin(message); + } + + /** + * ๅค„็†็”จๆˆท็™ปๅ‡บๆถˆๆฏ + * + * @param message ็”จๆˆทๆถˆๆฏ + */ + private void handleLogoutMessage(UserMessage message) { + log.info("ๅค„็†็”จๆˆท็™ปๅ‡บๆถˆๆฏ - userId: {}, username: {}", message.getUserId(), message.getUsername()); + // ๆธ…็†็”จๆˆท็ผ“ๅญ˜ใ€็™ปๅ‡บๅ…ถไป–่ฎพๅค‡็ญ‰ + } + + /** + * ๅค„็†็”จๆˆทๆ›ดๆ–ฐๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ๅŒๆญฅ็”จๆˆทๆ•ฐๆฎๅˆฐๅ…ถไป–็ณป็ปŸ + * 2. ๆ›ดๆ–ฐ็ผ“ๅญ˜ + * 3. ่ฎฐๅฝ•ๅ˜ๆ›ดๆ—ฅๅฟ— + * + * @param message ็”จๆˆทๆถˆๆฏ + */ + private void handleUpdateMessage(UserMessage message) { + log.info("ๅค„็†็”จๆˆทๆ›ดๆ–ฐๆถˆๆฏ - userId: {}, content: {}", message.getUserId(), message.getContent()); + + // 1. ๅŒๆญฅๆ•ฐๆฎๅˆฐๆ•ฐๆฎไป“ๅบ“ + syncToDataWarehouse(message); + + // 2. ๆ›ดๆ–ฐ็ผ“ๅญ˜ + evictUserCache(message.getUserId()); + + // 3. ่ฎฐๅฝ•ๅ˜ๆ›ดๆ—ฅๅฟ— + logChange(message); + } + + /** + * ๅค„็†็”จๆˆทๅˆ ้™คๆถˆๆฏ + * + * @param message ็”จๆˆทๆถˆๆฏ + */ + private void handleDeleteMessage(UserMessage message) { + log.info("ๅค„็†็”จๆˆทๅˆ ้™คๆถˆๆฏ - userId: {}, username: {}", message.getUserId(), message.getUsername()); + + // 1. ๆธ…็†็”จๆˆทๅ…ณ่”ๆ•ฐๆฎ + // 2. ๅฝ’ๆกฃ็”จๆˆทๆ•ฐๆฎ + // 3. ๆธ…็†็ผ“ๅญ˜ + } + + // ==================== ไปฅไธ‹ๆ˜ฏ่พ…ๅŠฉๆ–นๆณ• ==================== + + /** + * ๅ‘้€ๆฌข่ฟŽ้‚ฎไปถ + */ + private void sendWelcomeEmail(String username, String email) { + log.info("ๅ‘้€ๆฌข่ฟŽ้‚ฎไปถ - username: {}, email: {}", username, email); + // TODO: ๅฎž็Žฐ้‚ฎไปถๅ‘้€้€ป่พ‘ + } + + /** + * ๅˆๅง‹ๅŒ–็”จๆˆทๆ•ฐๆฎ + */ + private void initializeUserData(Long userId) { + log.info("ๅˆๅง‹ๅŒ–็”จๆˆทๆ•ฐๆฎ - userId: {}", userId); + // TODO: ๅฎž็Žฐๆ•ฐๆฎๅˆๅง‹ๅŒ–้€ป่พ‘ + } + + /** + * ่ฎฐๅฝ•ๆณจๅ†Œๆ—ฅๅฟ— + */ + private void logRegistration(UserMessage message) { + log.info("่ฎฐๅฝ•ๆณจๅ†Œๆ—ฅๅฟ— - userId: {}", message.getUserId()); + // TODO: ๅฎž็Žฐๆ—ฅๅฟ—่ฎฐๅฝ•้€ป่พ‘ + } + + /** + * ไฟๅญ˜็™ปๅฝ•ๆ—ฅๅฟ— + */ + private void saveLoginLog(UserMessage message) { + log.info("ไฟๅญ˜็™ปๅฝ•ๆ—ฅๅฟ— - userId: {}", message.getUserId()); + // TODO: ๅฎž็Žฐ็™ปๅฝ•ๆ—ฅๅฟ—ไฟๅญ˜้€ป่พ‘ + } + + /** + * ๆ›ดๆ–ฐๆœ€ๅŽ็™ปๅฝ•ๆ—ถ้—ด + */ + private void updateLastLoginTime(Long userId) { + log.info("ๆ›ดๆ–ฐๆœ€ๅŽ็™ปๅฝ•ๆ—ถ้—ด - userId: {}", userId); + // TODO: ๅฎž็Žฐๆ›ดๆ–ฐ้€ป่พ‘ + } + + /** + * ๆฃ€ๆŸฅๅผ‚ๅธธ็™ปๅฝ• + */ + private void checkAbnormalLogin(UserMessage message) { + log.info("ๆฃ€ๆŸฅๅผ‚ๅธธ็™ปๅฝ• - userId: {}", message.getUserId()); + // TODO: ๅฎž็Žฐๅผ‚ๅธธ็™ปๅฝ•ๆฃ€ๆต‹้€ป่พ‘ + } + + /** + * ๅŒๆญฅๅˆฐๆ•ฐๆฎไป“ๅบ“ + */ + private void syncToDataWarehouse(UserMessage message) { + log.info("ๅŒๆญฅๆ•ฐๆฎๅˆฐไป“ๅบ“ - userId: {}", message.getUserId()); + // TODO: ๅฎž็Žฐๆ•ฐๆฎๅŒๆญฅ้€ป่พ‘ + } + + /** + * ๆธ…้™ค็”จๆˆท็ผ“ๅญ˜ + */ + private void evictUserCache(Long userId) { + log.info("ๆธ…้™ค็”จๆˆท็ผ“ๅญ˜ - userId: {}", userId); + // TODO: ๅฎž็Žฐ็ผ“ๅญ˜ๆธ…็†้€ป่พ‘ + } + + /** + * ่ฎฐๅฝ•ๅ˜ๆ›ดๆ—ฅๅฟ— + */ + private void logChange(UserMessage message) { + log.info("่ฎฐๅฝ•ๅ˜ๆ›ดๆ—ฅๅฟ— - userId: {}", message.getUserId()); + // TODO: ๅฎž็Žฐๅ˜ๆ›ดๆ—ฅๅฟ—่ฎฐๅฝ•้€ป่พ‘ + } +} diff --git a/src/main/java/com/aisi/template/mq/message/UserMessage.java b/src/main/java/com/aisi/template/mq/message/UserMessage.java new file mode 100644 index 0000000..8e3f6c9 --- /dev/null +++ b/src/main/java/com/aisi/template/mq/message/UserMessage.java @@ -0,0 +1,65 @@ +package com.aisi.template.mq.message; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * ็”จๆˆท็›ธๅ…ณๆถˆๆฏ + * ็”จไบŽ RocketMQ ๆถˆๆฏไผ ่พ“๏ผŒๅฎž็Žฐ Serializable ๆŽฅๅฃไปฅๆ”ฏๆŒๅบๅˆ—ๅŒ– + * + * @author Claude + * @since 2024-04-09 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * ๆถˆๆฏๅ”ฏไธ€ๆ ‡่ฏ† + */ + private String messageId; + + /** + * ็”จๆˆทID + */ + private Long userId; + + /** + * ็”จๆˆทๅ + */ + private String username; + + /** + * ้‚ฎ็ฎฑ + */ + private String email; + + /** + * ๆถˆๆฏ็ฑปๅž‹๏ผšREGISTER-ๆณจๅ†Œ, LOGIN-็™ปๅฝ•, LOGOUT-็™ปๅ‡บ, UPDATE-ๆ›ดๆ–ฐ, DELETE-ๅˆ ้™ค + */ + private String messageType; + + /** + * ๆถˆๆฏๅ†…ๅฎน/่ฏฆๆƒ… + */ + private String content; + + /** + * ๅˆ›ๅปบๆ—ถ้—ด + */ + private LocalDateTime createTime; + + /** + * ๆ‰ฉๅฑ•ไฟกๆฏ๏ผˆJSON ๆ ผๅผ๏ผ‰ + */ + private String extInfo; +} diff --git a/src/main/java/com/aisi/template/mq/producer/UserMessageProducer.java b/src/main/java/com/aisi/template/mq/producer/UserMessageProducer.java new file mode 100644 index 0000000..a676903 --- /dev/null +++ b/src/main/java/com/aisi/template/mq/producer/UserMessageProducer.java @@ -0,0 +1,180 @@ +package com.aisi.template.mq.producer; + +import com.aisi.template.mq.message.UserMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.core.RocketMQTemplate; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * ็”จๆˆทๆถˆๆฏ็”Ÿไบง่€… + * ่ดŸ่ดฃๅ‘ RocketMQ ๅ‘้€็”จๆˆท็›ธๅ…ณๆถˆๆฏ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * 1. ็”จๆˆทๆณจๅ†ŒๆˆๅŠŸๅŽๅ‘้€ๆฌข่ฟŽๆถˆๆฏ + * 2. ็”จๆˆท็™ปๅฝ•ๅŽๅ‘้€็™ปๅฝ•้€š็Ÿฅ + * 3. ็”จๆˆท็™ปๅ‡บๅŽ่ฎฐๅฝ•็™ปๅ‡บๆ—ฅๅฟ— + * 4. ็”จๆˆทไฟกๆฏๆ›ดๆ–ฐๅŽๅŒๆญฅๆ•ฐๆฎ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Component +@ConditionalOnProperty(name = "rocketmq.name-server") +public class UserMessageProducer { + + /** + * RocketMQ ๆจกๆฟ๏ผŒ็”จไบŽๅ‘้€ๆถˆๆฏ + */ + private final RocketMQTemplate rocketMQTemplate; + + /** + * ็”จๆˆทไธป้ข˜ๅ็งฐ + * ไปŽ้…็ฝฎๆ–‡ไปถ่ฏปๅ–๏ผŒ้ป˜่ฎคไธบ user-topic + */ + @Value("${rocketmq.producer.user-topic:user-topic}") + private String userTopic; + + /** + * ๆž„้€ ๅ‡ฝๆ•ฐๆณจๅ…ฅไพ่ต– + * + * @param rocketMQTemplate RocketMQ ๆจกๆฟ + */ + public UserMessageProducer(RocketMQTemplate rocketMQTemplate) { + this.rocketMQTemplate = rocketMQTemplate; + } + + /** + * ๅ‘้€็”จๆˆทๆณจๅ†Œๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบ็”จๆˆทๆถˆๆฏๅฏน่ฑก + * 2. ่ฎพ็ฝฎๆถˆๆฏ็ฑปๅž‹ไธบ REGISTER + * 3. ๅ‘้€ๅˆฐ RocketMQ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param email ้‚ฎ็ฎฑ + */ + public void sendRegisterMessage(Long userId, String username, String email) { + // 1. ๆž„ๅปบๆถˆๆฏๅฏน่ฑก + UserMessage message = UserMessage.builder() + .messageId(UUID.randomUUID().toString()) + .userId(userId) + .username(username) + .email(email) + .messageType("REGISTER") + .content("็”จๆˆทๆณจๅ†ŒๆˆๅŠŸ: " + username) + .createTime(LocalDateTime.now()) + .build(); + + // 2. ๅ‘้€ๆถˆๆฏๅˆฐ RocketMQ + // syncSend: ๅŒๆญฅๅ‘้€๏ผŒไผš็ญ‰ๅพ…ๅ‘้€็ป“ๆžœ + // MessageBuilder.wrap: ็”จไบŽๆž„ๅปบ Spring Messaging ๆถˆๆฏ + rocketMQTemplate.syncSend(userTopic, MessageBuilder.withPayload(message).build()); + + log.info("็”จๆˆทๆณจๅ†Œๆถˆๆฏๅ‘้€ๆˆๅŠŸ - userId: {}, username: {}", userId, username); + } + + /** + * ๅ‘้€็”จๆˆท็™ปๅฝ•ๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบ็”จๆˆทๆถˆๆฏๅฏน่ฑก + * 2. ่ฎพ็ฝฎๆถˆๆฏ็ฑปๅž‹ไธบ LOGIN + * 3. ๅผ‚ๆญฅๅ‘้€ๅˆฐ RocketMQ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param ip ็™ปๅฝ•IPๅœฐๅ€ + */ + public void sendLoginMessage(Long userId, String username, String ip) { + // 1. ๆž„ๅปบๆถˆๆฏๅฏน่ฑก + UserMessage message = UserMessage.builder() + .messageId(UUID.randomUUID().toString()) + .userId(userId) + .username(username) + .messageType("LOGIN") + .content("็”จๆˆท็™ปๅฝ• - IP: " + ip) + .createTime(LocalDateTime.now()) + .extInfo("{\"ip\":\"" + ip + "\"}") + .build(); + + // 2. ๅผ‚ๆญฅๅ‘้€ๆถˆๆฏ + // asyncSend: ไธไผš้˜ปๅกžๅฝ“ๅ‰็บฟ็จ‹๏ผŒ้€‚ๅˆไธ้œ€่ฆ็ซ‹ๅณ็Ÿฅ้“ๅ‘้€็ป“ๆžœ็š„ๅœบๆ™ฏ + rocketMQTemplate.asyncSend(userTopic, MessageBuilder.withPayload(message).build(), null); + + log.info("็”จๆˆท็™ปๅฝ•ๆถˆๆฏๅ‘้€ๆˆๅŠŸ - userId: {}, username: {}, ip: {}", userId, username, ip); + } + + /** + * ๅ‘้€็”จๆˆท็™ปๅ‡บๆถˆๆฏ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + */ + public void sendLogoutMessage(Long userId, String username) { + UserMessage message = UserMessage.builder() + .messageId(UUID.randomUUID().toString()) + .userId(userId) + .username(username) + .messageType("LOGOUT") + .content("็”จๆˆท็™ปๅ‡บ") + .createTime(LocalDateTime.now()) + .build(); + + rocketMQTemplate.syncSend(userTopic, MessageBuilder.withPayload(message).build()); + log.info("็”จๆˆท็™ปๅ‡บๆถˆๆฏๅ‘้€ๆˆๅŠŸ - userId: {}, username: {}", userId, username); + } + + /** + * ๅ‘้€็”จๆˆทๆ›ดๆ–ฐๆถˆๆฏ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบ็”จๆˆทๆถˆๆฏๅฏน่ฑก + * 2. ่ฎพ็ฝฎๆถˆๆฏ็ฑปๅž‹ไธบ UPDATE + * 3. ๆทปๅŠ ๆ›ดๆ–ฐๅ†…ๅฎน็š„ๆ‰ฉๅฑ•ไฟกๆฏ + * 4. ๅ‘้€ๅˆฐ RocketMQ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param updateContent ๆ›ดๆ–ฐๅ†…ๅฎนๆ่ฟฐ + */ + public void sendUpdateMessage(Long userId, String username, String updateContent) { + UserMessage message = UserMessage.builder() + .messageId(UUID.randomUUID().toString()) + .userId(userId) + .username(username) + .messageType("UPDATE") + .content("็”จๆˆทไฟกๆฏๆ›ดๆ–ฐ: " + updateContent) + .createTime(LocalDateTime.now()) + .extInfo("{\"updateContent\":\"" + updateContent + "\"}") + .build(); + + rocketMQTemplate.syncSend(userTopic, MessageBuilder.withPayload(message).build()); + log.info("็”จๆˆทๆ›ดๆ–ฐๆถˆๆฏๅ‘้€ๆˆๅŠŸ - userId: {}, username: {}, content: {}", userId, username, updateContent); + } + + /** + * ๅ‘้€็”จๆˆทๅˆ ้™คๆถˆๆฏ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + */ + public void sendDeleteMessage(Long userId, String username) { + UserMessage message = UserMessage.builder() + .messageId(UUID.randomUUID().toString()) + .userId(userId) + .username(username) + .messageType("DELETE") + .content("็”จๆˆทๅทฒๅˆ ้™ค: " + username) + .createTime(LocalDateTime.now()) + .build(); + + rocketMQTemplate.syncSend(userTopic, MessageBuilder.withPayload(message).build()); + log.info("็”จๆˆทๅˆ ้™คๆถˆๆฏๅ‘้€ๆˆๅŠŸ - userId: {}, username: {}", userId, username); + } +} diff --git a/src/main/java/com/aisi/template/repository/PasswordResetCodeRepository.java b/src/main/java/com/aisi/template/repository/PasswordResetCodeRepository.java index 2a1d681..5e89826 100644 --- a/src/main/java/com/aisi/template/repository/PasswordResetCodeRepository.java +++ b/src/main/java/com/aisi/template/repository/PasswordResetCodeRepository.java @@ -7,11 +7,66 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Optional; +/** + * ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŸบๆœฌๆ“ไฝœ๏ผš็ปงๆ‰ฟ CRUD ๆ“ไฝœ + * 2. ้ชŒ่ฏ็ ๆŸฅ่ฏข๏ผšๆŒ‰้‚ฎ็ฎฑๆŸฅๆ‰พ + * 3. ้ชŒ่ฏ็ ็ปŸ่ฎก๏ผš็ปŸ่ฎก่ฏทๆฑ‚ๆฌกๆ•ฐ + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ้ชŒ่ฏ็ ๅ“ˆๅธŒๅญ˜ๅ‚จ๏ผŒไธๅญ˜ๅ‚จๆ˜Žๆ–‡ + * - ๆ”ฏๆŒๆœ‰ๆ•ˆๆœŸๆŽงๅˆถ + * - ๆ”ฏๆŒๅฐ่ฏ•ๆฌกๆ•ฐ้™ๅˆถ + * + * @author Claude + * @since 2024-04-09 + */ public interface PasswordResetCodeRepository extends JpaRepository { + /** + * ๆŸฅๆ‰พ้‚ฎ็ฎฑ็š„ๆ‰€ๆœ‰ๆœชไฝฟ็”จ้ชŒ่ฏ็  + * ่ฏดๆ˜Ž๏ผš + * - ่ฟ”ๅ›žๆŒ‡ๅฎš้‚ฎ็ฎฑๆ‰€ๆœ‰ๆœชไฝฟ็”จ็š„้ชŒ่ฏ็  + * - ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅˆ— + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๆ’ค้”€ๆ—ง้ชŒ่ฏ็ ๆ—ถไฝฟ็”จ + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @return ๆœชไฝฟ็”จ็š„้ชŒ่ฏ็ ๅˆ—่กจ + */ List findByEmailAndUsedFalse(String email); + /** + * ๆŸฅๆ‰พ้‚ฎ็ฎฑ็š„ๆœ€ๆ–ฐๆœชไฝฟ็”จ้ชŒ่ฏ็  + * ่ฏดๆ˜Ž๏ผš + * - ่ฟ”ๅ›žๆŒ‡ๅฎš้‚ฎ็ฎฑๆœ€ๆ–ฐ็š„ใ€ๆœชไฝฟ็”จ็š„้ชŒ่ฏ็  + * - ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅˆ—๏ผŒๅ–็ฌฌไธ€ๆก + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ้ชŒ่ฏๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + * - ๆฃ€ๆŸฅ้ชŒ่ฏ็ ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @return ๆœ€ๆ–ฐ็š„้ชŒ่ฏ็ ๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ Optional findFirstByEmailAndUsedFalseOrderByCreatedAtDesc(String email); + /** + * ็ปŸ่ฎกๆŒ‡ๅฎšๆ—ถ้—ดๅŽ้‚ฎ็ฎฑ็š„้ชŒ่ฏ็ ่ฏทๆฑ‚ๆ•ฐ + * ่ฏดๆ˜Ž๏ผš + * - ็”จไบŽ้™ๅˆถ่ฏทๆฑ‚้ข‘็އ + * - ้˜ฒๆญข้ชŒ่ฏ็ ๆŽฅๅฃ่ขซๆปฅ็”จ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๆฃ€ๆŸฅ 1 ๅฐๆ—ถๅ†…่ฏทๆฑ‚ๆฌกๆ•ฐ๏ผˆ้™ๅˆถ 5 ๆฌก๏ผ‰ + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @param createdAt ่ฏทๆฑ‚ๆ—ถ้—ด๏ผˆ็ปŸ่ฎกๆญคๆ—ถ้—ดไน‹ๅŽ็š„่ฏทๆฑ‚๏ผ‰ + * @return ่ฏทๆฑ‚ๆ•ฐ้‡ + */ long countByEmailAndCreatedAtAfter(String email, LocalDateTime createdAt); } diff --git a/src/main/java/com/aisi/template/repository/RefreshTokenRepository.java b/src/main/java/com/aisi/template/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..ecbcf52 --- /dev/null +++ b/src/main/java/com/aisi/template/repository/RefreshTokenRepository.java @@ -0,0 +1,82 @@ +package com.aisi.template.repository; + +import com.aisi.template.domain.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * Refresh Token ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰ Refresh Token ็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. Token ๆŸฅ่ฏข๏ผšๆŒ‰ๅ“ˆๅธŒๅ€ผๆŸฅๆ‰พใ€ๆŒ‰็”จๆˆทIDๆŸฅๆ‰พๆœ‰ๆ•ˆToken + * 2. Token ๆธ…็†๏ผšๅˆ ้™ค่ฟ‡ๆœŸ็š„Token + * 3. Token ๆ’ค้”€๏ผšๆ‰น้‡ๆ’ค้”€็”จๆˆท็š„Token + * + * ๅฎ‰ๅ…จ่ฏดๆ˜Ž๏ผš + * - ๆ•ฐๆฎๅบ“ๅชๅญ˜ๅ‚จ Token ็š„ๅ“ˆๅธŒๅ€ผ๏ผŒไธๅญ˜ๅ‚จๅŽŸๅง‹ๅ€ผ + * - ๅŽŸๅง‹ Token ๅชๅœจๅˆ›ๅปบๆ—ถ่ฟ”ๅ›ž็ป™ๅฎขๆˆท็ซฏ + * + * @author Claude + * @since 2024-04-09 + */ +public interface RefreshTokenRepository extends JpaRepository { + + /** + * ๆ นๆฎ Token ๅ“ˆๅธŒๅ€ผๆŸฅๆ‰พ Refresh Token + * ่ฏดๆ˜Ž๏ผš + * - ๆ•ฐๆฎๅบ“ๅชๅญ˜ๅ‚จๅ“ˆๅธŒๅ€ผ + * - ๅฎขๆˆท็ซฏไผ ๅ…ฅๅŽŸๅง‹ๅ€ผ๏ผŒ้œ€่ฆๅ…ˆๅ“ˆๅธŒๅ†ๆŸฅ่ฏข + * + * @param tokenHash Token ็š„ SHA-256 ๅ“ˆๅธŒๅ€ผ + * @return Refresh Token ๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + Optional findByTokenHash(String tokenHash); + + /** + * ๆŸฅๆ‰พ็”จๆˆท็š„ๆ‰€ๆœ‰ๆœ‰ๆ•ˆ Refresh Token + * ่ฏดๆ˜Ž๏ผš + * - ๆœ‰ๆ•ˆ = ๆœช่ฟ‡ๆœŸ ไธ” ๆœชๆ’ค้”€ + * - ็”จไบŽๅฑ•็คบ็”จๆˆท็š„็™ปๅฝ•่ฎพๅค‡ๅˆ—่กจ + * + * @param userId ็”จๆˆทID + * @param now ๅฝ“ๅ‰ๆ—ถ้—ด + * @return ๆœ‰ๆ•ˆ Refresh Token ๅˆ—่กจ + */ + @Query("SELECT rt FROM RefreshToken rt WHERE rt.userId = :userId AND rt.revoked = false AND rt.expiresAt > :now") + List findValidTokensByUserId(@Param("userId") Long userId, @Param("now") LocalDateTime now); + + /** + * ๅˆ ้™คๆ‰€ๆœ‰่ฟ‡ๆœŸ็š„ Refresh Token + * ่ฏดๆ˜Ž๏ผš + * - ็”จไบŽๅฎšๆœŸๆธ…็†ไปปๅŠก + * - ๅปบ่ฎฎ้€š่ฟ‡ๅฎšๆ—ถไปปๅŠกๆฏๅคฉๆ‰ง่กŒ + * + * @param expiresAt ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆๅˆ ้™คๆ—ฉไบŽๆญคๆ—ถ้—ด็š„Token๏ผ‰ + */ + void deleteByExpiresAtBefore(LocalDateTime expiresAt); + + /** + * ๆ’ค้”€็”จๆˆท็š„ๆ‰€ๆœ‰ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ๅฐ†็”จๆˆทๆ‰€ๆœ‰ๆœชๆ’ค้”€็š„ Token ๆ ‡่ฎฐไธบๅทฒๆ’ค้”€ + * 2. ่ฎฐๅฝ•ๆ’ค้”€ๆ—ถ้—ด + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทไฟฎๆ”นๅฏ†็ ๅŽๆ’ค้”€ๆ‰€ๆœ‰ Token + * - ็”จๆˆท็™ปๅ‡บๆ‰€ๆœ‰่ฎพๅค‡ + * - ็ฎก็†ๅ‘˜ๅผบๅˆถ็”จๆˆทไธ‹็บฟ + * + * @param userId ็”จๆˆทID + * @param revokedAt ๆ’ค้”€ๆ—ถ้—ด + */ + @Query("UPDATE RefreshToken rt SET rt.revoked = true, rt.revokedAt = :revokedAt WHERE rt.userId = :userId AND rt.revoked = false") + @Modifying(clearAutomatically = true, flushAutomatically = true) + void revokeAllUserTokens(@Param("userId") Long userId, @Param("revokedAt") LocalDateTime revokedAt); +} diff --git a/src/main/java/com/aisi/template/repository/SysAuditLogRepository.java b/src/main/java/com/aisi/template/repository/SysAuditLogRepository.java new file mode 100644 index 0000000..84cc099 --- /dev/null +++ b/src/main/java/com/aisi/template/repository/SysAuditLogRepository.java @@ -0,0 +1,86 @@ +package com.aisi.template.repository; + +import com.aisi.template.domain.entity.SysAuditLog; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * ็ณป็ปŸๅฎก่ฎกๆ—ฅๅฟ—ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰ๅฎก่ฎกๆ—ฅๅฟ—็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŸบๆœฌๆ“ไฝœ๏ผš็ปงๆ‰ฟ CRUD ๆ“ไฝœ + * 2. ๆ—ฅๅฟ—ๆŸฅ่ฏข๏ผšๆŒ‰็”จๆˆทใ€ๆ“ไฝœใ€่ต„ๆบใ€ๆ—ถ้—ด่Œƒๅ›ดๆŸฅ่ฏข + * 3. ๅŠจๆ€ๆŸฅ่ฏข๏ผšๆ”ฏๆŒ Specification ๅŠจๆ€ๆกไปถๆŸฅ่ฏข + * 4. ๆ—ฅๅฟ—ๆธ…็†๏ผšๅˆ ้™ค่ฟ‡ๆœŸๆ—ฅๅฟ— + * + * ๅฎก่ฎกๆ—ฅๅฟ—ๅ†…ๅฎน๏ผš + * - ๆ“ไฝœ็”จๆˆท + * - ๆ“ไฝœ็ฑปๅž‹๏ผˆCREATE, UPDATE, DELETE๏ผ‰ + * - ๆ“ไฝœ่ต„ๆบ๏ผˆuser, role, menu๏ผ‰ + * - ๆ“ไฝœ็ป“ๆžœ๏ผˆๆˆๅŠŸ/ๅคฑ่ดฅ๏ผ‰ + * - ่ฏทๆฑ‚ไฟกๆฏ๏ผˆIPๅœฐๅ€ใ€User-Agent๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysAuditLogRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * ๆ นๆฎ็”จๆˆทIDๆŸฅๆ‰พๅฎก่ฎกๆ—ฅๅฟ— + * ่ฏดๆ˜Ž๏ผš + * - ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅบ + * - ็”จไบŽๆŸฅ็œ‹็”จๆˆท็š„ๆ“ไฝœๅކๅฒ + * + * @param userId ็”จๆˆทID + * @return ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ—่กจ + */ + List findByUserIdOrderByCreatedAtDesc(Long userId); + + /** + * ๆ นๆฎๆ“ไฝœ็ฑปๅž‹ๆŸฅๆ‰พๅฎก่ฎกๆ—ฅๅฟ— + * ่ฏดๆ˜Ž๏ผš + * - ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅบ + * - ็”จไบŽๆŸฅ็œ‹็‰นๅฎš็ฑปๅž‹็š„ๆ“ไฝœ่ฎฐๅฝ• + * + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆLOGIN, LOGOUT, CREATE, UPDATE, DELETE๏ผ‰ + * @return ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ—่กจ + */ + List findByActionOrderByCreatedAtDesc(String action); + + /** + * ๆ นๆฎ่ต„ๆบๆŸฅๆ‰พๅฎก่ฎกๆ—ฅๅฟ— + * ่ฏดๆ˜Ž๏ผš + * - ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅบ + * - ็”จไบŽๆŸฅ็œ‹็‰นๅฎš่ต„ๆบ็š„ๅ˜ๆ›ดๅކๅฒ + * + * @param resource ่ต„ๆบๅ็งฐ๏ผˆuser, role, menu, permission๏ผ‰ + * @return ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ—่กจ + */ + List findByResourceOrderByCreatedAtDesc(String resource); + + /** + * ๆ นๆฎๆ—ถ้—ด่Œƒๅ›ดๆŸฅๆ‰พๅฎก่ฎกๆ—ฅๅฟ— + * ่ฏดๆ˜Ž๏ผš + * - ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅบ + * - ็”จไบŽๆ—ถ้—ด่Œƒๅ›ดๅ†…็š„็ปŸ่ฎกๅˆ†ๆž + * + * @param start ๅผ€ๅง‹ๆ—ถ้—ด + * @param end ็ป“ๆŸๆ—ถ้—ด + * @return ๅฎก่ฎกๆ—ฅๅฟ—ๅˆ—่กจ + */ + List findByCreatedAtBetweenOrderByCreatedAtDesc(LocalDateTime start, LocalDateTime end); + + /** + * ๅˆ ้™คๆŒ‡ๅฎšๆ—ฅๆœŸไน‹ๅ‰็š„ๅฎก่ฎกๆ—ฅๅฟ— + * ่ฏดๆ˜Ž๏ผš + * - ็”จไบŽๅฎšๆœŸๆธ…็†ๆ—งๆ—ฅๅฟ— + * - ๅปบ่ฎฎ้€š่ฟ‡ๅฎšๆ—ถไปปๅŠกๅฎšๆœŸๆ‰ง่กŒ + * + * @param date ๆ—ฅๆœŸ๏ผˆๅˆ ้™คๆ—ฉไบŽๆญคๆ—ฅๆœŸ็š„ๆ—ฅๅฟ—๏ผ‰ + */ + void deleteByCreatedAtBefore(LocalDateTime date); +} diff --git a/src/main/java/com/aisi/template/repository/SysMenuRepository.java b/src/main/java/com/aisi/template/repository/SysMenuRepository.java new file mode 100644 index 0000000..b06b2b1 --- /dev/null +++ b/src/main/java/com/aisi/template/repository/SysMenuRepository.java @@ -0,0 +1,110 @@ +package com.aisi.template.repository; + +import com.aisi.template.domain.entity.SysMenu; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Set; + +/** + * ็ณป็ปŸ่œๅ•ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰่œๅ•็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŸบๆœฌๆ“ไฝœ๏ผš็ปงๆ‰ฟ CRUD ๆ“ไฝœ + * 2. ่œๅ•ๆŸฅ่ฏข๏ผšๆŒ‰็ˆถIDใ€็Šถๆ€ใ€็ฑปๅž‹ๆŸฅ่ฏข + * 3. ๅ…ณ่”ๆŸฅ่ฏข๏ผšๆŸฅ่ฏข่ง’่‰ฒ็š„่œๅ•ๅˆ—่กจ + * + * ่œๅ•็ฑปๅž‹๏ผš + * - DIRECTORY(1)๏ผš็›ฎๅฝ• + * - MENU(2)๏ผš่œๅ• + * - BUTTON(3)๏ผšๆŒ‰้’ฎ + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysMenuRepository extends JpaRepository { + + /** + * ๆ นๆฎ็ˆถIDๆŸฅๆ‰พ่œๅ• + * ่ฏดๆ˜Ž๏ผš + * - ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * - ็”จไบŽๆž„ๅปบ่œๅ•ๆ ‘ + * + * @param parentId ็ˆถ่œๅ•ID๏ผˆnull ่กจ็คบๆ น่œๅ•๏ผ‰ + * @return ๅญ่œๅ•ๅˆ—่กจ + */ + List findByParentIdOrderBySortOrder(Long parentId); + + /** + * ๆ นๆฎ็Šถๆ€ๆŸฅๆ‰พ่œๅ• + * ่ฏดๆ˜Ž๏ผš + * - ๅช่ฟ”ๅ›žๅฏ็”จ็Šถๆ€็š„่œๅ• + * - ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * @param status ็Šถๆ€๏ผˆ1=ๅฏ็”จ๏ผŒ0=็ฆ็”จ๏ผ‰ + * @return ่œๅ•ๅˆ—่กจ + */ + List findByStatusOrderBySortOrder(Integer status); + + /** + * ๆ นๆฎๅฏ่งๆ€งใ€็Šถๆ€ใ€็ฑปๅž‹ๆŸฅๆ‰พ่œๅ• + * ่ฏดๆ˜Ž๏ผš + * - ๅŒๆ—ถๆปก่ถณๅฏ่งใ€ๅฏ็”จใ€ๆŒ‡ๅฎš็ฑปๅž‹ + * - ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * @param visible ๅฏ่งๆ€ง๏ผˆ1=ๅฏ่ง๏ผŒ0=ไธๅฏ่ง๏ผ‰ + * @param status ็Šถๆ€๏ผˆ1=ๅฏ็”จ๏ผŒ0=็ฆ็”จ๏ผ‰ + * @param menuType ่œๅ•็ฑปๅž‹๏ผˆ1=็›ฎๅฝ•๏ผŒ2=่œๅ•๏ผŒ3=ๆŒ‰้’ฎ๏ผ‰ + * @return ่œๅ•ๅˆ—่กจ + */ + List findByVisibleAndStatusAndMenuTypeOrderBySortOrder(Integer visible, Integer status, Integer menuType); + + /** + * ๆ นๆฎ่ง’่‰ฒIDๆŸฅๆ‰พ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ้€š่ฟ‡่ง’่‰ฒๅ…ณ่”ๆŸฅ่ฏข่œๅ• + * 2. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * @param roleId ่ง’่‰ฒID + * @return ่œๅ•้›†ๅˆ + */ + @Query("SELECT m FROM SysMenu m JOIN m.roles r WHERE r.id = :roleId ORDER BY m.sortOrder") + Set findByRoleId(@Param("roleId") Long roleId); + + /** + * ๆ นๆฎๅคšไธช่ง’่‰ฒIDๆŸฅๆ‰พ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ฟ™ไบ›่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆ‰€ๆœ‰่œๅ• + * 2. ไฝฟ็”จ DISTINCT ๅŽป้‡ + * 3. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–็”จๆˆท็š„ๆ‰€ๆœ‰่œๅ•๏ผˆ็”จๆˆทๆœ‰ๅคšไธช่ง’่‰ฒ๏ผ‰ + * + * @param roleIds ่ง’่‰ฒIDๅˆ—่กจ + * @return ่œๅ•้›†ๅˆ + */ + @Query("SELECT DISTINCT m FROM SysMenu m JOIN m.roles r WHERE r.id IN :roleIds ORDER BY m.sortOrder") + Set findByRoleIds(@Param("roleIds") List roleIds); + + /** + * ๆ นๆฎๅคšไธช่ง’่‰ฒIDๅ’Œ่œๅ•็ฑปๅž‹ๆŸฅๆ‰พ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ฟ™ไบ›่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆŒ‡ๅฎš็ฑปๅž‹่œๅ• + * 2. ๅช่ฟ”ๅ›žๅฏ็”จไธ”ๅฏ่ง็š„่œๅ• + * 3. ไฝฟ็”จ DISTINCT ๅŽป้‡ + * 4. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–็”จๆˆท็š„่œๅ•ๆ ‘๏ผˆๅช่ฟ”ๅ›ž็›ฎๅฝ•ๅ’Œ่œๅ•๏ผŒไธๅŒ…ๆ‹ฌๆŒ‰้’ฎ๏ผ‰ + * + * @param roleIds ่ง’่‰ฒIDๅˆ—่กจ + * @param menuType ่œๅ•็ฑปๅž‹๏ผˆ1=็›ฎๅฝ•๏ผŒ2=่œๅ•๏ผ‰ + * @return ่œๅ•ๅˆ—่กจ + */ + @Query("SELECT DISTINCT m FROM SysMenu m JOIN m.roles r WHERE r.id IN :roleIds AND m.menuType = :menuType AND m.status = 1 AND m.visible = 1 ORDER BY m.sortOrder") + List findByRoleIdsAndMenuType(@Param("roleIds") List roleIds, @Param("menuType") Integer menuType); +} diff --git a/src/main/java/com/aisi/template/repository/SysPermissionRepository.java b/src/main/java/com/aisi/template/repository/SysPermissionRepository.java new file mode 100644 index 0000000..2d35c9a --- /dev/null +++ b/src/main/java/com/aisi/template/repository/SysPermissionRepository.java @@ -0,0 +1,97 @@ +package com.aisi.template.repository; + +import com.aisi.template.domain.entity.SysPermission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * ็ณป็ปŸๆƒ้™ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰ๆƒ้™็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŸบๆœฌๆ“ไฝœ๏ผš็ปงๆ‰ฟ CRUD ๆ“ไฝœ + * 2. ๆƒ้™ๆŸฅ่ฏข๏ผšๆŒ‰็ผ–็ ใ€็Šถๆ€ใ€่ต„ๆบใ€ๆ“ไฝœๆŸฅ่ฏข + * 3. ๅ…ณ่”ๆŸฅ่ฏข๏ผšๆŸฅ่ฏข่ง’่‰ฒ็š„ๆƒ้™ๅˆ—่กจ + * + * ๆƒ้™ๆ ผๅผ๏ผš + * - ่ต„ๆบ:ๆ“ไฝœ๏ผŒๅฆ‚ user:read, user:write + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysPermissionRepository extends JpaRepository { + + /** + * ๆ นๆฎๆƒ้™็ผ–็ ๆŸฅๆ‰พๆƒ้™ + * ่ฏดๆ˜Ž๏ผš + * - ๆƒ้™็ผ–็ ๆ˜ฏๅ”ฏไธ€็š„๏ผŒๅฆ‚๏ผšuser:read, role:write + * + * @param permissionCode ๆƒ้™็ผ–็  + * @return ๆƒ้™ๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + Optional findByPermissionCode(String permissionCode); + + /** + * ๆ นๆฎ็Šถๆ€ๆŸฅๆ‰พๆƒ้™ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–ๆ‰€ๆœ‰ๅฏ็”จ็š„ๆƒ้™ + * + * @param status ็Šถๆ€๏ผˆ1=ๅฏ็”จ๏ผŒ0=็ฆ็”จ๏ผ‰ + * @return ๆƒ้™ๅˆ—่กจ + */ + List findByStatus(Integer status); + + /** + * ๆ นๆฎ่ต„ๆบๆŸฅๆ‰พๆƒ้™ + * ่ฏดๆ˜Ž๏ผš + * - ่ฟ”ๅ›žๆŒ‡ๅฎš่ต„ๆบ็š„ๆ‰€ๆœ‰ๆ“ไฝœๆƒ้™ + * - ไพ‹ๅฆ‚๏ผšresource="user" ่ฟ”ๅ›ž user:read, user:write, user:delete + * + * @param resource ่ต„ๆบๅ็งฐ๏ผˆๅฆ‚๏ผšuser, role, menu๏ผ‰ + * @return ๆƒ้™ๅˆ—่กจ + */ + List findByResource(String resource); + + /** + * ๆ นๆฎๆ“ไฝœ็ฑปๅž‹ๆŸฅๆ‰พๆƒ้™ + * ่ฏดๆ˜Ž๏ผš + * - ่ฟ”ๅ›žๆ‹ฅๆœ‰ๆŒ‡ๅฎšๆ“ไฝœ็š„ๆ‰€ๆœ‰่ต„ๆบๆƒ้™ + * - ไพ‹ๅฆ‚๏ผšaction="read" ่ฟ”ๅ›ž user:read, role:read, menu:read ็ญ‰ + * + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆread, write, delete๏ผ‰ + * @return ๆƒ้™ๅˆ—่กจ + */ + List findByAction(String action); + + /** + * ๆ นๆฎ่ง’่‰ฒIDๆŸฅๆ‰พๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ้€š่ฟ‡่ง’่‰ฒๅ…ณ่”ๆŸฅ่ฏขๆƒ้™ + * 2. ่ฟ”ๅ›ž่ฏฅ่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆ‰€ๆœ‰ๆƒ้™ + * + * @param roleId ่ง’่‰ฒID + * @return ๆƒ้™้›†ๅˆ + */ + @Query("SELECT p FROM SysPermission p JOIN p.roles r WHERE r.id = :roleId") + Set findByRoleId(@Param("roleId") Long roleId); + + /** + * ๆ นๆฎๅคšไธช่ง’่‰ฒIDๆŸฅๆ‰พๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ฟ™ไบ›่ง’่‰ฒๆ‹ฅๆœ‰็š„ๆ‰€ๆœ‰ๆƒ้™ + * 2. ไฝฟ็”จ DISTINCT ๅŽป้‡ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–็”จๆˆท็š„ๆ‰€ๆœ‰ๆƒ้™๏ผˆ็”จๆˆทๆœ‰ๅคšไธช่ง’่‰ฒ๏ผ‰ + * + * @param roleIds ่ง’่‰ฒIDๅˆ—่กจ + * @return ๆƒ้™้›†ๅˆ + */ + @Query("SELECT DISTINCT p FROM SysPermission p JOIN p.roles r WHERE r.id IN :roleIds") + Set findByRoleIds(@Param("roleIds") List roleIds); +} diff --git a/src/main/java/com/aisi/template/repository/SysRoleRepository.java b/src/main/java/com/aisi/template/repository/SysRoleRepository.java new file mode 100644 index 0000000..8785b42 --- /dev/null +++ b/src/main/java/com/aisi/template/repository/SysRoleRepository.java @@ -0,0 +1,79 @@ +package com.aisi.template.repository; + +import com.aisi.template.domain.entity.SysRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/** + * ็ณป็ปŸ่ง’่‰ฒๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰่ง’่‰ฒ็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŸบๆœฌๆ“ไฝœ๏ผš็ปงๆ‰ฟ CRUD ๆ“ไฝœ + * 2. ่ง’่‰ฒๆŸฅ่ฏข๏ผšๆŒ‰่ง’่‰ฒ็ผ–็ ใ€็Šถๆ€ๆŸฅ่ฏข + * 3. ๅ…ณ่”ๆŸฅ่ฏข๏ผš้ข„ๅŠ ่ฝฝๆƒ้™ๅˆ—่กจ + * 4. ๅŠจๆ€ๆŸฅ่ฏข๏ผšๆ”ฏๆŒ Specification ๅŠจๆ€ๆกไปถๆŸฅ่ฏข + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysRoleRepository extends JpaRepository, JpaSpecificationExecutor { + + /** + * ๆ นๆฎ่ง’่‰ฒ็ผ–็ ๆŸฅๆ‰พ่ง’่‰ฒ + * ่ฏดๆ˜Ž๏ผš + * - ่ง’่‰ฒ็ผ–็ ๆ˜ฏๅ”ฏไธ€็š„๏ผŒๅฆ‚๏ผšROLE_USER, ROLE_ADMIN + * + * @param roleCode ่ง’่‰ฒ็ผ–็  + * @return ่ง’่‰ฒๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + Optional findByRoleCode(String roleCode); + + /** + * ๆ นๆฎ็Šถๆ€ๆŸฅๆ‰พ่ง’่‰ฒ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–ๆ‰€ๆœ‰ๅฏ็”จ็š„่ง’่‰ฒ + * - ่Žทๅ–ๆ‰€ๆœ‰็ฆ็”จ็š„่ง’่‰ฒ + * + * @param status ็Šถๆ€๏ผˆ1=ๅฏ็”จ๏ผŒ0=็ฆ็”จ๏ผ‰ + * @return ่ง’่‰ฒๅˆ—่กจ + */ + List findByStatus(Integer status); + + /** + * ๆ นๆฎIDๆŸฅๆ‰พ่ง’่‰ฒ๏ผˆ้ข„ๅŠ ่ฝฝๆƒ้™๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ LEFT JOIN FETCH ไธ€ๆฌกๆ€งๅŠ ่ฝฝ่ง’่‰ฒๅŠๅ…ถๆƒ้™ + * 2. ้ฟๅ… N+1 ๆŸฅ่ฏข้—ฎ้ข˜ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–่ง’่‰ฒ่ฏฆๆƒ…ๆ—ถ้œ€่ฆ่ฟ”ๅ›žๆƒ้™ๅˆ—่กจ + * - ่ง’่‰ฒ็ฎก็†้กต้ขๅฑ•็คบ + * + * @param id ่ง’่‰ฒID + * @return ่ง’่‰ฒๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + @Query("SELECT r FROM SysRole r LEFT JOIN FETCH r.permissions WHERE r.id = :id") + Optional findByIdWithPermissions(@Param("id") Long id); + + /** + * ๆŸฅๆ‰พๆ‰€ๆœ‰่ง’่‰ฒ๏ผˆ้ข„ๅŠ ่ฝฝๆƒ้™๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ LEFT JOIN FETCH ไธ€ๆฌกๆ€งๅŠ ่ฝฝๆ‰€ๆœ‰่ง’่‰ฒๅŠๆƒ้™ + * 2. ไฝฟ็”จ DISTINCT ๅŽป้‡ + * 3. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่ง’่‰ฒๅˆ—่กจๅฑ•็คบ + * - ่ง’่‰ฒ้€‰ๆ‹ฉไธ‹ๆ‹‰ๆก† + * + * @return ่ง’่‰ฒๅˆ—่กจ + */ + @Query("SELECT DISTINCT r FROM SysRole r LEFT JOIN FETCH r.permissions ORDER BY r.sortOrder") + List findAllWithPermissions(); +} diff --git a/src/main/java/com/aisi/template/repository/UserRepository.java b/src/main/java/com/aisi/template/repository/UserRepository.java index 3bb5f91..fd7a25f 100644 --- a/src/main/java/com/aisi/template/repository/UserRepository.java +++ b/src/main/java/com/aisi/template/repository/UserRepository.java @@ -3,40 +3,168 @@ package com.aisi.template.repository; import com.aisi.template.domain.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; +/** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ๅฎšไน‰็”จๆˆท็›ธๅ…ณ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŸบๆœฌๆ“ไฝœ๏ผš็ปงๆ‰ฟ CRUD ๆ“ไฝœ + * 2. ็”จๆˆทๆŸฅ่ฏข๏ผšๆŒ‰็”จๆˆทๅใ€้‚ฎ็ฎฑๆŸฅ่ฏข + * 3. ๅ…ณ่”ๆŸฅ่ฏข๏ผšไฝฟ็”จ JOIN FETCH ่งฃๅ†ณๆ‡’ๅŠ ่ฝฝ้—ฎ้ข˜ + * 4. ๅŠจๆ€ๆŸฅ่ฏข๏ผšๆ”ฏๆŒ Specification ๅŠจๆ€ๆกไปถๆŸฅ่ฏข + * + * ๆณจๆ„ไบ‹้กน๏ผš + * - ไฝฟ็”จ JOIN FETCH ้ฟๅ…ๆ‡’ๅŠ ่ฝฝๆ—ถ็š„ N+1 ้—ฎ้ข˜ + * - ้‚ฎ็ฎฑๆŸฅ่ฏขๆ”ฏๆŒไธๅŒบๅˆ†ๅคงๅฐๅ†™ + * - ๆ”ฏๆŒๆŒ‰็Šถๆ€็ญ›้€‰็”จๆˆท + * + * @author Claude + * @since 2024-04-09 + */ public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { + /** + * ๆ นๆฎ็”จๆˆทๅๆŸฅๆ‰พ็”จๆˆท + * + * @param username ็”จๆˆทๅ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ Optional findByUsername(String username); + /** + * ๆ นๆฎ็”จๆˆทๅๆŸฅๆ‰พ็”จๆˆท๏ผˆ้ข„ๅŠ ่ฝฝ่ง’่‰ฒ๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ LEFT JOIN FETCH ไธ€ๆฌกๆ€งๅŠ ่ฝฝ็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + * 2. ้ฟๅ… N+1 ๆŸฅ่ฏข้—ฎ้ข˜ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็™ปๅฝ•ๆ—ถ้œ€่ฆๅŠ ่ฝฝ็”จๆˆท่ง’่‰ฒ + * - ่Žทๅ–็”จๆˆทไฟกๆฏๆ—ถ้œ€่ฆ่ฟ”ๅ›ž่ง’่‰ฒๅˆ—่กจ + * + * @param username ็”จๆˆทๅ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username") + Optional findByUsernameWithRoles(@Param("username") String username); + + /** + * ๆ นๆฎ็”จๆˆทๅๆŸฅๆ‰พ็”จๆˆท๏ผˆ้ข„ๅŠ ่ฝฝ่ง’่‰ฒๅ’Œๆƒ้™๏ผ‰ + * + * @param username ็”จๆˆทๅ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.roles r LEFT JOIN FETCH r.permissions WHERE u.username = :username") + Optional findByUsernameWithRolesAndPermissions(@Param("username") String username); + + /** + * ๆ นๆฎ็”จๆˆทIDๆŸฅๆ‰พ็”จๆˆท๏ผˆ้ข„ๅŠ ่ฝฝ่ง’่‰ฒ๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ไฝฟ็”จ LEFT JOIN FETCH ไธ€ๆฌกๆ€งๅŠ ่ฝฝ็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + * 2. ้ฟๅ… N+1 ๆŸฅ่ฏข้—ฎ้ข˜ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่Žทๅ–็”จๆˆท่œๅ•ๆ—ถ้œ€่ฆๅŠ ่ฝฝ่ง’่‰ฒ + * + * @param id ็”จๆˆทID + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ + @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.id = :id") + Optional findByIdWithRoles(@Param("id") Long id); + + /** + * ๆ นๆฎ้‚ฎ็ฎฑๆŸฅๆ‰พ็”จๆˆท + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ Optional findByEmail(String email); + /** + * ๆ นๆฎ้‚ฎ็ฎฑๆŸฅๆ‰พ็”จๆˆท๏ผˆไธๅŒบๅˆ†ๅคงๅฐๅ†™๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - ๆ•ฐๆฎๅบ“ไฝฟ็”จ LOWER() ๅ‡ฝๆ•ฐ่ฟ›่กŒๆฏ”่พƒ + * - ้€‚็”จไบŽ็”จๆˆท่พ“ๅ…ฅ้‚ฎ็ฎฑๅคงๅฐๅ†™ไธ็กฎๅฎš็š„ๅœบๆ™ฏ + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ Optional findByEmailIgnoreCase(String email); + /** + * ๆ นๆฎ็”จๆˆทๅๅ’Œๅฏ†็ ๆŸฅๆ‰พ็”จๆˆท + * ่ฏดๆ˜Ž๏ผš + * - ๅฏ†็ ๆ˜ฏๅทฒๅŠ ๅฏ†็š„ๅ“ˆๅธŒๅ€ผ + * - ไธ€่ˆฌไธไฝฟ็”จๆญคๆ–นๆณ•๏ผŒๅบ”ๅ…ˆๆŸฅ่ฏข็”จๆˆทๅ†้ชŒ่ฏๅฏ†็  + * + * @param username ็”จๆˆทๅ + * @param password ๅฏ†็ ๅ“ˆๅธŒๅ€ผ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ Optional findByUsernameAndPassword(String username, String password); + /** + * ๆ นๆฎ้‚ฎ็ฎฑๅ’Œๅฏ†็ ๆŸฅๆ‰พ็”จๆˆท + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @param password ๅฏ†็ ๅ“ˆๅธŒๅ€ผ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ + */ Optional findByEmailAndPassword(String email, String password); /** - * ๆ นๆฎ็”จๆˆทๅๆŸฅๆ‰พๅฏ็”จ็š„็”จๆˆท + * ๆ นๆฎ็”จๆˆทๅๅ’Œ็Šถๆ€ๆŸฅๆ‰พ็”จๆˆท + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅชๆŸฅๆ‰พๅฏ็”จ็Šถๆ€็š„็”จๆˆท + * - ๅชๆŸฅๆ‰พ็ฆ็”จ็Šถๆ€็š„็”จๆˆท + * + * @param username ็”จๆˆทๅ + * @param status ็Šถๆ€๏ผˆ1=ๅฏ็”จ๏ผŒ0=็ฆ็”จ๏ผ‰ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ */ Optional findByUsernameAndStatus(String username, Integer status); /** - * ๆ นๆฎ้‚ฎ็ฎฑๆŸฅๆ‰พๅฏ็”จ็š„็”จๆˆท + * ๆ นๆฎ้‚ฎ็ฎฑๅ’Œ็Šถๆ€ๆŸฅๆ‰พ็”จๆˆท + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @param status ็Šถๆ€๏ผˆ1=ๅฏ็”จ๏ผŒ0=็ฆ็”จ๏ผ‰ + * @return ็”จๆˆทๅฏน่ฑก๏ผˆๅฏ่ƒฝไธบ็ฉบ๏ผ‰ */ Optional findByEmailAndStatus(String email, Integer status); /** * ๆฃ€ๆŸฅ็”จๆˆทๅๆ˜ฏๅฆๅญ˜ๅœจ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทๆณจๅ†Œๆ—ถๆ ก้ชŒ็”จๆˆทๅๆ˜ฏๅฆๅทฒ่ขซๅ ็”จ + * + * @param username ็”จๆˆทๅ + * @return true ่กจ็คบ็”จๆˆทๅๅทฒๅญ˜ๅœจ */ boolean existsByUsername(String username); /** * ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ + * ่ฏดๆ˜Ž๏ผš + * - ็ฒพ็กฎๅŒน้…๏ผŒๅŒบๅˆ†ๅคงๅฐๅ†™ + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @return true ่กจ็คบ้‚ฎ็ฎฑๅทฒๅญ˜ๅœจ */ boolean existsByEmail(String email); + /** + * ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ๏ผˆไธๅŒบๅˆ†ๅคงๅฐๅ†™๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - ๆ•ฐๆฎๅบ“ไฝฟ็”จ LOWER() ๅ‡ฝๆ•ฐ่ฟ›่กŒๆฏ”่พƒ + * - ้€‚็”จไบŽ็”จๆˆทๆณจๅ†Œๆ—ถๆ ก้ชŒ้‚ฎ็ฎฑ + * + * @param email ้‚ฎ็ฎฑๅœฐๅ€ + * @return true ่กจ็คบ้‚ฎ็ฎฑๅทฒๅญ˜ๅœจ + */ boolean existsByEmailIgnoreCase(String email); } diff --git a/src/main/java/com/aisi/template/service/AuditLogService.java b/src/main/java/com/aisi/template/service/AuditLogService.java new file mode 100644 index 0000000..c764730 --- /dev/null +++ b/src/main/java/com/aisi/template/service/AuditLogService.java @@ -0,0 +1,65 @@ +package com.aisi.template.service; + +import com.aisi.template.domain.entity.SysAuditLog; + +import java.time.LocalDateTime; + +/** + * ๅฎก่ฎกๆ—ฅๅฟ—ๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰ๅฎก่ฎกๆ—ฅๅฟ—็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—๏ผš่ฎฐๅฝ•็”จๆˆทๆ“ไฝœ + * 2. ๆธ…็†ๆ—ฅๅฟ—๏ผšๅˆ ้™ค่ฟ‡ๆœŸ็š„ๆ—ฅๅฟ— + * + * @author Claude + * @since 2024-04-09 + */ +public interface AuditLogService { + + /** + * ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ— + * + * @param auditLog ๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“ + */ + void createAuditLog(SysAuditLog auditLog); + + /** + * ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—๏ผˆไพฟๆทๆ–นๆณ•๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“ + * 2. ่ฎพ็ฝฎๆ—ฅๅฟ—ไฟกๆฏ + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆLOGIN, LOGOUT, CREATE, UPDATE, DELETE๏ผ‰ + * @param resource ่ต„ๆบ็ฑปๅž‹๏ผˆuser, role, menu๏ผ‰ + * @param resourceId ่ต„ๆบID + * @param description ๆ“ไฝœๆ่ฟฐ + * @param requestMethod ่ฏทๆฑ‚ๆ–นๆณ•๏ผˆGET, POST, PUT, DELETE๏ผ‰ + * @param requestUri ่ฏทๆฑ‚URI + * @param ipAddress IPๅœฐๅ€ + * @param userAgent User-Agent + * @param status ๆ“ไฝœ็Šถๆ€๏ผˆ1=ๆˆๅŠŸ๏ผŒ0=ๅคฑ่ดฅ๏ผ‰ + * @param errorMessage ้”™่ฏฏไฟกๆฏ๏ผˆๅคฑ่ดฅๆ—ถ๏ผ‰ + */ + void log(Long userId, String username, String action, String resource, + String resourceId, String description, String requestMethod, + String requestUri, String ipAddress, String userAgent, + Integer status, String errorMessage); + + /** + * ๆธ…็†ๆ—งๆ—ฅๅฟ— + * ๆญฅ้ชค๏ผš + * 1. ่ฎก็ฎ—่ฟ‡ๆœŸๆ—ถ้—ด + * 2. ๅˆ ้™ค่ฟ‡ๆœŸๆ—ฅๅฟ— + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅฎšๆ—ถไปปๅŠกๅฎšๆœŸๆธ…็† + * - ๅปบ่ฎฎไฟ็•™ๆœ€่ฟ‘ 90 ๅคฉ็š„ๆ—ฅๅฟ— + * + * @param daysToKeep ไฟ็•™ๅคฉๆ•ฐ + */ + void cleanupOldLogs(int daysToKeep); +} diff --git a/src/main/java/com/aisi/template/service/EmailService.java b/src/main/java/com/aisi/template/service/EmailService.java index 50f0f8a..5491d4d 100644 --- a/src/main/java/com/aisi/template/service/EmailService.java +++ b/src/main/java/com/aisi/template/service/EmailService.java @@ -1,6 +1,33 @@ package com.aisi.template.service; +/** + * ้‚ฎไปถๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰้‚ฎไปถๅ‘้€็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + * 2. ๅ‘้€ๆณจๅ†Œๆฌข่ฟŽ้‚ฎไปถ + * 3. ๅ‘้€้€š็Ÿฅ้‚ฎไปถ + * + * ้‚ฎไปถ็ฑปๅž‹๏ผš + * - ้ชŒ่ฏ็ ้‚ฎไปถ๏ผšๅŒ…ๅซ 6 ไฝๆ•ฐๅญ—้ชŒ่ฏ็  + * - ๆฌข่ฟŽ้‚ฎไปถ๏ผšๆฌข่ฟŽ็”จๆˆทๆณจๅ†Œ + * - ้€š็Ÿฅ้‚ฎไปถ๏ผš็ณป็ปŸ้€š็Ÿฅ + * + * @author Claude + * @since 2024-04-09 + */ public interface EmailService { + /** + * ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ้‚ฎไปถ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบ้‚ฎไปถๅ†…ๅฎน๏ผˆๅŒ…ๅซ้ชŒ่ฏ็ ๏ผ‰ + * 2. ่ฎพ็ฝฎ้‚ฎไปถไธป้ข˜ๅ’Œๆ”ถไปถไบบ + * 3. ๅ‘้€้‚ฎไปถ + * + * @param email ๆ”ถไปถไบบ้‚ฎ็ฎฑ + * @param code 6 ไฝๆ•ฐๅญ—้ชŒ่ฏ็  + */ void sendPasswordResetCode(String email, String code); } diff --git a/src/main/java/com/aisi/template/service/LoginAttemptService.java b/src/main/java/com/aisi/template/service/LoginAttemptService.java new file mode 100644 index 0000000..1049095 --- /dev/null +++ b/src/main/java/com/aisi/template/service/LoginAttemptService.java @@ -0,0 +1,91 @@ +package com.aisi.template.service; + +/** + * ็™ปๅฝ•ๅฐ่ฏ•ๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐๅ’Œ่ดฆๆˆท้”ๅฎš็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅๅฐ่ฏ• + * 2. ็™ปๅฝ•ๆˆๅŠŸๅŽ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + * 3. ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + * 4. ้”ๅฎš่ดฆๆˆท๏ผˆๆ‰‹ๅŠจ้”ๅฎš๏ผ‰ + * 5. ่Žทๅ–ๅ‰ฉไฝ™้”ๅฎšๆ—ถ้—ด + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ่ฟž็ปญๅคฑ่ดฅ 5 ๆฌก้”ๅฎš่ดฆๆˆท 30 ๅˆ†้’Ÿ + * - ็™ปๅฝ•ๆˆๅŠŸๅŽ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + * - ้”ๅฎšๆœŸ้—ดๆ— ๆณ•็™ปๅฝ• + * + * @author Claude + * @since 2024-04-09 + */ +public interface LoginAttemptService { + + /** + * ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅๅฐ่ฏ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๅฝ“ๅ‰ๅคฑ่ดฅๆฌกๆ•ฐ + * 2. ๅคฑ่ดฅๆฌกๆ•ฐๅŠ  1 + * 3. ๆฃ€ๆŸฅๆ˜ฏๅฆ่ถ…่ฟ‡ๆœ€ๅคงๅคฑ่ดฅๆฌกๆ•ฐ + * 4. ๅฆ‚ๆžœ่ถ…่ฟ‡๏ผŒ้”ๅฎš่ดฆๆˆทๅนถ่ฟ”ๅ›ž true + * 5. ไฟๅญ˜ๆ›ดๆ–ฐๅˆฐๆ•ฐๆฎๅบ“ + * + * @param username ็”จๆˆทๅ + * @return true ่กจ็คบ่ดฆๆˆทๅบ”่ฏฅ่ขซ้”ๅฎš + */ + boolean recordFailedAttempt(String username); + + /** + * ้‡็ฝฎ็™ปๅฝ•ๅคฑ่ดฅ่ฎกๆ•ฐ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆท + * 2. ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐไธบ 0 + * 3. ๆธ…้™ค้”ๅฎšๆ—ถ้—ด๏ผˆๅฆ‚ๆžœๆœ‰๏ผ‰ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็™ปๅฝ•ๆˆๅŠŸๅŽ่ฐƒ็”จ + * - ็ฎก็†ๅ‘˜ๆ‰‹ๅŠจ่งฃ้”ๅŽ่ฐƒ็”จ + * + * @param username ็”จๆˆทๅ + */ + void resetFailedAttempts(String username); + + /** + * ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆท็š„้”ๅฎšๆ—ถ้—ดๅ’Œๅคฑ่ดฅๆฌกๆ•ฐ + * 2. ๅˆคๆ–ญๆ˜ฏๅฆๅœจ้”ๅฎšๆœŸๅ†… + * + * @param username ็”จๆˆทๅ + * @return true ่กจ็คบ่ดฆๆˆท่ขซ้”ๅฎš + */ + boolean isLocked(String username); + + /** + * ้”ๅฎš่ดฆๆˆท + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆท + * 2. ่ฎพ็ฝฎ้”ๅฎšๆ—ถ้—ดไธบๅฝ“ๅ‰ๆ—ถ้—ด + ๆŒ‡ๅฎšๅˆ†้’Ÿๆ•ฐ + * 3. ไฟๅญ˜ๆ›ดๆ–ฐๅˆฐๆ•ฐๆฎๅบ“ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็ฎก็†ๅ‘˜ๆ‰‹ๅŠจ้”ๅฎš็”จๆˆท + * - ่‡ชๅŠจ้”ๅฎš๏ผˆ็”ฑ recordFailedAttempt ่งฆๅ‘๏ผ‰ + * + * @param username ็”จๆˆทๅ + * @param lockDurationMinutes ้”ๅฎšๆ—ถ้•ฟ๏ผˆๅˆ†้’Ÿ๏ผ‰ + */ + void lockAccount(String username, int lockDurationMinutes); + + /** + * ่Žทๅ–่ดฆๆˆทๅ‰ฉไฝ™้”ๅฎšๆ—ถ้—ด + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆท็š„้”ๅฎšๆ—ถ้—ด + * 2. ่ฎก็ฎ—ไธŽๅฝ“ๅ‰ๆ—ถ้—ด็š„ๅทฎๅ€ผ + * 3. ่ฝฌๆขไธบๅˆ†้’Ÿ + * + * @param username ็”จๆˆทๅ + * @return ๅ‰ฉไฝ™้”ๅฎšๆ—ถ้—ด๏ผˆๅˆ†้’Ÿ๏ผ‰๏ผŒๆœช้”ๅฎš่ฟ”ๅ›ž 0 + */ + Long getRemainingLockTimeMinutes(String username); +} diff --git a/src/main/java/com/aisi/template/service/PasswordResetService.java b/src/main/java/com/aisi/template/service/PasswordResetService.java index 706ae6a..04781f8 100644 --- a/src/main/java/com/aisi/template/service/PasswordResetService.java +++ b/src/main/java/com/aisi/template/service/PasswordResetService.java @@ -4,9 +4,51 @@ import com.aisi.template.domain.RestBean; import com.aisi.template.domain.dto.PasswordResetConfirmDto; import com.aisi.template.domain.dto.PasswordResetRequestDto; +/** + * ๅฏ†็ ้‡็ฝฎๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰ๅฏ†็ ้‡็ฝฎ็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅ‘้€้‡็ฝฎ้ชŒ่ฏ็ ๏ผš็”Ÿๆˆ้ชŒ่ฏ็ ๅนถๅ‘้€ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ + * 2. ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็ ๏ผš้ชŒ่ฏ้ชŒ่ฏ็ ๅนถๆ›ดๆ–ฐๅฏ†็  + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ้ชŒ่ฏ็  6 ไฝๆ•ฐๅญ—๏ผŒ10 ๅˆ†้’Ÿๆœ‰ๆ•ˆ + * - ๅŒไธ€้‚ฎ็ฎฑ 60 ็ง’ๅ†…ๅช่ƒฝ่ฏทๆฑ‚ไธ€ๆฌก + * - ้ชŒ่ฏ็ ๆœ€ๅคšๅฐ่ฏ• 5 ๆฌก + * - ๆ–ฐๅฏ†็ ๅฟ…้กป็ฌฆๅˆๅผบๅบฆ่ฆๆฑ‚ + * + * @author Claude + * @since 2024-04-09 + */ public interface PasswordResetService { + /** + * ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ้‚ฎ็ฎฑๆ ผๅผ + * 2. ๆฃ€ๆŸฅ่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด๏ผˆ้˜ฒๆญข้ข‘็น่ฏทๆฑ‚๏ผ‰ + * 3. ็”Ÿๆˆ 6 ไฝ้šๆœบๆ•ฐๅญ—้ชŒ่ฏ็  + * 4. ๅฐ†้ชŒ่ฏ็ ๅ“ˆๅธŒๅŽๅญ˜ๅ‚จๅˆฐ Redis๏ผˆ10ๅˆ†้’Ÿๆœ‰ๆ•ˆ๏ผ‰ + * 5. ๅ‘้€้ชŒ่ฏ็ ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ + * + * @param requestDto ้‡็ฝฎ่ฏทๆฑ‚๏ผˆ้‚ฎ็ฎฑ๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ RestBean sendResetCode(PasswordResetRequestDto requestDto); + /** + * ไฝฟ็”จ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็  + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ้‚ฎ็ฎฑๅ’Œ้ชŒ่ฏ็ ๆ˜ฏๅฆๅŒน้… + * 2. ๆฃ€ๆŸฅ้ชŒ่ฏ็ ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * 3. ๆ ก้ชŒ้ชŒ่ฏ็ ๅฐ่ฏ•ๆฌกๆ•ฐ๏ผˆ่ถ…่ฟ‡ 5 ๆฌกๅˆ™้ชŒ่ฏ็ ๅคฑๆ•ˆ๏ผ‰ + * 4. ๆ ก้ชŒๆ–ฐๅฏ†็ ๅผบๅบฆ + * 5. ๆ›ดๆ–ฐ็”จๆˆทๅฏ†็  + * 6. ๅˆ ้™คๅทฒไฝฟ็”จ็š„้ชŒ่ฏ็  + * + * @param confirmDto ็กฎ่ฎค่ฏทๆฑ‚๏ผˆ้‚ฎ็ฎฑใ€้ชŒ่ฏ็ ใ€ๆ–ฐๅฏ†็ ๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ RestBean resetPassword(PasswordResetConfirmDto confirmDto); } diff --git a/src/main/java/com/aisi/template/service/SysMenuService.java b/src/main/java/com/aisi/template/service/SysMenuService.java new file mode 100644 index 0000000..ee05ab4 --- /dev/null +++ b/src/main/java/com/aisi/template/service/SysMenuService.java @@ -0,0 +1,134 @@ +package com.aisi.template.service; + +import com.aisi.template.domain.dto.MenuDto; +import com.aisi.template.domain.vo.MenuVo; + +import java.util.List; + +/** + * ็ณป็ปŸ่œๅ•ๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰่œๅ•็›ธๅ…ณ็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่œๅ•็ฎก็†๏ผšๅˆ›ๅปบใ€ๆ›ดๆ–ฐใ€ๅˆ ้™คใ€ๆŸฅ่ฏข่œๅ• + * 2. ่œๅ•ๆ ‘ๆž„ๅปบ๏ผšๆž„ๅปบ่œๅ•็š„ๅฑ‚็บง็ป“ๆž„ + * 3. ็”จๆˆท่œๅ•๏ผšๆ นๆฎ็”จๆˆท่ง’่‰ฒ่Žทๅ–ๅฏ่ฎฟ้—ฎ็š„่œๅ• + * + * ่œๅ•็ฑปๅž‹๏ผš + * - DIRECTORY๏ผš็›ฎๅฝ•๏ผˆไป…็”จไบŽๅˆ†็ป„๏ผŒไธๅฏนๅบ”ๅ…ทไฝ“้กต้ข๏ผ‰ + * - MENU๏ผš่œๅ•้กน๏ผˆๅฏนๅบ”ๅ…ทไฝ“้กต้ข๏ผ‰ + * - BUTTON๏ผšๆŒ‰้’ฎ๏ผˆ้กต้ขๅ†…็š„ๆ“ไฝœๆŒ‰้’ฎ๏ผ‰ + * + * ่ฎพ่ฎก่ฏดๆ˜Ž๏ผš + * - ่œๅ•้‡‡็”จๆ ‘ๅฝข็ป“ๆž„๏ผˆ้€š่ฟ‡ parentId ่‡ชๅ…ณ่”ๅฎž็Žฐ๏ผ‰ + * - ๅˆ ้™ค็ˆถ่œๅ•ๆ—ถไผš็บง่”ๅˆ ้™คๅญ่œๅ• + * - ่œๅ•ไธŽ่ง’่‰ฒๆ˜ฏๅคšๅฏนๅคšๅ…ณ็ณป + * - ็”จๆˆท้€š่ฟ‡่ง’่‰ฒ่Žทๅพ—่œๅ•่ฎฟ้—ฎๆƒ้™ + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysMenuService { + + /** + * ๅˆ›ๅปบ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ็ˆถ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ๏ผˆๅฆ‚ๆžœๆ˜ฏๅญ่œๅ•๏ผ‰ + * 2. ๆ ก้ชŒ่œๅ•่ทฏๅพ„ๆ˜ฏๅฆ้‡ๅค + * 3. ๅˆ›ๅปบ่œๅ•ๅฎžไฝ“ + * 4. ่ฎก็ฎ—่œๅ•ๅฑ‚็บงๅ’ŒๆŽ’ๅบ + * 5. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param menuDto ่œๅ•ไฟกๆฏ + * @return ๅˆ›ๅปบ็š„่œๅ•่ง†ๅ›พๅฏน่ฑก + */ + MenuVo create(MenuDto menuDto); + + /** + * ๆ›ดๆ–ฐ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๅฐ†่œๅ•่ฎพ็ฝฎไธบ่‡ชๅทฑ็š„ๅญ่œๅ•๏ผˆๅพช็Žฏๅผ•็”จ๏ผ‰ + * 3. ๆ›ดๆ–ฐ่œๅ•ไฟกๆฏ + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param id ่œๅ•ID + * @param menuDto ่œๅ•ไฟกๆฏ + * @return ๆ›ดๆ–ฐๅŽ็š„่œๅ•่ง†ๅ›พๅฏน่ฑก + */ + MenuVo update(Long id, MenuDto menuDto); + + /** + * ๅˆ ้™ค่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰ๅญ่œๅ• + * 3. ๅˆ ้™ค่œๅ•ไธŽ่ง’่‰ฒ็š„ๅ…ณ่”ๅ…ณ็ณป + * 4. ้€’ๅฝ’ๅˆ ้™คๆ‰€ๆœ‰ๅญ่œๅ• + * 5. ๅˆ ้™คๅฝ“ๅ‰่œๅ• + * + * ๆณจๆ„๏ผš + * - ๅˆ ้™ค็ˆถ่œๅ•ไผš็บง่”ๅˆ ้™คๆ‰€ๆœ‰ๅญ่œๅ• + * - ๅˆ ้™คๆ“ไฝœ้œ€่ฆ่ฐจๆ…Ž๏ผŒๅปบ่ฎฎไฝฟ็”จ่ฝฏๅˆ ้™ค + * + * @param id ่œๅ•ID + */ + void delete(Long id); + + /** + * ๆ นๆฎID่Žทๅ–่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่œๅ• + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param id ่œๅ•ID + * @return ่œๅ•่ง†ๅ›พๅฏน่ฑก + */ + MenuVo getById(Long id); + + /** + * ่Žทๅ–่œๅ•ๆ ‘ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่œๅ• + * 2. ๆŒ‰็ˆถIDๅˆ†็ป„ + * 3. ้€’ๅฝ’ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„ + * 4. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * ๆ ‘ๅฝข็ป“ๆž„๏ผš + * - ๆ น่Š‚็‚น๏ผˆparentId = null๏ผ‰ + * - ๅญ่Š‚็‚น 1 + * - ๅญ่Š‚็‚น 1.1 + * - ๅญ่Š‚็‚น 2 + * + * @return ่œๅ•ๆ ‘ๅˆ—่กจ + */ + List getMenuTree(); + + /** + * ่Žทๅ–็”จๆˆท่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆท็š„ๆ‰€ๆœ‰่ง’่‰ฒ + * 2. ๆŸฅ่ฏข่ง’่‰ฒๅ…ณ่”็š„ๆ‰€ๆœ‰่œๅ• + * 3. ๅŽป้‡ๅนถๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„ + * 4. ๅช่ฟ”ๅ›žๆœ‰ๆƒ้™่ฎฟ้—ฎ็š„่œๅ• + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅ‰็ซฏๅฏผ่ˆช่œๅ•ๆธฒๆŸ“ + * - ็”จๆˆทๆƒ้™ๅˆคๆ–ญ + * + * @param userId ็”จๆˆทID + * @return ็”จๆˆทๅฏ่ฎฟ้—ฎ็š„่œๅ•ๆ ‘ + */ + List getUserMenus(Long userId); + + /** + * ๆ นๆฎ็ˆถID่Žทๅ–ๅญ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆŒ‡ๅฎš็ˆถIDไธ‹็š„ๆ‰€ๆœ‰ๅญ่œๅ• + * 2. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * + * @param parentId ็ˆถ่œๅ•ID๏ผˆnull ่กจ็คบๆŸฅ่ฏขๆ น่œๅ•๏ผ‰ + * @return ๅญ่œๅ•ๅˆ—่กจ + */ + List getByParentId(Long parentId); +} diff --git a/src/main/java/com/aisi/template/service/SysPermissionService.java b/src/main/java/com/aisi/template/service/SysPermissionService.java new file mode 100644 index 0000000..0b43393 --- /dev/null +++ b/src/main/java/com/aisi/template/service/SysPermissionService.java @@ -0,0 +1,83 @@ +package com.aisi.template.service; + +import com.aisi.template.domain.vo.PermissionVo; + +import java.util.List; + +/** + * ็ณป็ปŸๆƒ้™ๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰ๆƒ้™็›ธๅ…ณ็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๆƒ้™ๆŸฅ่ฏข๏ผš่Žทๅ–ๆ‰€ๆœ‰ๆƒ้™ใ€ๆŒ‰ๆกไปถๆŸฅ่ฏข + * 2. ๆƒ้™็ฎก็†๏ผšๆƒ้™้€šๅธธ็”ฑ็ณป็ปŸๅˆๅง‹ๅŒ–๏ผŒไธๆ”ฏๆŒๅŠจๆ€ๅˆ›ๅปบ/ๅˆ ้™ค + * + * ๆƒ้™ๆ ผๅผ๏ผš + * - ่ต„ๆบ:ๆ“ไฝœ ๆ ผๅผ๏ผŒๅฆ‚ user:read, user:write + * - ่ต„ๆบ๏ผš็ณป็ปŸไธญ็š„ๅฎžไฝ“๏ผŒๅฆ‚ user, role, permission, menu + * - ๆ“ไฝœ๏ผšread๏ผˆ่ฏปๅ–๏ผ‰ใ€write๏ผˆๅ†™ๅ…ฅ๏ผ‰ใ€delete๏ผˆๅˆ ้™ค๏ผ‰ + * + * ่ฎพ่ฎก่ฏดๆ˜Ž๏ผš + * - ๆƒ้™ๆ˜ฏ็ณป็ปŸไธญ้ข„ๅฎšไน‰็š„๏ผŒ้€š่ฟ‡ๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌๅˆๅง‹ๅŒ– + * - ่ง’่‰ฒไธŽๆƒ้™ๆ˜ฏๅคšๅฏนๅคšๅ…ณ็ณป + * - ๆƒ้™้€š่ฟ‡่ง’่‰ฒๅˆ†้…็ป™็”จๆˆท + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysPermissionService { + + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰ๆƒ้™ + * 2. ๆŒ‰่ต„ๆบๅˆ†็ป„ๆŽ’ๅบ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๆƒ้™ๅˆ†้…็•Œ้ข + * - ๆƒ้™ๅˆ—่กจๅฑ•็คบ + * + * @return ๆ‰€ๆœ‰ๆƒ้™่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + */ + List getAllPermissions(); + + /** + * ๆ นๆฎID่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆƒ้™ + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param id ๆƒ้™ID + * @return ๆƒ้™่ง†ๅ›พๅฏน่ฑก + */ + PermissionVo getById(Long id); + + /** + * ๆ นๆฎ่ต„ๆบ่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŒ‰่ต„ๆบๅ็งฐๆŸฅ่ฏขๆƒ้™ + * 2. ่ฟ”ๅ›ž่ฏฅ่ต„ๆบ็š„ๆ‰€ๆœ‰ๆ“ไฝœๆƒ้™ + * + * ็คบไพ‹๏ผš + * - resource="user" ่ฟ”ๅ›ž user:read, user:write, user:delete + * + * @param resource ่ต„ๆบๅ็งฐ + * @return ่ฏฅ่ต„ๆบ็š„ๆ‰€ๆœ‰ๆƒ้™ + */ + List getByResource(String resource); + + /** + * ๆ นๆฎๆ“ไฝœ็ฑปๅž‹่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŒ‰ๆ“ไฝœ็ฑปๅž‹ๆŸฅ่ฏขๆƒ้™ + * 2. ่ฟ”ๅ›žๆ‹ฅๆœ‰่ฏฅๆ“ไฝœ็š„ๆ‰€ๆœ‰่ต„ๆบๆƒ้™ + * + * ็คบไพ‹๏ผš + * - action="read" ่ฟ”ๅ›ž user:read, role:read, menu:read ็ญ‰ + * + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆread, write, delete๏ผ‰ + * @return ๆ‹ฅๆœ‰่ฏฅๆ“ไฝœ็š„ๆ‰€ๆœ‰ๆƒ้™ + */ + List getByAction(String action); +} diff --git a/src/main/java/com/aisi/template/service/SysRoleService.java b/src/main/java/com/aisi/template/service/SysRoleService.java new file mode 100644 index 0000000..bd088b0 --- /dev/null +++ b/src/main/java/com/aisi/template/service/SysRoleService.java @@ -0,0 +1,139 @@ +package com.aisi.template.service; + +import com.aisi.template.domain.dto.RoleDto; +import com.aisi.template.domain.dto.RoleQueryDto; +import com.aisi.template.domain.dto.PageResult; +import com.aisi.template.domain.vo.RoleVo; + +import java.util.List; + +/** + * ็ณป็ปŸ่ง’่‰ฒๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰่ง’่‰ฒ็›ธๅ…ณ็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่ง’่‰ฒ็ฎก็†๏ผšๅˆ›ๅปบใ€ๆ›ดๆ–ฐใ€ๅˆ ้™คใ€ๆŸฅ่ฏข่ง’่‰ฒ + * 2. ๆƒ้™ๅˆ†้…๏ผšไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ + * 3. ๆƒ้™ๆŸฅ่ฏข๏ผš่Žทๅ–่ง’่‰ฒ็š„ๆƒ้™ๅˆ—่กจ + * + * ่ฎพ่ฎก่ฏดๆ˜Ž๏ผš + * - ่ง’่‰ฒไธŽๆƒ้™ๆ˜ฏๅคšๅฏนๅคšๅ…ณ็ณป + * - ๅˆ ้™ค่ง’่‰ฒๅ‰้œ€่ฆๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰็”จๆˆทๅ…ณ่” + * - ่ถ…็บง็ฎก็†ๅ‘˜่ง’่‰ฒไธๅฏๅˆ ้™ค + * + * @author Claude + * @since 2024-04-09 + */ +public interface SysRoleService { + + /** + * ๅˆ›ๅปบ่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ่ง’่‰ฒ็ผ–็ ๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + * 2. ๅˆ›ๅปบ่ง’่‰ฒๅฎžไฝ“ + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * 4. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param roleDto ่ง’่‰ฒไฟกๆฏ๏ผˆ่ง’่‰ฒๅใ€็ผ–็ ใ€ๆ่ฟฐ๏ผ‰ + * @return ๅˆ›ๅปบ็š„่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + */ + RoleVo create(RoleDto roleDto); + + /** + * ๆ›ดๆ–ฐ่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ–ฐ่ง’่‰ฒ็ผ–็ ๆ˜ฏๅฆไธŽๅ…ถไป–่ง’่‰ฒๅ†ฒ็ช + * 3. ๆ›ดๆ–ฐ่ง’่‰ฒไฟกๆฏ + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param id ่ง’่‰ฒID + * @param roleDto ่ง’่‰ฒไฟกๆฏ + * @return ๆ›ดๆ–ฐๅŽ็š„่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + */ + RoleVo update(Long id, RoleDto roleDto); + + /** + * ๅˆ ้™ค่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰็”จๆˆทๅ…ณ่”ๆญค่ง’่‰ฒ + * 3. ๆฃ€ๆŸฅๆ˜ฏๅฆไธบ่ถ…็บง็ฎก็†ๅ‘˜่ง’่‰ฒ๏ผˆ็ฆๆญขๅˆ ้™ค๏ผ‰ + * 4. ๅˆ ้™ค่ง’่‰ฒไธŽๆƒ้™็š„ๅ…ณ่”ๅ…ณ็ณป + * 5. ๅˆ ้™ค่ง’่‰ฒ + * + * @param id ่ง’่‰ฒID + */ + void delete(Long id); + + /** + * ๆ นๆฎID่Žทๅ–่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒ + * 2. ๅŠ ่ฝฝๅ…ณ่”็š„ๆƒ้™ๅˆ—่กจ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param id ่ง’่‰ฒID + * @return ่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + */ + RoleVo getById(Long id); + + /** + * ่Žทๅ–ๆ‰€ๆœ‰่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่ง’่‰ฒ + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ่ง’่‰ฒไธ‹ๆ‹‰้€‰ๆ‹ฉๆก† + * - ่ง’่‰ฒๅˆ—่กจๅฑ•็คบ + * + * @return ๆ‰€ๆœ‰่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + */ + List getAllRoles(); + + /** + * ๅˆ†้กตๆŸฅ่ฏข่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบๅˆ†้กตๅ‚ๆ•ฐ + * 2. ๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏขๆกไปถ๏ผˆๅ…ณ้”ฎ่ฏใ€็Šถๆ€๏ผ‰ + * 3. ๆ‰ง่กŒๅˆ†้กตๆŸฅ่ฏข + * 4. ่ฝฌๆข็ป“ๆžœไธบ่ง†ๅ›พๅฏน่ฑก + * + * @param queryDto ๆŸฅ่ฏขๆกไปถ + * @param page ้กต็ ๏ผˆไปŽ1ๅผ€ๅง‹๏ผ‰ + * @param size ๆฏ้กตๅคงๅฐ + * @return ๅˆ†้กต่ง’่‰ฒๅˆ—่กจ + */ + PageResult queryRoles(RoleQueryDto queryDto, int page, int size); + + /** + * ไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ้ชŒ่ฏๆ‰€ๆœ‰ๆƒ้™IDๆ˜ฏๅฆๅญ˜ๅœจ + * 3. ๆธ…็ฉบ่ง’่‰ฒๅŽŸๆœ‰ๆƒ้™ + * 4. ๆทปๅŠ ๆ–ฐๆƒ้™ + * 5. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * ๆณจๆ„๏ผš + * - ๆญคๆ“ไฝœไผšๅฎŒๅ…จๆ›ฟๆข่ง’่‰ฒ็š„ๆƒ้™ๅˆ—่กจ + * - ๆƒ้™ๅ˜ๆ›ดๅŽ็”จๆˆท้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ•ๆ‰่ƒฝ็”Ÿๆ•ˆ + * + * @param roleId ่ง’่‰ฒID + * @param permissionIds ๆƒ้™IDๅˆ—่กจ + */ + void assignPermissions(Long roleId, List permissionIds); + + /** + * ่Žทๅ–่ง’่‰ฒ็š„ๆƒ้™IDๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒ + * 2. ๅŠ ่ฝฝๅ…ณ่”็š„ๆƒ้™ + * 3. ๆๅ–ๆƒ้™IDๅˆ—่กจ + * + * @param roleId ่ง’่‰ฒID + * @return ๆƒ้™IDๅˆ—่กจ + */ + List getRolePermissionIds(Long roleId); +} diff --git a/src/main/java/com/aisi/template/service/TokenService.java b/src/main/java/com/aisi/template/service/TokenService.java new file mode 100644 index 0000000..101a9da --- /dev/null +++ b/src/main/java/com/aisi/template/service/TokenService.java @@ -0,0 +1,161 @@ +package com.aisi.template.service; + +import com.aisi.template.domain.dto.RefreshTokenDto; +import com.aisi.template.domain.vo.LoginResponseVo; + +/** + * Token ๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰ Refresh Token ๅ’Œ้ป‘ๅๅ•็ฎก็†็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. Refresh Token ็ฎก็†๏ผšๅˆ›ๅปบใ€ๅˆทๆ–ฐใ€ๆ’ค้”€ Refresh Token + * 2. Token ้ป‘ๅๅ•๏ผš็ฎก็†ๅทฒๆ’ค้”€็š„ JWT Token + * 3. ๆธ…็†ไปปๅŠก๏ผšๆธ…็†่ฟ‡ๆœŸ็š„ Token + * + * Token ่ฏดๆ˜Ž๏ผš + * - Access Token๏ผš็ŸญๆœŸๆœ‰ๆ•ˆ๏ผˆ้ป˜่ฎค 1 ๅฐๆ—ถ๏ผ‰๏ผŒ็”จไบŽ API ่ฎฟ้—ฎ + * - Refresh Token๏ผš้•ฟๆœŸๆœ‰ๆ•ˆ๏ผˆ้ป˜่ฎค 7 ๅคฉ๏ผ‰๏ผŒ็”จไบŽๅˆทๆ–ฐ Access Token + * - Token ้ป‘ๅๅ•๏ผš็”จไบŽๅฎž็Žฐ็™ปๅ‡บๅŠŸ่ƒฝ๏ผŒๅฐ†ๅทฒๆ’ค้”€็š„ Token ๅŠ ๅ…ฅ้ป‘ๅๅ• + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - Refresh Token ๅญ˜ๅ‚จๅœจๆ•ฐๆฎๅบ“๏ผŒๆ”ฏๆŒๆ’ค้”€ + * - Refresh Token ๆฏๆฌกไฝฟ็”จๅŽ็”Ÿๆˆๆ–ฐ็š„๏ผˆToken ่ฝฎๆข๏ผ‰ + * - ็™ปๅ‡บๆ—ถๅฐ† Access Token ๅŠ ๅ…ฅ Redis ้ป‘ๅๅ• + * - ๅฎšๆ—ถๆธ…็†่ฟ‡ๆœŸ็š„ Token ๅ’Œ้ป‘ๅๅ•่ฎฐๅฝ• + * + * @author Claude + * @since 2024-04-09 + */ +public interface TokenService { + + /** + * ๅˆ›ๅปบ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆ้šๆœบ็š„ Token ๅญ—็ฌฆไธฒ + * 2. ่ฎก็ฎ— Token ็š„ๅ“ˆๅธŒๅ€ผ๏ผˆSHA-256๏ผ‰ + * 3. ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆๅฝ“ๅ‰ๆ—ถ้—ด + 7 ๅคฉ๏ผ‰ + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * 5. ่ฟ”ๅ›žๅŽŸๅง‹ Token๏ผˆไธๅญ˜ๅ‚จๅŽŸๅง‹ๅ€ผ๏ผ‰ + * + * ๅฎ‰ๅ…จ่ฏดๆ˜Ž๏ผš + * - ๆ•ฐๆฎๅบ“ๅชๅญ˜ๅ‚จ Token ็š„ๅ“ˆๅธŒๅ€ผ๏ผŒ้˜ฒๆญขๆณ„้œฒ + * - Token ๆ ผๅผ๏ผšUUID ้šๆœบๅญ—็ฌฆไธฒ + * + * @param userId ็”จๆˆทID + * @param deviceInfo ่ฎพๅค‡ไฟกๆฏ๏ผˆๅฏ้€‰๏ผ‰ + * @param ipAddress IP ๅœฐๅ€๏ผˆๅฏ้€‰๏ผ‰ + * @return Refresh Token ๅŽŸๅง‹ๅ€ผ + */ + String createRefreshToken(Long userId, String deviceInfo, String ipAddress); + + /** + * ๅˆทๆ–ฐ Access Token + * ๆญฅ้ชค๏ผš + * 1. ่ฎก็ฎ— Refresh Token ็š„ๅ“ˆๅธŒๅ€ผ + * 2. ๆŸฅ่ฏขๆ•ฐๆฎๅบ“ไธญ็š„ Token ่ฎฐๅฝ• + * 3. ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๅญ˜ๅœจใ€ๆ˜ฏๅฆ่ฟ‡ๆœŸใ€ๆ˜ฏๅฆ่ขซๆ’ค้”€ + * 4. ้ชŒ่ฏ Token ๆ˜ฏๅฆๅฑžไบŽๅฝ“ๅ‰็”จๆˆท + * 5. ็”Ÿๆˆๆ–ฐ็š„ Access Token + * 6. ็”Ÿๆˆๆ–ฐ็š„ Refresh Token๏ผˆToken ่ฝฎๆข๏ผ‰ + * 7. ๆ’ค้”€ๆ—ง็š„ Refresh Token + * + * Token ่ฝฎๆข๏ผš + * - ๆฏๆฌกๅˆทๆ–ฐๆ—ถ็”Ÿๆˆๆ–ฐ็š„ Refresh Token + * - ๆ—ง็š„ Refresh Token ็ซ‹ๅณๅคฑๆ•ˆ + * - ้˜ฒๆญข Token ่ขซ้‡ๅคไฝฟ็”จ + * + * @param refreshToken Refresh Token ๅŽŸๅง‹ๅ€ผ + * @return ๆ–ฐ็š„็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซๆ–ฐ็š„ Access Token ๅ’Œ Refresh Token๏ผ‰ + */ + LoginResponseVo refreshToken(String refreshToken); + + /** + * ๆ’ค้”€ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ่ฎก็ฎ— Token ็š„ๅ“ˆๅธŒๅ€ผ + * 2. ๆŸฅ่ฏขๆ•ฐๆฎๅบ“ไธญ็š„ Token ่ฎฐๅฝ• + * 3. ๅฐ†ๆ’ค้”€็Šถๆ€่ฎพ็ฝฎไธบ true + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทไธปๅŠจ็™ปๅ‡บ + * - Token ๅˆทๆ–ฐๆ—ถๆ’ค้”€ๆ—ง Token + * - ็ฎก็†ๅ‘˜ๅผบๅˆถ็”จๆˆทไธ‹็บฟ + * + * @param tokenHash Refresh Token ็š„ๅ“ˆๅธŒๅ€ผ + */ + void revokeRefreshToken(String tokenHash); + + /** + * ๆ’ค้”€็”จๆˆท็š„ๆ‰€ๆœ‰ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆท็š„ๆ‰€ๆœ‰ๆœ‰ๆ•ˆ Refresh Token + * 2. ๆ‰น้‡่ฎพ็ฝฎไธบๆ’ค้”€็Šถๆ€ + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ไฟฎๆ”นๅฏ†็ ๅŽๆ’ค้”€ๆ‰€ๆœ‰่ฎพๅค‡ + * - ็ฎก็†ๅ‘˜ๅผบๅˆถ็”จๆˆทไธ‹็บฟ + * - ็”จๆˆท้‡็ฝฎๆ‰€ๆœ‰็™ปๅฝ•ไผš่ฏ + * + * @param userId ็”จๆˆทID + */ + void revokeAllUserTokens(Long userId); + + /** + * ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ JWT Token ไธญๆๅ– jti๏ผˆJWT ID๏ผ‰ + * 2. ๅœจ Redis ไธญๆŸฅ่ฏข้ป‘ๅๅ• + * 3. ่ฟ”ๅ›žๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param jti JWT ID + * @return true ่กจ็คบ Token ๅทฒ่ขซๆ’ค้”€๏ผˆๅœจ้ป‘ๅๅ•ไธญ๏ผ‰ + */ + boolean isTokenBlacklisted(String jti); + + /** + * ๅฐ† Token ๅŠ ๅ…ฅ้ป‘ๅๅ• + * ๆญฅ้ชค๏ผš + * 1. ไปŽ JWT Token ไธญๆๅ– jti ๅ’Œ่ฟ‡ๆœŸๆ—ถ้—ด + * 2. ่ฎก็ฎ—ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆ—ถ้—ด + * 3. ๅœจ Redis ไธญ่ฎพ็ฝฎ้ป‘ๅๅ•่ฎฐๅฝ•๏ผˆๅธฆ่ฟ‡ๆœŸๆ—ถ้—ด๏ผ‰ + * + * ไธบไป€ไนˆ้œ€่ฆ้ป‘ๅๅ•๏ผš + * - JWT Token ๆ˜ฏๆ— ็Šถๆ€็š„๏ผŒไธ€ๆ—ฆ็ญพๅ‘ๆ— ๆณ•ไธปๅŠจๆ’ค้”€ + * - ้€š่ฟ‡้ป‘ๅๅ•ๆœบๅˆถๅฎž็Žฐ็™ปๅ‡บๅŠŸ่ƒฝ + * - ้ป‘ๅๅ•่ฎฐๅฝ•็š„่ฟ‡ๆœŸๆ—ถ้—ดไธŽ Token ไธ€่‡ด + * + * @param jti JWT ID + * @param expirationSeconds ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + */ + void addTokenToBlacklist(String jti, long expirationSeconds); + + /** + * ๆธ…็†่ฟ‡ๆœŸ็š„ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่ฟ‡ๆœŸๆ—ถ้—ดๅฐไบŽๅฝ“ๅ‰ๆ—ถ้—ด็š„ Token + * 2. ๆ‰น้‡ๅˆ ้™ค + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅฎšๆ—ถไปปๅŠก๏ผˆๆฏๅคฉๅ‡Œๆ™จๆ‰ง่กŒ๏ผ‰ + * - ๆ‰‹ๅŠจ่งฆๅ‘ๆธ…็† + * + * ๆณจๆ„๏ผš + * - ๅทฒๆ’ค้”€็š„ Token ไนŸ้œ€่ฆๆธ…็† + * - ๅปบ่ฎฎๅฎšๆœŸๆ‰ง่กŒ๏ผŒ้˜ฒๆญขๆ•ฐๆฎๅ †็งฏ + */ + void cleanupExpiredTokens(); + + /** + * ๆธ…็†่ฟ‡ๆœŸ็š„้ป‘ๅๅ•่ฎฐๅฝ• + * ๆญฅ้ชค๏ผš + * 1. ๆ‰ซๆ Redis ไธญ็š„้ป‘ๅๅ• + * 2. ๅˆ ้™คๅทฒ่ฟ‡ๆœŸ็š„่ฎฐๅฝ• + * + * ๆณจๆ„๏ผš + * - Redis ็š„ key ไผš่‡ชๅŠจ่ฟ‡ๆœŸ๏ผŒๆญคๆ–นๆณ•็”จไบŽๆ‰‹ๅŠจๆธ…็† + * - ไธ€่ˆฌไธ้œ€่ฆ่ฐƒ็”จ๏ผŒ้™ค้ž้œ€่ฆ็ซ‹ๅณ้‡Šๆ”พๅ†…ๅญ˜ + */ + void cleanupExpiredBlacklistEntries(); +} diff --git a/src/main/java/com/aisi/template/service/UserService.java b/src/main/java/com/aisi/template/service/UserService.java index d01907e..f0b06cb 100644 --- a/src/main/java/com/aisi/template/service/UserService.java +++ b/src/main/java/com/aisi/template/service/UserService.java @@ -6,19 +6,101 @@ import com.aisi.template.domain.dto.UserDto; import com.aisi.template.domain.dto.UserQueryDto; import com.aisi.template.domain.dto.UserRoleUpdateDto; import com.aisi.template.domain.dto.UserStatusUpdateDto; +import com.aisi.template.domain.vo.LoginResponseVo; import com.aisi.template.domain.vo.UserVo; +/** + * ็”จๆˆทๆœๅŠกๆŽฅๅฃ + * ๅฎšไน‰็”จๆˆท็›ธๅ…ณ็š„ไธšๅŠกๆ“ไฝœ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ็”จๆˆท่ฎค่ฏ๏ผšๆณจๅ†Œใ€็™ปๅฝ• + * 2. ็”จๆˆทไฟกๆฏ๏ผš่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * 3. ็”จๆˆท็ฎก็†๏ผšๅˆ†้กตๆŸฅ่ฏขใ€็Šถๆ€ๆ›ดๆ–ฐใ€่ง’่‰ฒๅˆ†้… + * + * @author Claude + * @since 2024-04-09 + */ public interface UserService { + /** + * ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ SecurityContext ่Žทๅ–ๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท + * 2. ๆŸฅ่ฏข็”จๆˆท่ฏฆ็ป†ไฟกๆฏ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @return ็”จๆˆทไฟกๆฏ่ง†ๅ›พๅฏน่ฑก + */ RestBean getUserInfo(); - RestBean register(UserDto userDto); + /** + * ็”จๆˆทๆณจๅ†Œ + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ็”จๆˆทๅใ€้‚ฎ็ฎฑๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + * 2. ๆ ก้ชŒๅฏ†็ ๅผบๅบฆ + * 3. ๅˆ›ๅปบ็”จๆˆทๅนถๅˆ†้…้ป˜่ฎค่ง’่‰ฒ + * 4. ็”Ÿๆˆ Access Token ๅ’Œ Refresh Token + * 5. ๅ‘้€ๆณจๅ†Œๆถˆๆฏๅˆฐ MQ + * + * @param userDto ็”จๆˆทๆณจๅ†Œไฟกๆฏ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซ Token ๅ’Œ็”จๆˆทไฟกๆฏ๏ผ‰ + */ + RestBean register(UserDto userDto); - RestBean login(UserDto userDto); + /** + * ็”จๆˆท็™ปๅฝ• + * ๆญฅ้ชค๏ผš + * 1. ้ชŒ่ฏ็”จๆˆทๅๅ’Œๅฏ†็  + * 2. ๆฃ€ๆŸฅ่ดฆๆˆท็Šถๆ€๏ผˆๆ˜ฏๅฆ่ขซ็ฆ็”จใ€้”ๅฎš๏ผ‰ + * 3. ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ๏ผˆ่ถ…่ฟ‡้˜ˆๅ€ผๅˆ™้”ๅฎš๏ผ‰ + * 4. ็™ปๅฝ•ๆˆๅŠŸๅŽ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + * 5. ็”Ÿๆˆ Access Token ๅ’Œ Refresh Token + * 6. ๅ‘้€็™ปๅฝ•ๆถˆๆฏๅˆฐ MQ + * + * @param userDto ็™ปๅฝ•ไฟกๆฏ๏ผˆ็”จๆˆทๅใ€ๅฏ†็ ๏ผ‰ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซ Token ๅ’Œ็”จๆˆทไฟกๆฏ๏ผ‰ + */ + RestBean login(UserDto userDto); + /** + * ๅˆ†้กตๆŸฅ่ฏข็”จๆˆทๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏขๆกไปถ + * 2. ๆ”ฏๆŒๆŒ‰็”จๆˆทๅ/้‚ฎ็ฎฑๆจก็ณŠๆœ็ดข + * 3. ๆ”ฏๆŒๆŒ‰็Šถๆ€็ญ›้€‰ + * 4. ๅˆ†้กต่ฟ”ๅ›ž็ป“ๆžœ + * + * @param queryDto ๆŸฅ่ฏขๆกไปถ + * @return ๅˆ†้กต็”จๆˆทๅˆ—่กจ + */ RestBean> getUserList(UserQueryDto queryDto); + /** + * ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ็ฆๆญขไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„็Šถๆ€ + * 3. ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ + * + * @param userId ็”จๆˆทID + * @param updateDto ็Šถๆ€ๆ›ดๆ–ฐ่ฏทๆฑ‚ + * @return ๆ›ดๆ–ฐๅŽ็š„็”จๆˆทไฟกๆฏ + */ RestBean updateUserStatus(Long userId, UserStatusUpdateDto updateDto); + /** + * ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ็ฆๆญขไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ + * 3. ้ชŒ่ฏ่ง’่‰ฒIDๆ˜ฏๅฆๅญ˜ๅœจ + * 4. ๆธ…็ฉบ็”จๆˆทๅŽŸๆœ‰่ง’่‰ฒ + * 5. ๆทปๅŠ ๆ–ฐ่ง’่‰ฒ + * + * @param userId ็”จๆˆทID + * @param updateDto ่ง’่‰ฒๆ›ดๆ–ฐ่ฏทๆฑ‚ + * @return ๆ›ดๆ–ฐๅŽ็š„็”จๆˆทไฟกๆฏ + */ RestBean updateUserRole(Long userId, UserRoleUpdateDto updateDto); } diff --git a/src/main/java/com/aisi/template/service/impl/AuditLogServiceImpl.java b/src/main/java/com/aisi/template/service/impl/AuditLogServiceImpl.java new file mode 100644 index 0000000..a3b25ed --- /dev/null +++ b/src/main/java/com/aisi/template/service/impl/AuditLogServiceImpl.java @@ -0,0 +1,133 @@ +package com.aisi.template.service.impl; + +import com.aisi.template.domain.entity.SysAuditLog; +import com.aisi.template.repository.SysAuditLogRepository; +import com.aisi.template.service.AuditLogService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * ๅฎก่ฎกๆ—ฅๅฟ—ๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃๅฎก่ฎกๆ—ฅๅฟ—็š„่ฎฐๅฝ•ๅ’Œๆธ…็† + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—๏ผš่ฎฐๅฝ•็”จๆˆทๆ“ไฝœ + * 2. ๆธ…็†ๆ—งๆ—ฅๅฟ—๏ผšๅฎšๆœŸๅˆ ้™ค่ฟ‡ๆœŸๆ—ฅๅฟ— + * + * ๆ€ง่ƒฝไผ˜ๅŒ–๏ผš + * - ไฝฟ็”จ @Async ๆณจ่งฃๅฎž็Žฐๅผ‚ๆญฅ่ฎฐๅฝ•๏ผŒไธ้˜ปๅกžไธปๆต็จ‹ + * - ไฝฟ็”จ @Transactional ไฟ่ฏๆ•ฐๆฎไธ€่‡ดๆ€ง + * - ๅผ‚ๅธธๆ•่Žท็กฎไฟๆ—ฅๅฟ—่ฎฐๅฝ•ๅคฑ่ดฅไธๅฝฑๅ“ไธšๅŠก + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AuditLogServiceImpl implements AuditLogService { + + /** + * ๅฎก่ฎกๆ—ฅๅฟ—ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final SysAuditLogRepository auditLogRepository; + + /** + * ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—๏ผˆๅผ‚ๆญฅ๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ไฟๅญ˜ๅฎก่ฎกๆ—ฅๅฟ—ๅˆฐๆ•ฐๆฎๅบ“ + * 2. ๆ•่Žทๅผ‚ๅธธ๏ผŒ็กฎไฟ่ฎฐๅฝ•ๅคฑ่ดฅไธๅฝฑๅ“ไธšๅŠก + * + * @param auditLog ๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“ + */ + @Async + @Override + @Transactional(rollbackFor = Exception.class) + public void createAuditLog(SysAuditLog auditLog) { + try { + auditLogRepository.save(auditLog); + } catch (Exception e) { + log.error("ไฟๅญ˜ๅฎก่ฎกๆ—ฅๅฟ—ๅคฑ่ดฅ", e); + } + } + + /** + * ่ฎฐๅฝ•ๅฎก่ฎกๆ—ฅๅฟ—๏ผˆๅผ‚ๆญฅ๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“ + * 2. ่ฎพ็ฝฎๆ—ฅๅฟ—ไฟกๆฏ + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * 4. ๆ•่Žทๅผ‚ๅธธ๏ผŒ็กฎไฟ่ฎฐๅฝ•ๅคฑ่ดฅไธๅฝฑๅ“ไธšๅŠก + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆLOGIN, LOGOUT, CREATE, UPDATE, DELETE๏ผ‰ + * @param resource ่ต„ๆบ็ฑปๅž‹๏ผˆuser, role, menu๏ผ‰ + * @param resourceId ่ต„ๆบID + * @param description ๆ“ไฝœๆ่ฟฐ + * @param requestMethod ่ฏทๆฑ‚ๆ–นๆณ•๏ผˆGET, POST, PUT, DELETE๏ผ‰ + * @param requestUri ่ฏทๆฑ‚URI + * @param ipAddress IPๅœฐๅ€ + * @param userAgent User-Agent + * @param status ๆ“ไฝœ็Šถๆ€๏ผˆ1=ๆˆๅŠŸ๏ผŒ0=ๅคฑ่ดฅ๏ผ‰ + * @param errorMessage ้”™่ฏฏไฟกๆฏ๏ผˆๅคฑ่ดฅๆ—ถ๏ผ‰ + */ + @Async + @Override + @Transactional(rollbackFor = Exception.class) + public void log(Long userId, String username, String action, String resource, + String resourceId, String description, String requestMethod, + String requestUri, String ipAddress, String userAgent, + Integer status, String errorMessage) { + // 1. ๅˆ›ๅปบๅฎก่ฎกๆ—ฅๅฟ—ๅฎžไฝ“ + SysAuditLog auditLog = new SysAuditLog(); + auditLog.setUserId(userId); + auditLog.setUsername(username); + auditLog.setAction(action); + auditLog.setResource(resource); + auditLog.setResourceId(resourceId); + auditLog.setDescription(description); + auditLog.setRequestMethod(requestMethod); + auditLog.setRequestUri(requestUri); + auditLog.setIpAddress(ipAddress); + auditLog.setUserAgent(userAgent); + auditLog.setStatus(status); + auditLog.setErrorMessage(errorMessage); + + // 2. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + try { + auditLogRepository.save(auditLog); + } catch (Exception e) { + log.error("ไฟๅญ˜ๅฎก่ฎกๆ—ฅๅฟ—ๅคฑ่ดฅ", e); + } + } + + /** + * ๆธ…็†ๆ—งๆ—ฅๅฟ— + * ๆญฅ้ชค๏ผš + * 1. ่ฎก็ฎ—ๆˆชๆญขๆ—ฅๆœŸ๏ผˆๅฝ“ๅ‰ๆ—ถ้—ด - ไฟ็•™ๅคฉๆ•ฐ๏ผ‰ + * 2. ๅˆ ้™คๆˆชๆญขๆ—ฅๆœŸไน‹ๅ‰็š„ๆ—ฅๅฟ— + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅฎšๆ—ถไปปๅŠกๅฎšๆœŸๆธ…็†๏ผˆๅฆ‚ๆฏๅคฉๅ‡Œๆ™จๆ‰ง่กŒ๏ผ‰ + * - ๅปบ่ฎฎไฟ็•™ๆœ€่ฟ‘ 90 ๅคฉ็š„ๆ—ฅๅฟ— + * + * @param daysToKeep ไฟ็•™ๅคฉๆ•ฐ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void cleanupOldLogs(int daysToKeep) { + // 1. ่ฎก็ฎ—ๆˆชๆญขๆ—ฅๆœŸ + LocalDateTime cutoffDate = LocalDateTime.now().minusDays(daysToKeep); + + // 2. ๅˆ ้™คๆ—งๆ—ฅๅฟ— + auditLogRepository.deleteByCreatedAtBefore(cutoffDate); + + log.info("ๅทฒๆธ…็† {} ๅคฉๅ‰็š„ๅฎก่ฎกๆ—ฅๅฟ—", daysToKeep); + } +} diff --git a/src/main/java/com/aisi/template/service/impl/CustomUserDetailsService.java b/src/main/java/com/aisi/template/service/impl/CustomUserDetailsService.java index 5b541db..228c571 100644 --- a/src/main/java/com/aisi/template/service/impl/CustomUserDetailsService.java +++ b/src/main/java/com/aisi/template/service/impl/CustomUserDetailsService.java @@ -1,38 +1,103 @@ package com.aisi.template.service.impl; import com.aisi.template.domain.CustomUserDetails; +import com.aisi.template.domain.entity.SysRole; +import com.aisi.template.domain.entity.SysPermission; import com.aisi.template.domain.entity.User; import com.aisi.template.repository.UserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import java.util.Collection; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +/** + * ่‡ชๅฎšไน‰็”จๆˆท่ฏฆๆƒ…ๆœๅŠกๅฎž็Žฐ็ฑป + * ไธบ Spring Security ๆไพ›็”จๆˆท่ฎค่ฏๅ’ŒๆŽˆๆƒไฟกๆฏ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅŠ ่ฝฝ็”จๆˆทไฟกๆฏ๏ผšๆ นๆฎ็”จๆˆทๅๅŠ ่ฝฝ็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + * 2. ๆž„ๅปบ็”จๆˆทๆƒ้™๏ผšๅˆๅนถ่ง’่‰ฒๆƒ้™ๅ’Œๅ…ทไฝ“ๆƒ้™ + * 3. ๅˆ›ๅปบ่ฎค่ฏๅฏน่ฑก๏ผš่ฟ”ๅ›ž CustomUserDetails ๅฏน่ฑก + * + * ๆƒ้™ๆ ผๅผ๏ผš + * - ่ง’่‰ฒๆƒ้™๏ผšROLE_XXX๏ผˆๅฆ‚ ROLE_USER, ROLE_ADMIN๏ผ‰ + * - ๅ…ทไฝ“ๆƒ้™๏ผšXXX:YYY๏ผˆๅฆ‚ user:create, role:read๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ @Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { + + /** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ private final UserRepository userRepository; + /** + * ๆ นๆฎ็”จๆˆทๅๅŠ ่ฝฝ็”จๆˆท่ฏฆๆƒ… + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ๏ผˆไฝฟ็”จ JOIN FETCH ้ฟๅ…ๆ‡’ๅŠ ่ฝฝ้—ฎ้ข˜๏ผ‰ + * 2. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 3. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆ่ขซ็ฆ็”จ + * 4. ๆž„ๅปบๆƒ้™ๅˆ—่กจ๏ผˆ่ง’่‰ฒ + ๆƒ้™๏ผ‰ + * 5. ๅˆ›ๅปบ CustomUserDetails ๅฏน่ฑก + * + * @param username ็”จๆˆทๅ + * @return ็”จๆˆท่ฏฆๆƒ…ๅฏน่ฑก + * @throws UsernameNotFoundException ๅฝ“็”จๆˆทไธๅญ˜ๅœจๆˆ–่ขซ็ฆ็”จๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = userRepository.findByUsername(username) + // 1. ๆŸฅ่ฏข็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + User user = userRepository.findByUsernameWithRolesAndPermissions(username) .orElseThrow(() -> new UsernameNotFoundException("็”จๆˆทไธๅญ˜ๅœจ: " + username)); - // ๆฃ€ๆŸฅ็”จๆˆท็Šถๆ€ + // 2. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆ่ขซ็ฆ็”จ if (!user.isEnabled()) { throw new UsernameNotFoundException("็”จๆˆทๅทฒ่ขซ็ฆ็”จ: " + username); } + // 3. ่Žทๅ–็”จๆˆท่ง’่‰ฒ + Set roles = user.getRoles(); + Set roleCodes = roles.stream() + .map(SysRole::getRoleCode) + .collect(Collectors.toSet()); + + // 4. ๆž„ๅปบๆƒ้™ๅˆ—่กจ๏ผšๅŒ…ๅซ่ง’่‰ฒๆƒ้™ๅ’Œๅ…ทไฝ“ๆƒ้™ + Collection authorities = roles.stream() + .flatMap(role -> { + // 4.1 ๆทปๅŠ ่ง’่‰ฒๆƒ้™๏ผˆROLE_XXX ๆ ผๅผ๏ผ‰ + List roleAuths = List.of( + new SimpleGrantedAuthority(role.getRoleCode()) + ); + + // 4.2 ๆทปๅŠ ๅ…ทไฝ“ๆƒ้™๏ผˆXXX:YYY ๆ ผๅผ๏ผŒๅฆ‚ user:create, role:read๏ผ‰ + List permAuths = role.getPermissions().stream() + .map(permission -> new SimpleGrantedAuthority(permission.getPermissionCode())) + .collect(Collectors.toList()); + + // 4.3 ๅˆๅนถ่ง’่‰ฒๅ’Œๆƒ้™ + return java.util.stream.Stream.concat(roleAuths.stream(), permAuths.stream()); + }) + .collect(Collectors.toSet()); + + // 5. ๅˆ›ๅปบ CustomUserDetails ๅฏน่ฑก return new CustomUserDetails( user.getId(), user.getUsername(), user.getPassword(), - List.of(() -> user.getRole().getAuthority()), + authorities, user.isEnabled(), - user.getRole().name() + roleCodes ); } } diff --git a/src/main/java/com/aisi/template/service/impl/EmailServiceImpl.java b/src/main/java/com/aisi/template/service/impl/EmailServiceImpl.java index 3d32dfb..4d1a23c 100644 --- a/src/main/java/com/aisi/template/service/impl/EmailServiceImpl.java +++ b/src/main/java/com/aisi/template/service/impl/EmailServiceImpl.java @@ -4,29 +4,80 @@ import com.aisi.template.service.EmailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; +/** + * ้‚ฎไปถๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃๅ‘้€ๅ„็ฑป้‚ฎไปถ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ้‚ฎไปถ + * 2. ๅ‘้€ๆณจๅ†Œๆฌข่ฟŽ้‚ฎไปถ๏ผˆ้ข„็•™๏ผ‰ + * 3. ๅ‘้€้€š็Ÿฅ้‚ฎไปถ๏ผˆ้ข„็•™๏ผ‰ + * + * ้‚ฎไปถ้…็ฝฎ๏ผš + * - ๅ‘ไปถไบบๅœฐๅ€๏ผšไปŽ้…็ฝฎๆ–‡ไปถ่ฏปๅ–๏ผˆspring.mail.username๏ผ‰ + * - ้ชŒ่ฏ็ ๆœ‰ๆ•ˆๆœŸ๏ผšไปŽ้…็ฝฎ่ฏปๅ–๏ผˆapp.password-reset.code-expire-minutes๏ผ‰ + * + * @author Claude + * @since 2024-04-09 + */ @Slf4j @Service @RequiredArgsConstructor public class EmailServiceImpl implements EmailService { + /** + * Java Mail Sender + * Spring ๆไพ›็š„้‚ฎไปถๅ‘้€ๅทฅๅ…ท + */ private final JavaMailSender mailSender; + /** + * ๅ‘ไปถไบบ้‚ฎ็ฎฑๅœฐๅ€ + * ไปŽ้…็ฝฎๆ–‡ไปถ่ฏปๅ– + */ @Value("${spring.mail.username}") private String from; + /** + * ้ชŒ่ฏ็ ๆœ‰ๆ•ˆๆœŸ๏ผˆๅˆ†้’Ÿ๏ผ‰ + * ไปŽ้…็ฝฎ่ฏปๅ–๏ผŒ็”จไบŽๅœจ้‚ฎไปถไธญๆ็คบ็”จๆˆท + */ @Value("${app.password-reset.code-expire-minutes:10}") private Integer expireMinutes; + /** + * ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ้‚ฎไปถ + * ๆญฅ้ชค๏ผš + * 1. ๅˆ›ๅปบ็ฎ€ๅ•้‚ฎไปถๆถˆๆฏ + * 2. ่ฎพ็ฝฎๅ‘ไปถไบบ + * 3. ่ฎพ็ฝฎๆ”ถไปถไบบ + * 4. ่ฎพ็ฝฎ้‚ฎไปถไธป้ข˜ + * 5. ่ฎพ็ฝฎ้‚ฎไปถๅ†…ๅฎน๏ผˆๅŒ…ๅซ้ชŒ่ฏ็ ๅ’Œๆœ‰ๆ•ˆๆœŸ๏ผ‰ + * 6. ๅ‘้€้‚ฎไปถ + * + * @param email ๆ”ถไปถไบบ้‚ฎ็ฎฑ + * @param code 6 ไฝๆ•ฐๅญ—้ชŒ่ฏ็  + */ @Override public void sendPasswordResetCode(String email, String code) { + // 1. ๅˆ›ๅปบ็ฎ€ๅ•้‚ฎไปถๆถˆๆฏ SimpleMailMessage message = new SimpleMailMessage(); + + // 2. ่ฎพ็ฝฎๅ‘ไปถไบบ message.setFrom(from); + + // 3. ่ฎพ็ฝฎๆ”ถไปถไบบ message.setTo(email); + + // 4. ่ฎพ็ฝฎ้‚ฎไปถไธป้ข˜ message.setSubject("ๅฏ†็ ๆ‰พๅ›ž้ชŒ่ฏ็ "); + + // 5. ่ฎพ็ฝฎ้‚ฎไปถๅ†…ๅฎน message.setText(""" ๆ‚จๆญฃๅœจ่ฟ›่กŒๅฏ†็ ๆ‰พๅ›žๆ“ไฝœใ€‚ @@ -35,7 +86,9 @@ public class EmailServiceImpl implements EmailService { ๅฆ‚ๆžœ่ฟ™ไธๆ˜ฏๆ‚จ็š„ๆ“ไฝœ๏ผŒ่ฏทๅฟฝ็•ฅๆญค้‚ฎไปถใ€‚ """.formatted(code, expireMinutes)); + + // 6. ๅ‘้€้‚ฎไปถ mailSender.send(message); - log.info("ๅทฒๅ‘้€ๅฏ†็ ๆ‰พๅ›ž้ชŒ่ฏ็ ๅˆฐ้‚ฎ็ฎฑ: {}", email); + log.info("ๅทฒๅ‘้€ๅฏ†็ ๆ‰พๅ›ž้ชŒ่ฏ็ ๅˆฐ้‚ฎ็ฎฑ - email: {}, code: {}", email, code); } } diff --git a/src/main/java/com/aisi/template/service/impl/LoginAttemptServiceImpl.java b/src/main/java/com/aisi/template/service/impl/LoginAttemptServiceImpl.java new file mode 100644 index 0000000..ea8e119 --- /dev/null +++ b/src/main/java/com/aisi/template/service/impl/LoginAttemptServiceImpl.java @@ -0,0 +1,208 @@ +package com.aisi.template.service.impl; + +import com.aisi.template.domain.entity.User; +import com.aisi.template.repository.UserRepository; +import com.aisi.template.service.LoginAttemptService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +/** + * ็™ปๅฝ•ๅฐ่ฏ•ๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃๅค„็†็™ปๅฝ•ๅคฑ่ดฅ่ฎกๆ•ฐๅ’Œ่ดฆๆˆท้”ๅฎš + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅๅฐ่ฏ•๏ผš่ฎกๆ•ฐๅคฑ่ดฅๆฌกๆ•ฐ๏ผŒ่ถ…่ฟ‡้˜ˆๅ€ผๅˆ™้”ๅฎš่ดฆๆˆท + * 2. ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ๏ผš็™ปๅฝ•ๆˆๅŠŸๅŽๆธ…้›ถ + * 3. ๆฃ€ๆŸฅ่ดฆๆˆท้”ๅฎš็Šถๆ€๏ผšๅˆคๆ–ญ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + * 4. ้”ๅฎš่ดฆๆˆท๏ผšๆ‰‹ๅŠจๆˆ–่‡ชๅŠจ้”ๅฎš่ดฆๆˆท + * 5. ่Žทๅ–ๅ‰ฉไฝ™้”ๅฎšๆ—ถ้—ด๏ผš่ฎก็ฎ—่ดฆๆˆท่ฟ˜้œ€ๅคšไน…่งฃ้” + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ่ฟž็ปญๅคฑ่ดฅ 5 ๆฌก้”ๅฎš่ดฆๆˆท 30 ๅˆ†้’Ÿ + * - ้”ๅฎšๆœŸ้—ดๆ— ๆณ•็™ปๅฝ• + * - ็™ปๅฝ•ๆˆๅŠŸๅŽ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class LoginAttemptServiceImpl implements LoginAttemptService { + + /** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final UserRepository userRepository; + + /** + * ๆœ€ๅคงๅคฑ่ดฅๆฌกๆ•ฐ + * ไปŽ้…็ฝฎ่ฏปๅ–๏ผŒ้ป˜่ฎค 5 ๆฌก + */ + @Value("${app.login.max-attempts:5}") + private int maxAttempts; + + /** + * ้”ๅฎšๆ—ถ้•ฟ๏ผˆๅˆ†้’Ÿ๏ผ‰ + * ไปŽ้…็ฝฎ่ฏปๅ–๏ผŒ้ป˜่ฎค 30 ๅˆ†้’Ÿ + */ + @Value("${app.login.lock-duration-minutes:30}") + private int lockDurationMinutes; + + /** + * ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅๅฐ่ฏ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๅคฑ่ดฅๆฌกๆ•ฐๅŠ  1 + * 3. ๆฃ€ๆŸฅๆ˜ฏๅฆ่ถ…่ฟ‡ๆœ€ๅคงๅคฑ่ดฅๆฌกๆ•ฐ + * 4. ๅฆ‚ๆžœ่ถ…่ฟ‡๏ผŒ้”ๅฎš่ดฆๆˆทๅนถ่ฟ”ๅ›ž true + * 5. ไฟๅญ˜ๆ›ดๆ–ฐๅˆฐๆ•ฐๆฎๅบ“ + * + * @param username ็”จๆˆทๅ + * @return true ่กจ็คบ่ดฆๆˆทๅบ”่ฏฅ่ขซ้”ๅฎš + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean recordFailedAttempt(String username) { + // 1. ๆŸฅ่ฏข็”จๆˆท + User user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + return false; + } + + // 2. ๅคฑ่ดฅๆฌกๆ•ฐๅŠ  1 + int failedCount = user.getFailedLoginCount() + 1; + user.setFailedLoginCount(failedCount); + + // 3. ๆฃ€ๆŸฅๆ˜ฏๅฆๅบ”่ฏฅ้”ๅฎš่ดฆๆˆท + boolean shouldLock = failedCount >= maxAttempts; + if (shouldLock) { + // 3.1 ้”ๅฎš่ดฆๆˆท๏ผš่ฎก็ฎ—้”ๅฎšๅˆฐๆœŸๆ—ถ้—ด + LocalDateTime lockUntil = LocalDateTime.now().plusMinutes(lockDurationMinutes); + user.setLockedUntil(lockUntil); + log.warn("็”จๆˆทๅ› ็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ่ฟ‡ๅคš่ขซ้”ๅฎš - username: {}, failedCount: {}, lockUntil: {}", + username, failedCount, lockUntil); + } else { + log.debug("็”จๆˆท็™ปๅฝ•ๅคฑ่ดฅ - username: {}, failedCount: {}/{}", + username, failedCount, maxAttempts); + } + + // 4. ไฟๅญ˜ๆ›ดๆ–ฐ + userRepository.save(user); + return shouldLock; + } + + /** + * ้‡็ฝฎ็™ปๅฝ•ๅคฑ่ดฅ่ฎกๆ•ฐ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐไธบ 0 + * 3. ๆธ…้™ค้”ๅฎšๆ—ถ้—ด + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param username ็”จๆˆทๅ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void resetFailedAttempts(String username) { + // 1. ๆŸฅ่ฏข็”จๆˆท + User user = userRepository.findByUsername(username).orElse(null); + if (user != null) { + // 2. ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐไธบ 0 + user.setFailedLoginCount(0); + // 3. ๆธ…้™ค้”ๅฎšๆ—ถ้—ด + user.setLockedUntil(null); + userRepository.save(user); + log.debug("็”จๆˆท็™ปๅฝ•ๅคฑ่ดฅ่ฎกๆ•ฐๅทฒ้‡็ฝฎ - username: {}", username); + } + } + + /** + * ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ่ฐƒ็”จๅฎžไฝ“็š„ isLocked() ๆ–นๆณ•ๅˆคๆ–ญ + * + * ่ฏดๆ˜Ž๏ผš + * - ๅฎžไฝ“็š„ isLocked() ๆ–นๆณ•ไผš่‡ชๅŠจๅค„็†่ฟ‡ๆœŸ้”ๅฎš็š„ๅˆคๆ–ญ + * + * @param username ็”จๆˆทๅ + * @return true ่กจ็คบ่ดฆๆˆท่ขซ้”ๅฎš + */ + @Override + public boolean isLocked(String username) { + // 1. ๆŸฅ่ฏข็”จๆˆท + User user = userRepository.findByUsername(username).orElse(null); + if (user == null) { + return false; + } + // 2. ๅˆคๆ–ญๆ˜ฏๅฆ่ขซ้”ๅฎš๏ผˆๅฎžไฝ“็š„ isLocked() ๆ–นๆณ•ไผš่‡ชๅŠจๅค„็†่ฟ‡ๆœŸๅˆคๆ–ญ๏ผ‰ + return user.isLocked(); + } + + /** + * ้”ๅฎš่ดฆๆˆท + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ่ฎพ็ฝฎ้”ๅฎšๆ—ถ้—ดไธบๅฝ“ๅ‰ๆ—ถ้—ด + ๆŒ‡ๅฎšๅˆ†้’Ÿๆ•ฐ + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็ฎก็†ๅ‘˜ๆ‰‹ๅŠจ้”ๅฎš็”จๆˆท + * - ่‡ชๅŠจ้”ๅฎš๏ผˆ็”ฑ recordFailedAttempt ่งฆๅ‘๏ผ‰ + * + * @param username ็”จๆˆทๅ + * @param lockDurationMinutes ้”ๅฎšๆ—ถ้•ฟ๏ผˆๅˆ†้’Ÿ๏ผ‰ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void lockAccount(String username, int lockDurationMinutes) { + // 1. ๆŸฅ่ฏข็”จๆˆท + User user = userRepository.findByUsername(username).orElse(null); + if (user != null) { + // 2. ่ฎพ็ฝฎ้”ๅฎšๆ—ถ้—ด + LocalDateTime lockUntil = LocalDateTime.now().plusMinutes(lockDurationMinutes); + user.setLockedUntil(lockUntil); + userRepository.save(user); + log.warn("่ดฆๆˆทๅทฒ่ขซๆ‰‹ๅŠจ้”ๅฎš - username: {}, lockUntil: {}", username, lockUntil); + } + } + + /** + * ่Žทๅ–่ดฆๆˆทๅ‰ฉไฝ™้”ๅฎšๆ—ถ้—ด + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰้”ๅฎšๆ—ถ้—ด + * 3. ๆฃ€ๆŸฅ้”ๅฎšๆ˜ฏๅฆๅทฒ่ฟ‡ๆœŸ + * 4. ่ฎก็ฎ—ๅ‰ฉไฝ™ๅˆ†้’Ÿๆ•ฐ + * + * @param username ็”จๆˆทๅ + * @return ๅ‰ฉไฝ™้”ๅฎšๆ—ถ้—ด๏ผˆๅˆ†้’Ÿ๏ผ‰๏ผŒๆœช้”ๅฎš่ฟ”ๅ›ž 0 + */ + @Override + public Long getRemainingLockTimeMinutes(String username) { + // 1. ๆŸฅ่ฏข็”จๆˆท + User user = userRepository.findByUsername(username).orElse(null); + if (user == null || user.getLockedUntil() == null) { + return 0L; + } + + // 2. ๆฃ€ๆŸฅ้”ๅฎšๆ˜ฏๅฆๅทฒ่ฟ‡ๆœŸ + if (LocalDateTime.now().isAfter(user.getLockedUntil())) { + return 0L; + } + + // 3. ่ฎก็ฎ—ๅ‰ฉไฝ™ๅˆ†้’Ÿๆ•ฐ + long remainingMinutes = java.time.Duration.between( + LocalDateTime.now(), + user.getLockedUntil() + ).toMinutes(); + + return Math.max(0, remainingMinutes); + } +} diff --git a/src/main/java/com/aisi/template/service/impl/PasswordResetServiceImpl.java b/src/main/java/com/aisi/template/service/impl/PasswordResetServiceImpl.java index de83323..b47d49a 100644 --- a/src/main/java/com/aisi/template/service/impl/PasswordResetServiceImpl.java +++ b/src/main/java/com/aisi/template/service/impl/PasswordResetServiceImpl.java @@ -25,38 +25,107 @@ import java.util.List; import java.util.Optional; import java.security.SecureRandom; +/** + * ๅฏ†็ ้‡็ฝฎๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃๅฏ†็ ้‡็ฝฎๆต็จ‹็š„ไธšๅŠก้€ป่พ‘ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๅ‘้€้‡็ฝฎ้ชŒ่ฏ็ ๏ผš็”Ÿๆˆ้ชŒ่ฏ็ ๅนถๅ‘้€ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ + * 2. ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็ ๏ผš้ชŒ่ฏ้ชŒ่ฏ็ ๅนถๆ›ดๆ–ฐๅฏ†็  + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ้ชŒ่ฏ็  6 ไฝๆ•ฐๅญ—๏ผŒ10 ๅˆ†้’Ÿๆœ‰ๆ•ˆ + * - ๅŒไธ€้‚ฎ็ฎฑ 60 ็ง’ๅ†…ๅช่ƒฝ่ฏทๆฑ‚ไธ€ๆฌก๏ผˆๅ†ทๅดๆ—ถ้—ด๏ผ‰ + * - ๅŒไธ€้‚ฎ็ฎฑ 1 ๅฐๆ—ถๅ†…ๆœ€ๅคš่ฏทๆฑ‚ 5 ๆฌก + * - ้ชŒ่ฏ็ ๆœ€ๅคšๅฐ่ฏ• 5 ๆฌก๏ผŒ่ถ…่ฟ‡ๅŽๅคฑๆ•ˆ + * - ้ชŒ่ฏ็ ๅ“ˆๅธŒๅญ˜ๅ‚จ๏ผˆSHA-256๏ผ‰๏ผŒไธๅญ˜ๅ‚จๆ˜Žๆ–‡ + * - ๆ–ฐๅฏ†็ ไฝฟ็”จ BCrypt ๅŠ ๅฏ† + * + * @author Claude + * @since 2024-04-09 + */ @Slf4j @Service @RequiredArgsConstructor public class PasswordResetServiceImpl implements PasswordResetService { + + /** + * ๅฎ‰ๅ…จ้šๆœบๆ•ฐ็”Ÿๆˆๅ™จ + * ็”จไบŽ็”Ÿๆˆ้ชŒ่ฏ็  + */ private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + /** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ private final UserRepository userRepository; + + /** + * ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ private final PasswordResetCodeRepository passwordResetCodeRepository; + + /** + * ๅฏ†็ ็ผ–็ ๅ™จ + * ็”จไบŽๅŠ ๅฏ†ๆ–ฐๅฏ†็  + */ private final PasswordEncoder passwordEncoder; + + /** + * ้‚ฎไปถๆœๅŠก + * ็”จไบŽๅ‘้€้ชŒ่ฏ็ ้‚ฎไปถ + */ private final EmailService emailService; + /** + * ้ชŒ่ฏ็ ๆœ‰ๆ•ˆๆœŸ๏ผˆๅˆ†้’Ÿ๏ผ‰๏ผŒ้ป˜่ฎค 10 ๅˆ†้’Ÿ + */ @Value("${app.password-reset.code-expire-minutes:10}") private Integer expireMinutes; + /** + * ่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด๏ผˆ็ง’๏ผ‰๏ผŒ้ป˜่ฎค 60 ็ง’ + */ @Value("${app.password-reset.request-cooldown-seconds:60}") private Integer cooldownSeconds; + /** + * ้ชŒ่ฏ็ ๆœ€ๅคงๅฐ่ฏ•ๆฌกๆ•ฐ๏ผŒ้ป˜่ฎค 5 ๆฌก + */ @Value("${app.password-reset.max-attempts:5}") private Integer maxAttempts; + /** + * ๅ‘้€ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็  + * ๆญฅ้ชค๏ผš + * 1. ๆ ‡ๅ‡†ๅŒ–้‚ฎ็ฎฑ๏ผˆ่ฝฌๅฐๅ†™๏ผŒๅŽป้™ค็ฉบๆ ผ๏ผ‰ + * 2. ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ๏ผˆไธบ้˜ฒๆญข็”จๆˆทๅๆžšไธพ๏ผŒๆ— ่ฎบๆ˜ฏๅฆๅญ˜ๅœจ้ƒฝ่ฟ”ๅ›žๆˆๅŠŸ๏ผ‰ + * 3. ๆฃ€ๆŸฅ่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด๏ผˆ้˜ฒๆญข้ข‘็น่ฏทๆฑ‚๏ผ‰ + * 4. ๆฃ€ๆŸฅ 1 ๅฐๆ—ถๅ†…่ฏทๆฑ‚ๆฌกๆ•ฐ๏ผˆ้˜ฒๆญขๆปฅ็”จ๏ผ‰ + * 5. ๅฐ†่ฏฅ้‚ฎ็ฎฑ็š„ๆ‰€ๆœ‰ๆ—ง้ชŒ่ฏ็ ๆ ‡่ฎฐไธบๅทฒไฝฟ็”จ + * 6. ็”Ÿๆˆ 6 ไฝ้šๆœบๆ•ฐๅญ—้ชŒ่ฏ็  + * 7. ๅฐ†้ชŒ่ฏ็ ๅ“ˆๅธŒๅŽๅญ˜ๅ‚จๅˆฐๆ•ฐๆฎๅบ“ + * 8. ๅ‘้€้ชŒ่ฏ็ ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ + * + * @param requestDto ้‡็ฝฎ่ฏทๆฑ‚๏ผˆ้‚ฎ็ฎฑ๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ @Override @Transactional public RestBean sendResetCode(PasswordResetRequestDto requestDto) { + // 1. ๆ ‡ๅ‡†ๅŒ–้‚ฎ็ฎฑ๏ผˆ่ฝฌๅฐๅ†™๏ผŒๅŽป้™ค้ฆ–ๅฐพ็ฉบๆ ผ๏ผ‰ String email = requestDto.getEmail().trim().toLowerCase(); + // 2. ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ + // ๆณจๆ„๏ผšไธบ้˜ฒๆญข็”จๆˆทๅๆžšไธพๆ”ปๅ‡ป๏ผŒๆ— ่ฎบ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ้ƒฝ่ฟ”ๅ›ž็›ธๅŒๆถˆๆฏ Optional userOptional = userRepository.findByEmailIgnoreCase(email); if (userOptional.isEmpty()) { return RestBean.success(200, "ๅฆ‚ๆžœ้‚ฎ็ฎฑๅทฒๆณจๅ†Œ๏ผŒ้ชŒ่ฏ็ ๅฐ†ๅ‘้€่‡ณ่ฏฅ้‚ฎ็ฎฑ", null); } + // 3. ๆฃ€ๆŸฅ่ฏทๆฑ‚ๅ†ทๅดๆ—ถ้—ด๏ผˆ้˜ฒๆญข้ข‘็น่ฏทๆฑ‚๏ผ‰ Optional latestCode = passwordResetCodeRepository - .findFirstByEmailAndUsedFalseOrderByCreatedAtDesc(email); + .findFirstByEmailAndUsedFalseOrderByCreatedAtDesc(email); if (latestCode.isPresent()) { LocalDateTime nextAllowedAt = latestCode.get().getCreatedAt().plusSeconds(cooldownSeconds); if (nextAllowedAt.isAfter(LocalDateTime.now())) { @@ -65,64 +134,103 @@ public class PasswordResetServiceImpl implements PasswordResetService { } } - long recentCount = passwordResetCodeRepository.countByEmailAndCreatedAtAfter(email, LocalDateTime.now().minusHours(1)); + // 4. ๆฃ€ๆŸฅ 1 ๅฐๆ—ถๅ†…่ฏทๆฑ‚ๆฌกๆ•ฐ๏ผˆ้˜ฒๆญขๆปฅ็”จ๏ผ‰ + long recentCount = passwordResetCodeRepository.countByEmailAndCreatedAtAfter( + email, LocalDateTime.now().minusHours(1)); if (recentCount >= 5) { return RestBean.failure(429, "่ฏฅ้‚ฎ็ฎฑๅœจ 1 ๅฐๆ—ถๅ†…่ฏทๆฑ‚ๆฌกๆ•ฐ่ฟ‡ๅคš๏ผŒ่ฏท็จๅŽๅ†่ฏ•", null); } + // 5. ๅฐ†่ฏฅ้‚ฎ็ฎฑ็š„ๆ‰€ๆœ‰ๆ—ง้ชŒ่ฏ็ ๆ ‡่ฎฐไธบๅทฒไฝฟ็”จ๏ผˆ้ฟๅ…ๆททๆท†๏ผ‰ List activeCodes = passwordResetCodeRepository.findByEmailAndUsedFalse(email); for (PasswordResetCode item : activeCodes) { item.setUsed(true); } passwordResetCodeRepository.saveAll(activeCodes); + // 6. ็”Ÿๆˆ 6 ไฝ้šๆœบๆ•ฐๅญ—้ชŒ่ฏ็  String code = generateCode(); + + // 7. ๅฐ†้ชŒ่ฏ็ ๅ“ˆๅธŒๅŽๅญ˜ๅ‚จๅˆฐๆ•ฐๆฎๅบ“ PasswordResetCode resetCode = new PasswordResetCode(); resetCode.setEmail(email); - resetCode.setCodeHash(sha256(code)); + resetCode.setCodeHash(sha256(code)); // ๅชๅญ˜ๅ‚จๅ“ˆๅธŒๅ€ผ๏ผŒไธๅญ˜ๅ‚จๆ˜Žๆ–‡ resetCode.setExpiresAt(LocalDateTime.now().plusMinutes(expireMinutes)); resetCode.setUsed(false); resetCode.setAttemptCount(0); passwordResetCodeRepository.save(resetCode); - emailService.sendPasswordResetCode(email, code); + // 8. ๅ‘้€้ชŒ่ฏ็ ๅˆฐ็”จๆˆท้‚ฎ็ฎฑ๏ผˆๅฎน้”™๏ผš้‚ฎไปถๆœๅŠกไธๅฏ็”จๆ—ถไธๅฝฑๅ“้ชŒ่ฏ็ ็”Ÿๆˆ๏ผ‰ + try { + emailService.sendPasswordResetCode(email, code); + log.info("ๅฏ†็ ้‡็ฝฎ้ชŒ่ฏ็ ๅทฒๅ‘้€ - email: {}", email); + } catch (Exception e) { + log.warn("้‚ฎไปถๅ‘้€ๅคฑ่ดฅ๏ผŒ้ชŒ่ฏ็ ๅทฒ็”Ÿๆˆไฝ†ๆœชๅ‘้€ - email: {}, error: {}", email, e.getMessage()); + } return RestBean.success(200, "ๅฆ‚ๆžœ้‚ฎ็ฎฑๅทฒๆณจๅ†Œ๏ผŒ้ชŒ่ฏ็ ๅฐ†ๅ‘้€่‡ณ่ฏฅ้‚ฎ็ฎฑ", null); } + /** + * ไฝฟ็”จ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็  + * ๆญฅ้ชค๏ผš + * 1. ๆ ‡ๅ‡†ๅŒ–้‚ฎ็ฎฑ + * 2. ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ + * 3. ่Žทๅ–ๆœ€ๆ–ฐ็š„ๆœชไฝฟ็”จ้ชŒ่ฏ็  + * 4. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆๅทฒไฝฟ็”จ + * 5. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * 6. ้ชŒ่ฏๅฐ่ฏ•ๆฌกๆ•ฐๆ˜ฏๅฆ่ถ…้™ + * 7. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆๆญฃ็กฎ + * 8. ๆ›ดๆ–ฐ็”จๆˆทๅฏ†็  + * 9. ๆ ‡่ฎฐ้ชŒ่ฏ็ ไธบๅทฒไฝฟ็”จ + * 10. ๅฐ†่ฏฅ้‚ฎ็ฎฑ็š„ๆ‰€ๆœ‰ๅ…ถไป–้ชŒ่ฏ็ ๆ ‡่ฎฐไธบๅทฒไฝฟ็”จ + * + * @param confirmDto ็กฎ่ฎค่ฏทๆฑ‚๏ผˆ้‚ฎ็ฎฑใ€้ชŒ่ฏ็ ใ€ๆ–ฐๅฏ†็ ๏ผ‰ + * @return ๆˆๅŠŸๅ“ๅบ” + */ @Override @Transactional public RestBean resetPassword(PasswordResetConfirmDto confirmDto) { + // 1. ๆ ‡ๅ‡†ๅŒ–้‚ฎ็ฎฑ String email = confirmDto.getEmail().trim().toLowerCase(); + + // 2. ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ Optional userOptional = userRepository.findByEmailIgnoreCase(email); if (userOptional.isEmpty()) { return RestBean.failure(400, "้ชŒ่ฏ็ ๆˆ–้‚ฎ็ฎฑไธๆญฃ็กฎ", null); } + // 3. ่Žทๅ–ๆœ€ๆ–ฐ็š„ๆœชไฝฟ็”จ้ชŒ่ฏ็  PasswordResetCode resetCode = passwordResetCodeRepository - .findFirstByEmailAndUsedFalseOrderByCreatedAtDesc(email) - .orElse(null); + .findFirstByEmailAndUsedFalseOrderByCreatedAtDesc(email) + .orElse(null); if (resetCode == null) { return RestBean.failure(400, "่ฏทๅ…ˆ่Žทๅ–้ชŒ่ฏ็ ", null); } + // 4. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆๅทฒไฝฟ็”จ if (Boolean.TRUE.equals(resetCode.getUsed())) { return RestBean.failure(400, "้ชŒ่ฏ็ ๅทฒๅคฑๆ•ˆ๏ผŒ่ฏท้‡ๆ–ฐ่Žทๅ–", null); } + // 5. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆ่ฟ‡ๆœŸ if (resetCode.getExpiresAt().isBefore(LocalDateTime.now())) { resetCode.setUsed(true); passwordResetCodeRepository.save(resetCode); return RestBean.failure(400, "้ชŒ่ฏ็ ๅทฒ่ฟ‡ๆœŸ๏ผŒ่ฏท้‡ๆ–ฐ่Žทๅ–", null); } + // 6. ้ชŒ่ฏๅฐ่ฏ•ๆฌกๆ•ฐๆ˜ฏๅฆ่ถ…้™ if (resetCode.getAttemptCount() >= maxAttempts) { resetCode.setUsed(true); passwordResetCodeRepository.save(resetCode); return RestBean.failure(400, "้ชŒ่ฏ็ ๅฐ่ฏ•ๆฌกๆ•ฐ่ฟ‡ๅคš๏ผŒ่ฏท้‡ๆ–ฐ่Žทๅ–", null); } + // 7. ้ชŒ่ฏ้ชŒ่ฏ็ ๆ˜ฏๅฆๆญฃ็กฎ if (!sha256(confirmDto.getCode()).equals(resetCode.getCodeHash())) { + // 7.1 ๅขžๅŠ ๅฐ่ฏ•ๆฌกๆ•ฐ resetCode.setAttemptCount(resetCode.getAttemptCount() + 1); + // 7.2 ๅฆ‚ๆžœๅฐ่ฏ•ๆฌกๆ•ฐ่ถ…้™๏ผŒๆ ‡่ฎฐไธบๅทฒไฝฟ็”จ if (resetCode.getAttemptCount() >= maxAttempts) { resetCode.setUsed(true); } @@ -130,31 +238,58 @@ public class PasswordResetServiceImpl implements PasswordResetService { return RestBean.failure(400, "้ชŒ่ฏ็ ไธๆญฃ็กฎ", null); } + // 8. ๆ›ดๆ–ฐ็”จๆˆทๅฏ†็  User user = userOptional.get(); user.setPassword(passwordEncoder.encode(confirmDto.getNewPassword())); + user.setPasswordChangedAt(LocalDateTime.now()); userRepository.save(user); + // 9. ๆ ‡่ฎฐๅฝ“ๅ‰้ชŒ่ฏ็ ไธบๅทฒไฝฟ็”จ resetCode.setUsed(true); passwordResetCodeRepository.save(resetCode); + // 10. ๅฐ†่ฏฅ้‚ฎ็ฎฑ็š„ๆ‰€ๆœ‰ๅ…ถไป–้ชŒ่ฏ็ ๆ ‡่ฎฐไธบๅทฒไฝฟ็”จ List otherCodes = passwordResetCodeRepository.findByEmailAndUsedFalse(email); for (PasswordResetCode item : otherCodes) { item.setUsed(true); } passwordResetCodeRepository.saveAll(otherCodes); - log.info("็”จๆˆท้€š่ฟ‡้‚ฎ็ฎฑ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็ ๆˆๅŠŸ: {}", email); + log.info("็”จๆˆท้€š่ฟ‡้‚ฎ็ฎฑ้ชŒ่ฏ็ ้‡็ฝฎๅฏ†็ ๆˆๅŠŸ - email: {}", email); return RestBean.success(RestCode.SUCCESS); } + /** + * ็”Ÿๆˆ 6 ไฝ้šๆœบๆ•ฐๅญ—้ชŒ่ฏ็  + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆ 0 ๅˆฐ 999999 ไน‹้—ด็š„้šๆœบๆ•ฐ + * 2. ๆ ผๅผๅŒ–ไธบ 6 ไฝๆ•ฐๅญ—๏ผˆไธ่ถณ่กฅ้›ถ๏ผ‰ + * + * @return 6 ไฝๆ•ฐๅญ—้ชŒ่ฏ็  + */ private String generateCode() { return String.format("%06d", SECURE_RANDOM.nextInt(1_000_000)); } + /** + * ๅฏนๅญ—็ฌฆไธฒ่ฟ›่กŒ SHA-256 ๅ“ˆๅธŒ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ– SHA-256 ็ฎ—ๆณ•ๅฎžไพ‹ + * 2. ๅฏนๅญ—็ฌฆไธฒ็š„ๅญ—่Š‚่ฟ›่กŒๅ“ˆๅธŒ + * 3. ๅฐ†็ป“ๆžœ่ฝฌๆขไธบๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ + * + * @param value ๅพ…ๅ“ˆๅธŒ็š„ๅญ—็ฌฆไธฒ + * @return ๅ“ˆๅธŒๅŽ็š„ๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ + */ private String sha256(String value) { try { + // 1. ่Žทๅ– SHA-256 ็ฎ—ๆณ•ๅฎžไพ‹ MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + // 2. ๅฏนๅญ—็ฌฆไธฒ่ฟ›่กŒๅ“ˆๅธŒ byte[] hash = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + + // 3. ่ฝฌๆขไธบๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ StringBuilder builder = new StringBuilder(); for (byte b : hash) { String hex = Integer.toHexString(0xff & b); diff --git a/src/main/java/com/aisi/template/service/impl/SysMenuServiceImpl.java b/src/main/java/com/aisi/template/service/impl/SysMenuServiceImpl.java new file mode 100644 index 0000000..9d1cfaf --- /dev/null +++ b/src/main/java/com/aisi/template/service/impl/SysMenuServiceImpl.java @@ -0,0 +1,346 @@ +package com.aisi.template.service.impl; + +import com.aisi.template.domain.dto.MenuDto; +import com.aisi.template.domain.entity.SysMenu; +import com.aisi.template.domain.vo.MenuVo; +import com.aisi.template.exception.BusinessException; +import com.aisi.template.repository.SysMenuRepository; +import com.aisi.template.repository.SysRoleRepository; +import com.aisi.template.repository.UserRepository; +import com.aisi.template.service.SysMenuService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * ็ณป็ปŸ่œๅ•ๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃ่œๅ•็š„็ฎก็†ๅ’Œๆ ‘ๅฝข็ป“ๆž„ๆž„ๅปบ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่œๅ•็ฎก็†๏ผšๅˆ›ๅปบใ€ๆ›ดๆ–ฐใ€ๅˆ ้™คใ€ๆŸฅ่ฏข่œๅ• + * 2. ๆ ‘ๅฝข็ป“ๆž„๏ผšๆž„ๅปบ่œๅ•็š„ๅฑ‚็บงๅ…ณ็ณป + * 3. ็”จๆˆท่œๅ•๏ผšๆ นๆฎ็”จๆˆท่ง’่‰ฒ่Žทๅ–ๅฏ่ฎฟ้—ฎ็š„่œๅ• + * + * ่œๅ•็ฑปๅž‹๏ผš + * - DIRECTORY(1)๏ผš็›ฎๅฝ•๏ผŒ็”จไบŽๅˆ†็ป„๏ผŒไธๅฏนๅบ”ๅ…ทไฝ“้กต้ข + * - MENU(2)๏ผš่œๅ•้กน๏ผŒๅฏนๅบ”ๅ…ทไฝ“้กต้ข + * - BUTTON(3)๏ผšๆŒ‰้’ฎ๏ผŒ้กต้ขๅ†…็š„ๆ“ไฝœๆŒ‰้’ฎ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysMenuServiceImpl implements SysMenuService { + + /** + * ่œๅ•ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final SysMenuRepository menuRepository; + + /** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final UserRepository userRepository; + + /** + * ่ง’่‰ฒๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final SysRoleRepository roleRepository; + + /** + * ๅˆ›ๅปบ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๅฆ‚ๆžœๆœ‰็ˆถ่œๅ•๏ผŒๆฃ€ๆŸฅ็ˆถ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆž„ๅปบ่œๅ•ๅฎžไฝ“ๅฏน่ฑก + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param menuDto ่œๅ•ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * @return ๅˆ›ๅปบ็š„่œๅ•่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“็ˆถ่œๅ•ไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public MenuVo create(MenuDto menuDto) { + // 1. ๆฃ€ๆŸฅ็ˆถ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ๏ผˆๅฆ‚ๆžœๆŒ‡ๅฎšไบ†็ˆถ่œๅ•๏ผ‰ + if (menuDto.getParentId() != null && menuDto.getParentId() > 0) { + if (!menuRepository.existsById(menuDto.getParentId())) { + throw new BusinessException("็ˆถ่œๅ•ไธๅญ˜ๅœจ: " + menuDto.getParentId()); + } + } + + // 2. ๆž„ๅปบ่œๅ•ๅฎžไฝ“ + SysMenu menu = new SysMenu(); + menu.setParentId(menuDto.getParentId()); + menu.setMenuName(menuDto.getMenuName()); + menu.setMenuType(menuDto.getMenuType()); + menu.setMenuPath(menuDto.getMenuPath()); + menu.setComponent(menuDto.getComponent()); + menu.setIcon(menuDto.getIcon()); + menu.setSortOrder(menuDto.getSortOrder()); + menu.setVisible(menuDto.getVisible()); + menu.setStatus(menuDto.getStatus()); + menu.setPermissionCode(menuDto.getPermissionCode()); + + // 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + SysMenu savedMenu = menuRepository.save(menu); + log.info("่œๅ•ๅˆ›ๅปบๆˆๅŠŸ - id: {}, name: {}", savedMenu.getId(), savedMenu.getMenuName()); + return convertToVo(savedMenu); + } + + /** + * ๆ›ดๆ–ฐ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๅฐ†่œๅ•่ฎพ็ฝฎไธบ่‡ชๅทฑ็š„ๅญ่œๅ•๏ผˆๅพช็Žฏๅผ•็”จ๏ผ‰ + * 3. ๆ›ดๆ–ฐ่œๅ•ไฟกๆฏ + * + * @param id ่œๅ•ID + * @param menuDto ่œๅ•ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * @return ๆ›ดๆ–ฐๅŽ็š„่œๅ•่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“่œๅ•ไธๅญ˜ๅœจๆˆ–ไบง็”Ÿๅพช็Žฏๅผ•็”จๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public MenuVo update(Long id, MenuDto menuDto) { + // 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + SysMenu menu = menuRepository.findById(id) + .orElseThrow(() -> new BusinessException("่œๅ•ไธๅญ˜ๅœจ: " + id)); + + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๅฐ†่œๅ•่ฎพ็ฝฎไธบ่‡ชๅทฑ็š„ๅญ่œๅ• + if (menuDto.getParentId() != null && menuDto.getParentId().equals(id)) { + throw new BusinessException("ไธ่ƒฝๅฐ†่œๅ•่ฎพ็ฝฎไธบ่‡ชๅทฑ็š„ๅญ่œๅ•"); + } + + // 3. ๆ›ดๆ–ฐ่œๅ•ไฟกๆฏ + menu.setParentId(menuDto.getParentId()); + menu.setMenuName(menuDto.getMenuName()); + menu.setMenuType(menuDto.getMenuType()); + menu.setMenuPath(menuDto.getMenuPath()); + menu.setComponent(menuDto.getComponent()); + menu.setIcon(menuDto.getIcon()); + menu.setSortOrder(menuDto.getSortOrder()); + menu.setVisible(menuDto.getVisible()); + menu.setStatus(menuDto.getStatus()); + menu.setPermissionCode(menuDto.getPermissionCode()); + + // 4. ไฟๅญ˜ๆ›ดๆ–ฐ + SysMenu savedMenu = menuRepository.save(menu); + log.info("่œๅ•ๆ›ดๆ–ฐๆˆๅŠŸ - id: {}, name: {}", savedMenu.getId(), savedMenu.getMenuName()); + return convertToVo(savedMenu); + } + + /** + * ๅˆ ้™ค่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๅˆ ้™ค่œๅ•๏ผˆๅญ่œๅ•ไผš็บง่”ๅˆ ้™ค๏ผ‰ + * + * ๆณจๆ„๏ผš + * - ๅˆ ้™ค็ˆถ่œๅ•ไผš่‡ชๅŠจๅˆ ้™คๆ‰€ๆœ‰ๅญ่œๅ• + * - ๅปบ่ฎฎๅˆ ้™คๅ‰ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰ๅญ่œๅ• + * + * @param id ่œๅ•ID + * @throws BusinessException ๅฝ“่œๅ•ไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + // 1. ๆฃ€ๆŸฅ่œๅ•ๆ˜ฏๅฆๅญ˜ๅœจ + if (!menuRepository.existsById(id)) { + throw new BusinessException("่œๅ•ไธๅญ˜ๅœจ: " + id); + } + + // 2. ๅˆ ้™ค่œๅ•๏ผˆๅญ่œๅ•ไผš็บง่”ๅˆ ้™ค๏ผ‰ + menuRepository.deleteById(id); + log.info("่œๅ•ๅˆ ้™คๆˆๅŠŸ - id: {}", id); + } + + /** + * ๆ นๆฎID่Žทๅ–่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่œๅ• + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param id ่œๅ•ID + * @return ่œๅ•่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“่œๅ•ไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + public MenuVo getById(Long id) { + // 1. ๆŸฅ่ฏข่œๅ• + SysMenu menu = menuRepository.findById(id) + .orElseThrow(() -> new BusinessException("่œๅ•ไธๅญ˜ๅœจ: " + id)); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + return convertToVo(menu); + } + + /** + * ่Žทๅ–่œๅ•ๆ ‘ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰ๅฏ็”จ็Šถๆ€็š„่œๅ• + * 2. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * 3. ้€’ๅฝ’ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„ + * + * ๆ ‘ๅฝข็ป“ๆž„็คบไพ‹๏ผš + * - ็ณป็ปŸ็ฎก็† (parentId = null) + * - ็”จๆˆท็ฎก็† (parentId = 1) + * - ๆทปๅŠ ๆŒ‰้’ฎ (parentId = 2) + * - ็ผ–่พ‘ๆŒ‰้’ฎ (parentId = 2) + * - ่ง’่‰ฒ็ฎก็† (parentId = 1) + * + * @return ่œๅ•ๆ ‘ๅˆ—่กจ + */ + @Override + public List getMenuTree() { + // 1. ๆŸฅ่ฏขๆ‰€ๆœ‰ๅฏ็”จ็Šถๆ€็š„่œๅ•๏ผŒๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + List allMenus = menuRepository.findByStatusOrderBySortOrder(1); + + // 2. ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„๏ผˆๆ น่Š‚็‚น็š„ parentId ไธบ 0๏ผ‰ + return buildMenuTree(allMenus, 0L); + } + + /** + * ่Žทๅ–็”จๆˆท่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + * 2. ๆ นๆฎ่ง’่‰ฒๆŸฅ่ฏขๅฏ่ฎฟ้—ฎ็š„่œๅ• + * 3. ่ฟ‡ๆปคๆމๆŒ‰้’ฎ็ฑปๅž‹๏ผˆๅช่ฟ”ๅ›ž็›ฎๅฝ•ๅ’Œ่œๅ•๏ผ‰ + * 4. ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„ + * + * ่ฏดๆ˜Ž๏ผš + * - ๅช่ฟ”ๅ›ž็›ฎๅฝ•(DIRECTORY)ๅ’Œ่œๅ•(MENU)๏ผŒไธ่ฟ”ๅ›žๆŒ‰้’ฎ(BUTTON) + * - ๆŒ‰้’ฎๆƒ้™้€š่ฟ‡ๅ‰็ซฏ็š„ๆƒ้™ๆŒ‡ไปคๆŽงๅˆถ + * + * @param userId ็”จๆˆทID + * @return ็”จๆˆทๅฏ่ฎฟ้—ฎ็š„่œๅ•ๆ ‘ + * @throws BusinessException ๅฝ“็”จๆˆทไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + public List getUserMenus(Long userId) { + // 1. ๆŸฅ่ฏข็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + var user = userRepository.findByIdWithRoles(userId) + .orElseThrow(() -> new BusinessException("็”จๆˆทไธๅญ˜ๅœจ: " + userId)); + + // 2. ๆๅ–่ง’่‰ฒIDๅˆ—่กจ + List roleIds = user.getRoles().stream() + .map(role -> role.getId()) + .collect(Collectors.toList()); + + // 3. ๅฆ‚ๆžœ็”จๆˆทๆฒกๆœ‰่ง’่‰ฒ๏ผŒ่ฟ”ๅ›ž็ฉบๅˆ—่กจ + if (roleIds.isEmpty()) { + return new ArrayList<>(); + } + + // 4. ๆŸฅ่ฏข่ง’่‰ฒๅฏ่ฎฟ้—ฎ็š„่œๅ• + // 4.1 ๆŸฅ่ฏข็›ฎๅฝ•็ฑปๅž‹๏ผˆmenuType = 1๏ผ‰ + // 4.2 ๆŸฅ่ฏข่œๅ•็ฑปๅž‹๏ผˆmenuType = 2๏ผ‰ + // ๆณจๆ„๏ผšไธๆŸฅ่ฏขๆŒ‰้’ฎ็ฑปๅž‹๏ผˆmenuType = 3๏ผ‰ + Set menus = new java.util.HashSet<>(menuRepository.findByRoleIdsAndMenuType(roleIds, 2)); // ่œๅ• + menus.addAll(menuRepository.findByRoleIdsAndMenuType(roleIds, 1)); // ็›ฎๅฝ• + + // 5. ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„ + return buildMenuTree(new ArrayList<>(menus), 0L); + } + + /** + * ๆ นๆฎ็ˆถID่Žทๅ–ๅญ่œๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆŒ‡ๅฎš็ˆถIDไธ‹็š„ๆ‰€ๆœ‰ๅญ่œๅ• + * 2. ๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * + * @param parentId ็ˆถ่œๅ•ID๏ผˆnull ่กจ็คบๆŸฅ่ฏขๆ น่œๅ•๏ผ‰ + * @return ๅญ่œๅ•ๅˆ—่กจ + */ + @Override + public List getByParentId(Long parentId) { + // 1. ๆŸฅ่ฏขๅญ่œๅ•๏ผŒๆŒ‰ๆŽ’ๅบๅญ—ๆฎตๆŽ’ๅบ + List menus = menuRepository.findByParentIdOrderBySortOrder(parentId); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + return menus.stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + } + + /** + * ้€’ๅฝ’ๆž„ๅปบ่œๅ•ๆ ‘ + * ๆญฅ้ชค๏ผš + * 1. ้ๅކๆ‰€ๆœ‰่œๅ• + * 2. ๆ‰พๅˆฐๅฝ“ๅ‰็ˆถID็š„ๅญ่œๅ• + * 3. ้€’ๅฝ’ๆŸฅๆ‰พๅญ่œๅ•็š„ๅญ่œๅ• + * 4. ๆž„ๅปบๆ ‘ๅฝข็ป“ๆž„ + * + * @param menus ๆ‰€ๆœ‰่œๅ•ๅˆ—่กจ + * @param parentId ็ˆถ่œๅ•ID + * @return ๅญ่œๅ•ๆ ‘ๅˆ—่กจ + */ + private List buildMenuTree(List menus, Long parentId) { + List result = new ArrayList<>(); + + // 1. ้ๅކๆ‰€ๆœ‰่œๅ•๏ผŒๆ‰พๅˆฐๅฝ“ๅ‰็ˆถID็š„ๅญ่œๅ• + for (SysMenu menu : menus) { + // ๅค„็† parentId ไธบ null ็š„ๆƒ…ๅ†ต๏ผˆๆ น่Š‚็‚น๏ผ‰ + Long menuParentId = menu.getParentId() == null ? 0L : menu.getParentId(); + + if (menuParentId.equals(parentId)) { + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + MenuVo vo = convertToVo(menu); + + // 3. ้€’ๅฝ’ๆŸฅๆ‰พๅญ่œๅ• + vo.setChildren(buildMenuTree(menus, menu.getId())); + + // 4. ๆทปๅŠ ๅˆฐ็ป“ๆžœๅˆ—่กจ + result.add(vo); + } + } + + return result; + } + + /** + * ๅฐ†่œๅ•ๅฎžไฝ“่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + * ๆญฅ้ชค๏ผš + * 1. ๅคๅˆถๅŸบๆœฌไฟกๆฏ + * 2. ๅคๅˆถ่œๅ•็ฑปๅž‹ๅ’Œ่ทฏๅพ„ + * 3. ๅคๅˆถๆ˜พ็คบๅ’Œ็Šถๆ€ไฟกๆฏ + * 4. ๅคๅˆถๆ—ถ้—ดไฟกๆฏ + * + * @param menu ่œๅ•ๅฎžไฝ“ + * @return ่œๅ•่ง†ๅ›พๅฏน่ฑก + */ + private MenuVo convertToVo(SysMenu menu) { + MenuVo vo = new MenuVo(); + // 1. ๅคๅˆถๅŸบๆœฌไฟกๆฏ + vo.setId(menu.getId()); + vo.setParentId(menu.getParentId()); + vo.setMenuName(menu.getMenuName()); + + // 2. ๅคๅˆถ่œๅ•็ฑปๅž‹ๅ’Œ่ทฏๅพ„ + vo.setMenuType(menu.getMenuType()); + vo.setMenuPath(menu.getMenuPath()); + vo.setComponent(menu.getComponent()); + vo.setIcon(menu.getIcon()); + + // 3. ๅคๅˆถๆŽ’ๅบๅ’Œ็Šถๆ€ไฟกๆฏ + vo.setSortOrder(menu.getSortOrder()); + vo.setVisible(menu.getVisible()); + vo.setStatus(menu.getStatus()); + vo.setPermissionCode(menu.getPermissionCode()); + + // 4. ๅคๅˆถๆ—ถ้—ดไฟกๆฏ + vo.setCreatedAt(menu.getCreatedAt()); + vo.setUpdatedAt(menu.getUpdatedAt()); + + return vo; + } +} diff --git a/src/main/java/com/aisi/template/service/impl/SysPermissionServiceImpl.java b/src/main/java/com/aisi/template/service/impl/SysPermissionServiceImpl.java new file mode 100644 index 0000000..5ef4344 --- /dev/null +++ b/src/main/java/com/aisi/template/service/impl/SysPermissionServiceImpl.java @@ -0,0 +1,157 @@ +package com.aisi.template.service.impl; + +import com.aisi.template.domain.entity.SysPermission; +import com.aisi.template.domain.vo.PermissionVo; +import com.aisi.template.exception.BusinessException; +import com.aisi.template.repository.SysPermissionRepository; +import com.aisi.template.service.SysPermissionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * ็ณป็ปŸๆƒ้™ๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃๆƒ้™็š„ๆŸฅ่ฏขๅ’Œ็ฎก็† + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ๆƒ้™ๆŸฅ่ฏข๏ผš่Žทๅ–ๆ‰€ๆœ‰ๆƒ้™ใ€ๆŒ‰IDๆŸฅ่ฏขใ€ๆŒ‰่ต„ๆบๆŸฅ่ฏขใ€ๆŒ‰ๆ“ไฝœๆŸฅ่ฏข + * 2. ๆƒ้™่ฝฌๆข๏ผšๅฐ†ๆƒ้™ๅฎžไฝ“่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + * + * ่ฎพ่ฎก่ฏดๆ˜Ž๏ผš + * - ๆƒ้™ๆ˜ฏ็ณป็ปŸ้ข„ๅฎšไน‰็š„๏ผŒ้€š่ฟ‡ๆ•ฐๆฎๅบ“่ฟ็งป่„šๆœฌๅˆๅง‹ๅŒ– + * - ไธๆ”ฏๆŒๅŠจๆ€ๅˆ›ๅปบ/ๅˆ ้™คๆƒ้™๏ผˆไฟ่ฏ็ณป็ปŸ็จณๅฎšๆ€ง๏ผ‰ + * - ๆƒ้™้€š่ฟ‡่ง’่‰ฒๅˆ†้…็ป™็”จๆˆท + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class SysPermissionServiceImpl implements SysPermissionService { + + /** + * ๆƒ้™ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final SysPermissionRepository permissionRepository; + + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰ๅฏ็”จ็Šถๆ€็š„ๆƒ้™ + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * 3. ๆŒ‰่ต„ๆบๅ’Œๆ“ไฝœๆŽ’ๅบ่ฟ”ๅ›ž + * + * @return ๆ‰€ๆœ‰ๆƒ้™่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + */ + @Override + public List getAllPermissions() { + // 1. ๆŸฅ่ฏขๆ‰€ๆœ‰ๅฏ็”จ็Šถๆ€็š„ๆƒ้™๏ผˆstatus = 1๏ผ‰ + List permissions = permissionRepository.findByStatus(1); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + return permissions.stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + } + + /** + * ๆ นๆฎID่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎIDๆŸฅ่ฏขๆƒ้™ + * 2. ๅฆ‚ๆžœไธๅญ˜ๅœจ๏ผŒๆŠ›ๅ‡บๅผ‚ๅธธ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param id ๆƒ้™ID + * @return ๆƒ้™่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“ๆƒ้™ไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + public PermissionVo getById(Long id) { + // 1. ๆŸฅ่ฏขๆƒ้™ + SysPermission permission = permissionRepository.findById(id) + .orElseThrow(() -> new BusinessException("ๆƒ้™ไธๅญ˜ๅœจ: " + id)); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + return convertToVo(permission); + } + + /** + * ๆ นๆฎ่ต„ๆบ่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŒ‰่ต„ๆบๅ็งฐๆŸฅ่ฏขๆƒ้™ + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * + * ็คบไพ‹๏ผš + * - resource="user" ่ฟ”ๅ›ž user:read, user:write, user:delete + * + * @param resource ่ต„ๆบๅ็งฐ๏ผˆๅฆ‚๏ผšuser, role, menu๏ผ‰ + * @return ่ฏฅ่ต„ๆบ็š„ๆ‰€ๆœ‰ๆƒ้™ + */ + @Override + public List getByResource(String resource) { + // 1. ๆŒ‰่ต„ๆบๆŸฅ่ฏขๆƒ้™ + List permissions = permissionRepository.findByResource(resource); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + return permissions.stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + } + + /** + * ๆ นๆฎๆ“ไฝœ็ฑปๅž‹่Žทๅ–ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŒ‰ๆ“ไฝœ็ฑปๅž‹ๆŸฅ่ฏขๆƒ้™ + * 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * + * ็คบไพ‹๏ผš + * - action="read" ่ฟ”ๅ›ž user:read, role:read, menu:read ็ญ‰ + * + * @param action ๆ“ไฝœ็ฑปๅž‹๏ผˆread, write, delete๏ผ‰ + * @return ๆ‹ฅๆœ‰่ฏฅๆ“ไฝœ็š„ๆ‰€ๆœ‰ๆƒ้™ + */ + @Override + public List getByAction(String action) { + // 1. ๆŒ‰ๆ“ไฝœ็ฑปๅž‹ๆŸฅ่ฏขๆƒ้™ + List permissions = permissionRepository.findByAction(action); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + return permissions.stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + } + + /** + * ๅฐ†ๆƒ้™ๅฎžไฝ“่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + * ๆญฅ้ชค๏ผš + * 1. ๅคๅˆถๅŸบๆœฌไฟกๆฏ๏ผˆIDใ€็ผ–็ ใ€ๅ็งฐ๏ผ‰ + * 2. ๅคๅˆถ่ต„ๆบๆ“ไฝœไฟกๆฏ + * 3. ๅคๅˆถๆ่ฟฐๅ’Œๆ—ถ้—ดไฟกๆฏ + * + * @param permission ๆƒ้™ๅฎžไฝ“ + * @return ๆƒ้™่ง†ๅ›พๅฏน่ฑก + */ + private PermissionVo convertToVo(SysPermission permission) { + PermissionVo vo = new PermissionVo(); + // 1. ๅคๅˆถๅŸบๆœฌไฟกๆฏ + vo.setId(permission.getId()); + vo.setPermissionCode(permission.getPermissionCode()); + vo.setPermissionName(permission.getPermissionName()); + + // 2. ๅคๅˆถ่ต„ๆบๆ“ไฝœไฟกๆฏ + vo.setResource(permission.getResource()); + vo.setAction(permission.getAction()); + + // 3. ๅคๅˆถๅ…ถไป–ไฟกๆฏ + vo.setDescription(permission.getDescription()); + vo.setStatus(permission.getStatus()); + vo.setCreatedAt(permission.getCreatedAt()); + vo.setUpdatedAt(permission.getUpdatedAt()); + + return vo; + } +} diff --git a/src/main/java/com/aisi/template/service/impl/SysRoleServiceImpl.java b/src/main/java/com/aisi/template/service/impl/SysRoleServiceImpl.java new file mode 100644 index 0000000..a69d026 --- /dev/null +++ b/src/main/java/com/aisi/template/service/impl/SysRoleServiceImpl.java @@ -0,0 +1,343 @@ +package com.aisi.template.service.impl; + +import com.aisi.template.domain.dto.RoleDto; +import com.aisi.template.domain.dto.RoleQueryDto; +import com.aisi.template.domain.entity.SysPermission; +import com.aisi.template.domain.entity.SysRole; +import com.aisi.template.domain.dto.PageResult; +import com.aisi.template.domain.vo.PermissionVo; +import com.aisi.template.domain.vo.RoleVo; +import com.aisi.template.exception.BusinessException; +import com.aisi.template.repository.SysPermissionRepository; +import com.aisi.template.repository.SysRoleRepository; +import com.aisi.template.service.SysRoleService; +import jakarta.persistence.criteria.Predicate; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * ็ณป็ปŸ่ง’่‰ฒๆœๅŠกๅฎž็Žฐ็ฑป + * ๆไพ›่ง’่‰ฒ็š„ CRUD ๆ“ไฝœใ€ๆƒ้™ๅˆ†้…ใ€ๆŸฅ่ฏข็ญ‰ๅŠŸ่ƒฝ + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่ง’่‰ฒๅŸบๆœฌๆ“ไฝœ๏ผšๅˆ›ๅปบใ€ๆ›ดๆ–ฐใ€ๅˆ ้™คใ€ๆŸฅ่ฏข + * 2. ๆƒ้™็ฎก็†๏ผšไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ใ€่Žทๅ–่ง’่‰ฒๆƒ้™ + * 3. ๅˆ†้กตๆŸฅ่ฏข๏ผšๆ”ฏๆŒๆกไปถๆŸฅ่ฏขๅ’Œๅˆ†้กต + * + * @author Claude + * @since 2024-04-09 + */ +@Service +@RequiredArgsConstructor +public class SysRoleServiceImpl implements SysRoleService { + + /** + * ่ง’่‰ฒๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ็”จไบŽ่ง’่‰ฒ็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + */ + private final SysRoleRepository roleRepository; + + /** + * ๆƒ้™ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + * ็”จไบŽๆƒ้™็š„ๆ•ฐๆฎๅบ“ๆ“ไฝœ + */ + private final SysPermissionRepository permissionRepository; + + /** + * ๅˆ›ๅปบ่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ่ง’่‰ฒ็ผ–็ ๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + * 2. ๆž„ๅปบ่ง’่‰ฒๅฎžไฝ“ๅฏน่ฑก + * 3. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * @param roleDto ่ง’่‰ฒๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * @return ๅˆ›ๅปบ็š„่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“่ง’่‰ฒ็ผ–็ ๅทฒๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public RoleVo create(RoleDto roleDto) { + // 1. ๆฃ€ๆŸฅ่ง’่‰ฒ็ผ–็ ๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + roleRepository.findByRoleCode(roleDto.getRoleCode()).ifPresent(role -> { + throw new BusinessException("่ง’่‰ฒ็ผ–็ ๅทฒๅญ˜ๅœจ: " + role.getRoleCode()); + }); + + // 2. ๆž„ๅปบ่ง’่‰ฒๅฎžไฝ“ + SysRole role = new SysRole(); + role.setRoleCode(roleDto.getRoleCode()); + role.setRoleName(roleDto.getRoleName()); + role.setDescription(roleDto.getDescription()); + role.setSortOrder(roleDto.getSortOrder()); + role.setStatus(roleDto.getStatus()); + + // 3. ไฟๅญ˜่ง’่‰ฒๅˆฐๆ•ฐๆฎๅบ“ + SysRole savedRole = roleRepository.save(role); + return convertToVo(savedRole); + } + + /** + * ๆ›ดๆ–ฐ่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๅฆ‚ๆžœไฟฎๆ”นไบ†่ง’่‰ฒ็ผ–็ ๏ผŒๆฃ€ๆŸฅๆ–ฐ็ผ–็ ๆ˜ฏๅฆๅทฒ่ขซไฝฟ็”จ + * 3. ๆ›ดๆ–ฐ่ง’่‰ฒไฟกๆฏ + * + * @param id ่ง’่‰ฒID + * @param roleDto ่ง’่‰ฒๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก + * @return ๆ›ดๆ–ฐๅŽ็š„่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“่ง’่‰ฒไธๅญ˜ๅœจๆˆ–็ผ–็ ๅ†ฒ็ชๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public RoleVo update(Long id, RoleDto roleDto) { + // 1. ๆŸฅ่ฏข่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + SysRole role = roleRepository.findById(id) + .orElseThrow(() -> new BusinessException("่ง’่‰ฒไธๅญ˜ๅœจ: " + id)); + + // 2. ๅฆ‚ๆžœไฟฎๆ”นไบ†่ง’่‰ฒ็ผ–็ ๏ผŒๆฃ€ๆŸฅๆ–ฐ็ผ–็ ๆ˜ฏๅฆๅทฒ่ขซไฝฟ็”จ + if (!role.getRoleCode().equals(roleDto.getRoleCode())) { + roleRepository.findByRoleCode(roleDto.getRoleCode()).ifPresent(r -> { + throw new BusinessException("่ง’่‰ฒ็ผ–็ ๅทฒๅญ˜ๅœจ: " + r.getRoleCode()); + }); + } + + // 3. ๆ›ดๆ–ฐ่ง’่‰ฒไฟกๆฏ + role.setRoleCode(roleDto.getRoleCode()); + role.setRoleName(roleDto.getRoleName()); + role.setDescription(roleDto.getDescription()); + role.setSortOrder(roleDto.getSortOrder()); + role.setStatus(roleDto.getStatus()); + + // 4. ไฟๅญ˜ๆ›ดๆ–ฐ + SysRole savedRole = roleRepository.save(role); + return convertToVo(savedRole); + } + + /** + * ๅˆ ้™ค่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๅˆ ้™ค่ง’่‰ฒ๏ผˆๅ…ณ่”็š„ๆƒ้™ๅ…ณ็ณปไผš่‡ชๅŠจ็บง่”ๅˆ ้™ค๏ผ‰ + * + * ๆณจๆ„๏ผšๅฆ‚ๆžœ่ง’่‰ฒๅทฒๅˆ†้…็ป™็”จๆˆท๏ผŒ้œ€่ฆๅ…ˆ่งฃ้™คๅ…ณ่” + * + * @param id ่ง’่‰ฒID + * @throws BusinessException ๅฝ“่ง’่‰ฒไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void delete(Long id) { + // 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + if (!roleRepository.existsById(id)) { + throw new BusinessException("่ง’่‰ฒไธๅญ˜ๅœจ: " + id); + } + + // 2. ๅˆ ้™ค่ง’่‰ฒ + roleRepository.deleteById(id); + } + + /** + * ๆ นๆฎID่Žทๅ–่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒๅŸบๆœฌไฟกๆฏ + * 2. ไฝฟ็”จ JOIN FETCH ไธ€ๆฌกๆ€งๅŠ ่ฝฝๅ…ณ่”็š„ๆƒ้™๏ผˆ้ฟๅ… N+1 ้—ฎ้ข˜๏ผ‰ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @param id ่ง’่‰ฒID + * @return ่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + * @throws BusinessException ๅฝ“่ง’่‰ฒไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + public RoleVo getById(Long id) { + // 1. ๆŸฅ่ฏข่ง’่‰ฒ๏ผˆไฝฟ็”จ JOIN FETCH ้ฟๅ…ๆ‡’ๅŠ ่ฝฝ้—ฎ้ข˜๏ผ‰ + SysRole role = roleRepository.findByIdWithPermissions(id) + .orElseThrow(() -> new BusinessException("่ง’่‰ฒไธๅญ˜ๅœจ: " + id)); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + return convertToVo(role); + } + + /** + * ่Žทๅ–ๆ‰€ๆœ‰่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่ง’่‰ฒ + * 2. ไฝฟ็”จ JOIN FETCH ้ข„ๅŠ ่ฝฝๆƒ้™ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + * + * @return ่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + */ + @Override + public List getAllRoles() { + // 1. ๆŸฅ่ฏขๆ‰€ๆœ‰่ง’่‰ฒ๏ผˆไฝฟ็”จ JOIN FETCH ้ฟๅ…ๆ‡’ๅŠ ่ฝฝ้—ฎ้ข˜๏ผ‰ + List roles = roleRepository.findAllWithPermissions(); + + // 2. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ + return roles.stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + } + + /** + * ๅˆ†้กตๆŸฅ่ฏข่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏขๆกไปถ๏ผˆๆ”ฏๆŒ่ง’่‰ฒ็ผ–็ ใ€ๅ็งฐใ€็Šถๆ€๏ผ‰ + * 2. ๆ‰ง่กŒๅˆ†้กตๆŸฅ่ฏข + * 3. ่ฝฌๆข็ป“ๆžœไธบ่ง†ๅ›พๅฏน่ฑก + * + * @param queryDto ๆŸฅ่ฏขๆกไปถๅฏน่ฑก + * @param page ้กต็ ๏ผˆไปŽ 0 ๅผ€ๅง‹๏ผ‰ + * @param size ๆฏ้กตๅคงๅฐ + * @return ๅˆ†้กต็ป“ๆžœ + */ + @Override + public PageResult queryRoles(RoleQueryDto queryDto, int page, int size) { + // 1. ๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏขๆกไปถ + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + // 1.1 ่ง’่‰ฒ็ผ–็ ๆจก็ณŠๆŸฅ่ฏข + if (queryDto.getRoleCode() != null && !queryDto.getRoleCode().isEmpty()) { + predicates.add(cb.like(root.get("roleCode"), "%" + queryDto.getRoleCode() + "%")); + } + + // 1.2 ่ง’่‰ฒๅ็งฐๆจก็ณŠๆŸฅ่ฏข + if (queryDto.getRoleName() != null && !queryDto.getRoleName().isEmpty()) { + predicates.add(cb.like(root.get("roleName"), "%" + queryDto.getRoleName() + "%")); + } + + // 1.3 ็Šถๆ€็ฒพ็กฎๆŸฅ่ฏข + if (queryDto.getStatus() != null) { + predicates.add(cb.equal(root.get("status"), queryDto.getStatus())); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + // 2. ๆ‰ง่กŒๅˆ†้กตๆŸฅ่ฏข + Page rolePage = roleRepository.findAll(spec, PageRequest.of(page, size)); + + // 3. ่ฝฌๆขไธบๅˆ†้กต็ป“ๆžœ + return PageResult.of( + rolePage.getContent().stream().map(this::convertToVo).collect(Collectors.toList()), + rolePage.getTotalElements(), + rolePage.getNumber(), + rolePage.getSize() + ); + } + + /** + * ไธบ่ง’่‰ฒๅˆ†้…ๆƒ้™ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆ นๆฎๆƒ้™IDๅˆ—่กจๆŸฅ่ฏขๆ‰€ๆœ‰ๆƒ้™ + * 3. ๆธ…็ฉบ่ง’่‰ฒๅŽŸๆœ‰็š„ๆƒ้™ + * 4. ๆทปๅŠ ๆ–ฐ็š„ๆƒ้™ + * 5. ไฟๅญ˜ๆ›ดๆ–ฐ + * + * @param roleId ่ง’่‰ฒID + * @param permissionIds ๆƒ้™IDๅˆ—่กจ + * @throws BusinessException ๅฝ“่ง’่‰ฒๆˆ–ๆƒ้™ไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void assignPermissions(Long roleId, List permissionIds) { + // 1. ๆฃ€ๆŸฅ่ง’่‰ฒๆ˜ฏๅฆๅญ˜ๅœจ + SysRole role = roleRepository.findById(roleId) + .orElseThrow(() -> new BusinessException("่ง’่‰ฒไธๅญ˜ๅœจ: " + roleId)); + + // 2. ๆŸฅ่ฏขๆ‰€ๆœ‰ๆƒ้™๏ผˆๅฆ‚ๆžœๆƒ้™ไธๅญ˜ๅœจไผšๆŠ›ๅ‡บๅผ‚ๅธธ๏ผ‰ + Set permissions = permissionIds.stream() + .map(permissionId -> permissionRepository.findById(permissionId) + .orElseThrow(() -> new BusinessException("ๆƒ้™ไธๅญ˜ๅœจ: " + permissionId))) + .collect(Collectors.toSet()); + + // 3. ๆธ…็ฉบๅŽŸๆœ‰ๆƒ้™ๅนถๆทปๅŠ ๆ–ฐๆƒ้™ + role.getPermissions().clear(); + role.getPermissions().addAll(permissions); + + // 4. ไฟๅญ˜ๆ›ดๆ–ฐ + roleRepository.save(role); + } + + /** + * ่Žทๅ–่ง’่‰ฒ็š„ๆƒ้™IDๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข่ง’่‰ฒ๏ผˆไฝฟ็”จ JOIN FETCH ๅŠ ่ฝฝๆƒ้™๏ผ‰ + * 2. ๆๅ–ๆ‰€ๆœ‰ๆƒ้™็š„ID + * + * @param roleId ่ง’่‰ฒID + * @return ๆƒ้™IDๅˆ—่กจ + * @throws BusinessException ๅฝ“่ง’่‰ฒไธๅญ˜ๅœจๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + public List getRolePermissionIds(Long roleId) { + // 1. ๆŸฅ่ฏข่ง’่‰ฒๅŠๆƒ้™ + SysRole role = roleRepository.findByIdWithPermissions(roleId) + .orElseThrow(() -> new BusinessException("่ง’่‰ฒไธๅญ˜ๅœจ: " + roleId)); + + // 2. ๆๅ–ๆƒ้™IDๅˆ—่กจ + return role.getPermissions().stream() + .map(SysPermission::getId) + .collect(Collectors.toList()); + } + + /** + * ๅฐ†่ง’่‰ฒๅฎžไฝ“่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + * ๆญฅ้ชค๏ผš + * 1. ๅคๅˆถๅŸบๆœฌไฟกๆฏ + * 2. ่ฝฌๆขๆƒ้™ๅˆ—่กจ + * + * @param role ่ง’่‰ฒๅฎžไฝ“ + * @return ่ง’่‰ฒ่ง†ๅ›พๅฏน่ฑก + */ + private RoleVo convertToVo(SysRole role) { + RoleVo vo = new RoleVo(); + vo.setId(role.getId()); + vo.setRoleCode(role.getRoleCode()); + vo.setRoleName(role.getRoleName()); + vo.setDescription(role.getDescription()); + vo.setSortOrder(role.getSortOrder()); + vo.setStatus(role.getStatus()); + vo.setCreatedAt(role.getCreatedAt()); + vo.setUpdatedAt(role.getUpdatedAt()); + + // 1. ๅฆ‚ๆžœๆœ‰ๆƒ้™๏ผŒ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + if (role.getPermissions() != null && !role.getPermissions().isEmpty()) { + Set permissionVos = role.getPermissions().stream() + .map(this::convertToPermissionVo) + .collect(Collectors.toSet()); + vo.setPermissions(permissionVos); + } + + return vo; + } + + /** + * ๅฐ†ๆƒ้™ๅฎžไฝ“่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + * + * @param permission ๆƒ้™ๅฎžไฝ“ + * @return ๆƒ้™่ง†ๅ›พๅฏน่ฑก + */ + private PermissionVo convertToPermissionVo(SysPermission permission) { + PermissionVo vo = new PermissionVo(); + vo.setId(permission.getId()); + vo.setPermissionCode(permission.getPermissionCode()); + vo.setPermissionName(permission.getPermissionName()); + vo.setResource(permission.getResource()); + vo.setAction(permission.getAction()); + vo.setDescription(permission.getDescription()); + vo.setStatus(permission.getStatus()); + vo.setCreatedAt(permission.getCreatedAt()); + vo.setUpdatedAt(permission.getUpdatedAt()); + return vo; + } +} diff --git a/src/main/java/com/aisi/template/service/impl/TokenServiceImpl.java b/src/main/java/com/aisi/template/service/impl/TokenServiceImpl.java new file mode 100644 index 0000000..f62a2c5 --- /dev/null +++ b/src/main/java/com/aisi/template/service/impl/TokenServiceImpl.java @@ -0,0 +1,363 @@ +package com.aisi.template.service.impl; + +import com.aisi.template.domain.entity.RefreshToken; +import com.aisi.template.domain.entity.User; +import com.aisi.template.domain.vo.LoginResponseVo; +import com.aisi.template.exception.BusinessException; +import com.aisi.template.repository.RefreshTokenRepository; +import com.aisi.template.repository.UserRepository; +import com.aisi.template.service.TokenService; +import com.aisi.template.utils.JwtUtil; +import com.aisi.template.utils.RedisUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.util.HexFormat; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Token ๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃ Refresh Token ็š„็”Ÿๆˆใ€้ชŒ่ฏใ€ๅˆทๆ–ฐๅ’Œ้ป‘ๅๅ•็ฎก็† + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. Refresh Token ็ฎก็†๏ผš็”Ÿๆˆใ€ๅญ˜ๅ‚จใ€้ชŒ่ฏใ€ๆ’ค้”€ + * 2. Token ้ป‘ๅๅ•๏ผšๅฐ†ๅคฑๆ•ˆ็š„ JWT ๅŠ ๅ…ฅ Redis ้ป‘ๅๅ• + * 3. Token ๅˆทๆ–ฐ๏ผšไฝฟ็”จ Refresh Token ่Žทๅ–ๆ–ฐ็š„ Access Token + * 4. ๆธ…็†ไปปๅŠก๏ผšๆธ…็†่ฟ‡ๆœŸ็š„ Token ๅ’Œ้ป‘ๅๅ•่ฎฐๅฝ• + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenServiceImpl implements TokenService { + + /** + * Refresh Token ๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final RefreshTokenRepository refreshTokenRepository; + + /** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final UserRepository userRepository; + + /** + * JWT ๅทฅๅ…ท็ฑป + */ + private final JwtUtil jwtUtil; + + /** + * Redis ๅทฅๅ…ท็ฑป + */ + private final RedisUtils redisUtils; + + /** + * Refresh Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰๏ผŒไปŽ้…็ฝฎ่ฏปๅ–๏ผŒ้ป˜่ฎค 7 ๅคฉ + */ + @Value("${jwt.refresh-token-expiration:604800}") + private long refreshTokenExpirationSeconds; + + /** + * Access Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰๏ผŒไปŽ้…็ฝฎ่ฏปๅ–๏ผŒ้ป˜่ฎค 1 ๅฐๆ—ถ + */ + @Value("${jwt.access-token-expiration:3600}") + private long accessTokenExpirationSeconds; + + /** + * Token ้ป‘ๅๅ• Redis ้”ฎๅ‰็ผ€ + */ + private static final String BLACKLIST_KEY_PREFIX = "token:blacklist:"; + + /** + * Refresh Token Redis ้”ฎๅ‰็ผ€ + */ + private static final String REFRESH_TOKEN_PREFIX = "refresh_token:"; + + /** + * ๅˆ›ๅปบ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆ้šๆœบ UUID ไฝœไธบ Token + * 2. ๅฏน Token ่ฟ›่กŒ SHA-256 ๅ“ˆๅธŒ๏ผˆๆ•ฐๆฎๅบ“ๅชๅญ˜ๅ‚จๅ“ˆๅธŒๅ€ผ๏ผ‰ + * 3. ่ฎก็ฎ— Token ่ฟ‡ๆœŸๆ—ถ้—ด + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * 5. ๅŒๆ—ถๅœจ Redis ไธญ็ผ“ๅญ˜ไธ€ไปฝ๏ผˆ็”จไบŽๅฟซ้€ŸๆŸฅๆ‰พ๏ผ‰ + * + * @param userId ็”จๆˆทID + * @param deviceInfo ่ฎพๅค‡ไฟกๆฏ๏ผˆๅฆ‚๏ผšiPhone 14 Pro๏ผ‰ + * @param ipAddress IPๅœฐๅ€ + * @return Refresh Token ๅŽŸๅง‹ๅ€ผ๏ผˆๆœชๅ“ˆๅธŒ๏ผ‰ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public String createRefreshToken(Long userId, String deviceInfo, String ipAddress) { + // 1. ็”Ÿๆˆ้šๆœบ็š„ Refresh Token + String refreshToken = UUID.randomUUID().toString().replace("-", ""); + String tokenHash = hashToken(refreshToken); + + // 2. ่ฎก็ฎ—่ฟ‡ๆœŸๆ—ถ้—ด + LocalDateTime expiresAt = LocalDateTime.now().plusSeconds(refreshTokenExpirationSeconds); + + // 3. ๅˆ›ๅปบ Refresh Token ๅฎžไฝ“ + RefreshToken tokenEntity = new RefreshToken(); + tokenEntity.setUserId(userId); + tokenEntity.setTokenHash(tokenHash); + tokenEntity.setDeviceInfo(deviceInfo); + tokenEntity.setIpAddress(ipAddress); + tokenEntity.setExpiresAt(expiresAt); + tokenEntity.setRevoked(false); + + // 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + refreshTokenRepository.save(tokenEntity); + + // 5. ๅœจ Redis ไธญ็ผ“ๅญ˜ไธ€ไปฝ๏ผŒ่ฟ‡ๆœŸๆ—ถ้—ดไธŽ Refresh Token ไฟๆŒไธ€่‡ด + // ไฝœ็”จ๏ผšๅฟซ้€Ÿ้ชŒ่ฏ Refresh Token๏ผŒๅ‡ๅฐ‘ๆ•ฐๆฎๅบ“ๆŸฅ่ฏข + redisUtils.set(REFRESH_TOKEN_PREFIX + tokenHash, String.valueOf(userId), + refreshTokenExpirationSeconds, TimeUnit.SECONDS); + + log.info("Refresh Token ๅˆ›ๅปบๆˆๅŠŸ - userId: {}, deviceInfo: {}", userId, deviceInfo); + return refreshToken; + } + + /** + * ไฝฟ็”จ Refresh Token ๅˆทๆ–ฐ Access Token + * ๆญฅ้ชค๏ผš + * 1. ๅฏน Refresh Token ่ฟ›่กŒๅ“ˆๅธŒ + * 2. ๅ…ˆไปŽ Redis ๆŸฅๆ‰พ๏ผŒๆ‰พไธๅˆฐๅ†ๆŸฅๆ•ฐๆฎๅบ“ + * 3. ้ชŒ่ฏ Token ๆ˜ฏๅฆๆœ‰ๆ•ˆ๏ผˆๆœช่ฟ‡ๆœŸใ€ๆœชๆ’ค้”€๏ผ‰ + * 4. ่Žทๅ–็”จๆˆทไฟกๆฏๅนถ้ชŒ่ฏ็”จๆˆท็Šถๆ€ + * 5. ็”Ÿๆˆๆ–ฐ็š„ Access Token + * 6. ็”Ÿๆˆๆ–ฐ็š„ Refresh Token๏ผˆToken ่ฝฎๆขๆœบๅˆถ๏ผ‰ + * 7. ๆ’ค้”€ๆ—ง็š„ Refresh Token + * + * @param refreshToken Refresh Token ๅŽŸๅง‹ๅ€ผ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซๆ–ฐ็š„ Access Token ๅ’Œ Refresh Token๏ผ‰ + * @throws BusinessException ๅฝ“ Token ๆ— ๆ•ˆๆˆ–็”จๆˆท็Šถๆ€ๅผ‚ๅธธๆ—ถๆŠ›ๅ‡บๅผ‚ๅธธ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public LoginResponseVo refreshToken(String refreshToken) { + // 1. ่ฎก็ฎ— Token ๅ“ˆๅธŒๅ€ผ + String tokenHash = hashToken(refreshToken); + + // 2. ๅ…ˆไปŽ Redis ๆŸฅๆ‰พ๏ผˆๅฟซ้€Ÿ่ทฏๅพ„๏ผ‰ + String cachedUserId = redisUtils.get(REFRESH_TOKEN_PREFIX + tokenHash, String.class); + if (cachedUserId == null) { + // 2.1 Redis ๆฒกๆ‰พๅˆฐ๏ผŒๆŸฅๆ•ฐๆฎๅบ“๏ผˆๆ…ข้€Ÿ่ทฏๅพ„๏ผ‰ + RefreshToken tokenEntity = refreshTokenRepository.findByTokenHash(tokenHash) + .orElse(null); + if (tokenEntity == null) { + throw new BusinessException("Refresh Token ๆ— ๆ•ˆ"); + } + if (!tokenEntity.isValid()) { + throw new BusinessException("Refresh Token ๅทฒ่ฟ‡ๆœŸๆˆ–ๅทฒๆ’ค้”€"); + } + } + + // 3. ่Žทๅ– Token ๅฎžไฝ“๏ผˆไปŽๆ•ฐๆฎๅบ“๏ผ‰ + RefreshToken tokenEntity = refreshTokenRepository.findByTokenHash(tokenHash) + .orElseThrow(() -> new BusinessException("Refresh Token ๆ— ๆ•ˆ")); + + // 4. ้ชŒ่ฏ Token ๆ˜ฏๅฆๆœ‰ๆ•ˆ + if (!tokenEntity.isValid()) { + throw new BusinessException("Refresh Token ๅทฒ่ฟ‡ๆœŸๆˆ–ๅทฒๆ’ค้”€"); + } + + // 5. ่Žทๅ–็”จๆˆทไฟกๆฏ + User user = userRepository.findById(tokenEntity.getUserId()) + .orElseThrow(() -> new BusinessException("็”จๆˆทไธๅญ˜ๅœจ")); + + // 6. ๆฃ€ๆŸฅ็”จๆˆท็Šถๆ€ + if (!user.isEnabled()) { + throw new BusinessException("็”จๆˆท่ดฆๆˆทๅทฒ่ขซ็ฆ็”จ"); + } + + // 7. ็”Ÿๆˆๆ–ฐ็š„ Access Token + String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername()); + + // 8. ็”Ÿๆˆๆ–ฐ็š„ Refresh Token๏ผˆToken ่ฝฎๆข๏ผ‰ + // Token ่ฝฎๆข๏ผšๆฏๆฌกๅˆทๆ–ฐ้ƒฝ็”Ÿๆˆๆ–ฐ็š„ Refresh Token๏ผŒๆ—ง Token ๅคฑๆ•ˆ + // ๅฅฝๅค„๏ผšๆ้ซ˜ๅฎ‰ๅ…จๆ€ง๏ผŒไธ€ๆ—ฆ Token ๆณ„้œฒ๏ผŒไฝฟ็”จๅŽๅณๅคฑๆ•ˆ + revokeRefreshToken(tokenHash); + String newRefreshToken = createRefreshToken( + user.getId(), + tokenEntity.getDeviceInfo(), + tokenEntity.getIpAddress() + ); + + // 9. ๆž„ๅปบๅ“ๅบ” + LoginResponseVo response = LoginResponseVo.builder() + .accessToken(accessToken) + .refreshToken(newRefreshToken) + .expiresIn(accessTokenExpirationSeconds) + .userInfo(LoginResponseVo.UserInfoVo.builder() + .id(user.getId()) + .username(user.getUsername()) + .email(user.getEmail()) + .build()) + .build(); + + log.info("Token ๅˆทๆ–ฐๆˆๅŠŸ - userId: {}", user.getId()); + return response; + } + + /** + * ๆ’ค้”€ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅๆ‰พ Token ๅฎžไฝ“ + * 2. ๆ ‡่ฎฐไธบๅทฒๆ’ค้”€ + * 3. ่ฎฐๅฝ•ๆ’ค้”€ๆ—ถ้—ด + * 4. ๅˆ ้™ค Redis ็ผ“ๅญ˜ + * + * @param tokenHash Token ๅ“ˆๅธŒๅ€ผ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void revokeRefreshToken(String tokenHash) { + // 1. ๆŸฅๆ‰พ Token ๅฎžไฝ“ + RefreshToken token = refreshTokenRepository.findByTokenHash(tokenHash) + .orElse(null); + if (token != null) { + // 2. ๆ ‡่ฎฐไธบๅทฒๆ’ค้”€ + token.setRevoked(true); + token.setRevokedAt(LocalDateTime.now()); + refreshTokenRepository.save(token); + + // 3. ๅˆ ้™ค Redis ็ผ“ๅญ˜ + redisUtils.delete(REFRESH_TOKEN_PREFIX + tokenHash); + + log.info("Refresh Token ๅทฒๆ’ค้”€ - tokenHash: {}", tokenHash); + } + } + + /** + * ๆ’ค้”€็”จๆˆท็š„ๆ‰€ๆœ‰ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ๅฐ†็”จๆˆทๆ‰€ๆœ‰ๆœ‰ๆ•ˆ Token ๆ ‡่ฎฐไธบๅทฒๆ’ค้”€ + * 2. ๅˆ ้™คๅฏนๅบ”็š„ Redis ็ผ“ๅญ˜ + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆทไฟฎๆ”นๅฏ†็ ๅŽ๏ผŒๆ’ค้”€ๆ‰€ๆœ‰ Token + * - ็”จๆˆท็™ปๅ‡บๆ‰€ๆœ‰่ฎพๅค‡ + * - ็”จๆˆท่ดฆๆˆท่ขซ็ฆ็”จ + * + * @param userId ็”จๆˆทID + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void revokeAllUserTokens(Long userId) { + // 1. ๆŸฅๆ‰พ็”จๆˆทๆ‰€ๆœ‰ๆœ‰ๆ•ˆ Token๏ผŒ็”จไบŽๅˆ ้™ค Redis ็ผ“ๅญ˜ + var tokens = refreshTokenRepository.findValidTokensByUserId(userId, LocalDateTime.now()); + + // 2. ๆ’ค้”€ๆ•ฐๆฎๅบ“ไธญๆ‰€ๆœ‰ Token + refreshTokenRepository.revokeAllUserTokens(userId, LocalDateTime.now()); + + // 3. ๅˆ ้™ค Redis ็ผ“ๅญ˜ + tokens.forEach(token -> redisUtils.delete(REFRESH_TOKEN_PREFIX + token.getTokenHash())); + + log.info("็”จๆˆทๆ‰€ๆœ‰ Refresh Token ๅทฒๆ’ค้”€ - userId: {}", userId); + } + + /** + * ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบ Redis ้ป‘ๅๅ•้”ฎ + * 2. ๆฃ€ๆŸฅ้”ฎๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param jti JWT ID๏ผˆJWT ๅ”ฏไธ€ๆ ‡่ฏ†๏ผ‰ + * @return ๆ˜ฏๅฆๅœจ้ป‘ๅๅ•ไธญ + */ + @Override + public boolean isTokenBlacklisted(String jti) { + String key = BLACKLIST_KEY_PREFIX + jti; + return Boolean.TRUE.equals(redisUtils.hasKey(key)); + } + + /** + * ๅฐ† Token ๅŠ ๅ…ฅ้ป‘ๅๅ• + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบ Redis ้ป‘ๅๅ•้”ฎ + * 2. ่ฎพ็ฝฎ้ป‘ๅๅ•่ฎฐๅฝ•๏ผŒ่ฟ‡ๆœŸๆ—ถ้—ดไธŽ Token ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆœŸไธ€่‡ด + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆท็™ปๅ‡บๆ—ถ๏ผŒๅฐ†ๅฝ“ๅ‰ Token ๅŠ ๅ…ฅ้ป‘ๅๅ• + * - Token ๅˆทๆ–ฐๆ—ถ๏ผŒๅฐ†ๆ—ง Token ๅŠ ๅ…ฅ้ป‘ๅๅ• + * + * @param jti JWT ID + * @param expirationSeconds ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + */ + @Override + public void addTokenToBlacklist(String jti, long expirationSeconds) { + String key = BLACKLIST_KEY_PREFIX + jti; + // ่ฎพ็ฝฎ้ป‘ๅๅ•่ฎฐๅฝ•๏ผŒ่ฟ‡ๆœŸๆ—ถ้—ดไธŽ Token ๅ‰ฉไฝ™ๆœ‰ๆ•ˆๆœŸไธ€่‡ด + // ไฝœ็”จ๏ผš่‡ชๅŠจๆธ…็†่ฟ‡ๆœŸ็š„้ป‘ๅๅ•่ฎฐๅฝ•๏ผŒ้ฟๅ… Redis ๅ†…ๅญ˜ๆบขๅ‡บ + redisUtils.set(key, "1", expirationSeconds, TimeUnit.SECONDS); + log.info("Token ๅทฒๅŠ ๅ…ฅ้ป‘ๅๅ• - jti: {}, expire: {}s", jti, expirationSeconds); + } + + /** + * ๆธ…็†่ฟ‡ๆœŸ็š„ Refresh Token + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅๆ‰พๆ‰€ๆœ‰่ฟ‡ๆœŸ็š„ Token + * 2. ๆ‰น้‡ๅˆ ้™ค + * + * ๅปบ่ฎฎ้€š่ฟ‡ๅฎšๆ—ถไปปๅŠกๅฎšๆœŸๆ‰ง่กŒ๏ผˆๅฆ‚ๆฏๅคฉๅ‡Œๆ™จ๏ผ‰ + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void cleanupExpiredTokens() { + LocalDateTime now = LocalDateTime.now(); + refreshTokenRepository.deleteByExpiresAtBefore(now); + log.info("่ฟ‡ๆœŸ Refresh Token ๆธ…็†ๅฎŒๆˆ"); + } + + /** + * ๆธ…็†่ฟ‡ๆœŸ็š„้ป‘ๅๅ•่ฎฐๅฝ• + * ่ฏดๆ˜Ž๏ผš + * Redis ็š„้ป‘ๅๅ•่ฎฐๅฝ•่ฎพ็ฝฎไบ†่ฟ‡ๆœŸๆ—ถ้—ด๏ผŒไผš่‡ชๅŠจๅˆ ้™ค + * ๆญคๆ–นๆณ•ไป…ไฝœไธบๆ—ฅๅฟ—่ฎฐๅฝ•๏ผŒๅฎž้™…ๆ— ้œ€ๆ‰ง่กŒ + */ + @Override + public void cleanupExpiredBlacklistEntries() { + log.debug("้ป‘ๅๅ•่ฎฐๅฝ•็”ฑ Redis ่‡ชๅŠจ่ฟ‡ๆœŸๆธ…็†"); + } + + /** + * ๅฏน Token ่ฟ›่กŒ SHA-256 ๅ“ˆๅธŒ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ– SHA-256 ็ฎ—ๆณ•ๅฎžไพ‹ + * 2. ๅฏน Token ๅญ—่Š‚่ฟ›่กŒๅ“ˆๅธŒ + * 3. ่ฝฌๆขไธบๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ + * + * ๅฎ‰ๅ…จ่ฏดๆ˜Ž๏ผš + * - ๆ•ฐๆฎๅบ“ๅชๅญ˜ๅ‚จๅ“ˆๅธŒๅ€ผ๏ผŒไธๅญ˜ๅ‚จๅŽŸๅง‹ Token + * - ๅณไฝฟๆ•ฐๆฎๅบ“ๆณ„้œฒ๏ผŒๆ”ปๅ‡ป่€…ไนŸๆ— ๆณ•่Žทๅพ—ๆœ‰ๆ•ˆ็š„ Refresh Token + * + * @param token ๅŽŸๅง‹ Token + * @return ๅ“ˆๅธŒๅŽ็š„ Token๏ผˆๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ๏ผ‰ + */ + private String hashToken(String token) { + try { + // 1. ่Žทๅ– SHA-256 ็ฎ—ๆณ•ๅฎžไพ‹ + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + // 2. ๅฏน Token ่ฟ›่กŒๅ“ˆๅธŒ + byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8)); + + // 3. ่ฝฌๆขไธบๅๅ…ญ่ฟ›ๅˆถๅญ—็ฌฆไธฒ + return HexFormat.of().formatHex(hash); + } catch (NoSuchAlgorithmException e) { + // 4. SHA-256 ๆ˜ฏ Java ๆ ‡ๅ‡†ๅบ“ๅฟ…้กปๆ”ฏๆŒ็š„็ฎ—ๆณ•๏ผŒ็†่ฎบไธŠไธไผšๆŠ›ๅ‡บๆญคๅผ‚ๅธธ + throw new RuntimeException("SHA-256 ็ฎ—ๆณ•ไธๅฏ็”จ", e); + } + } +} diff --git a/src/main/java/com/aisi/template/service/impl/UserServiceImpl.java b/src/main/java/com/aisi/template/service/impl/UserServiceImpl.java index 65e6fec..12dbcbe 100644 --- a/src/main/java/com/aisi/template/service/impl/UserServiceImpl.java +++ b/src/main/java/com/aisi/template/service/impl/UserServiceImpl.java @@ -7,13 +7,18 @@ import com.aisi.template.domain.dto.UserDto; import com.aisi.template.domain.dto.UserQueryDto; import com.aisi.template.domain.dto.UserRoleUpdateDto; import com.aisi.template.domain.dto.UserStatusUpdateDto; +import com.aisi.template.domain.entity.SysRole; import com.aisi.template.domain.entity.User; -import com.aisi.template.domain.enums.Role; +import com.aisi.template.domain.vo.LoginResponseVo; import com.aisi.template.domain.vo.UserVo; +import com.aisi.template.repository.SysRoleRepository; import com.aisi.template.repository.UserRepository; +import com.aisi.template.service.TokenService; import com.aisi.template.service.UserService; import com.aisi.template.utils.JwtUtil; +import com.aisi.template.service.LoginAttemptService; import com.aisi.template.utils.SecurityUtils; +import com.aisi.template.mq.producer.UserMessageProducer; import jakarta.persistence.criteria.Predicate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,108 +29,303 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.Specification; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.stream.Collectors; +/** + * ็”จๆˆทๆœๅŠกๅฎž็Žฐ็ฑป + * ่ดŸ่ดฃ็”จๆˆท็›ธๅ…ณ็š„ไธšๅŠก้€ป่พ‘ๅค„็† + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ็”จๆˆทๆณจๅ†Œ๏ผšๆ ก้ชŒใ€ๅˆ›ๅปบใ€ๅˆ†้…้ป˜่ฎค่ง’่‰ฒใ€็”Ÿๆˆ Token + * 2. ็”จๆˆท็™ปๅฝ•๏ผš้ชŒ่ฏใ€็Šถๆ€ๆฃ€ๆŸฅใ€ๅคฑ่ดฅ่ฎกๆ•ฐใ€็”Ÿๆˆ Token + * 3. ็”จๆˆทไฟกๆฏ๏ผš่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * 4. ็”จๆˆท็ฎก็†๏ผšๅˆ†้กตๆŸฅ่ฏขใ€็Šถๆ€ๆ›ดๆ–ฐใ€่ง’่‰ฒๅˆ†้… + * + * ไบ‹ๅŠก่ฏดๆ˜Ž๏ผš + * - ๆณจๅ†Œๆ“ไฝœ๏ผš้œ€่ฆไบ‹ๅŠกไฟ่ฏๆ•ฐๆฎไธ€่‡ดๆ€ง + * - ็Šถๆ€ๆ›ดๆ–ฐ๏ผš้œ€่ฆไบ‹ๅŠกไฟ่ฏๆ•ฐๆฎไธ€่‡ดๆ€ง + * - ่ง’่‰ฒๆ›ดๆ–ฐ๏ผš้œ€่ฆไบ‹ๅŠกไฟ่ฏๆ•ฐๆฎไธ€่‡ดๆ€ง + * + * @author Claude + * @since 2024-04-09 + */ @Slf4j @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { + /** + * ๅฏ†็ ็ผ–็ ๅ™จ + * ไฝฟ็”จ BCrypt ็ฎ—ๆณ•ๅฏนๅฏ†็ ่ฟ›่กŒๅŠ ๅฏ† + */ private final PasswordEncoder passwordEncoder; + + /** + * JWT ๅทฅๅ…ท็ฑป + * ็”จไบŽ็”Ÿๆˆๅ’Œ้ชŒ่ฏ JWT Token + */ private final JwtUtil jwtUtil; + + /** + * ็”จๆˆทๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ private final UserRepository userRepository; + + /** + * ่ง’่‰ฒๆ•ฐๆฎ่ฎฟ้—ฎๆŽฅๅฃ + */ + private final SysRoleRepository roleRepository; + + /** + * Token ๆœๅŠก + * ๅค„็† Refresh Token ๅ’Œ้ป‘ๅๅ• + */ + private final TokenService tokenService; + + /** + * Security ๅทฅๅ…ท็ฑป + * ่Žทๅ–ๅฝ“ๅ‰็™ปๅฝ•็”จๆˆทไฟกๆฏ + */ private final SecurityUtils securityUtils; + /** + * ็™ปๅฝ•ๅฐ่ฏ•ๆœๅŠก + * ๅค„็†็™ปๅฝ•ๅคฑ่ดฅ่ฎกๆ•ฐๅ’Œ่ดฆๆˆท้”ๅฎš + */ + private final LoginAttemptService loginAttemptService; + + /** + * ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ SecurityContext ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทๅ + * 2. ไฝฟ็”จ JOIN FETCH ไธ€ๆฌกๆ€งๅŠ ่ฝฝ็”จๆˆทๅŠๅ…ถ่ง’่‰ฒ + * 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก่ฟ”ๅ›ž + * + * @return ็”จๆˆทไฟกๆฏ่ง†ๅ›พๅฏน่ฑก + */ @Override public RestBean getUserInfo() { + // 1. ่Žทๅ–ๅฝ“ๅ‰็™ปๅฝ•็”จๆˆทๅ String username = SecurityUtils.getUsername(); - User user = userRepository.findByUsername(username) + + // 2. ๆŸฅ่ฏข็”จๆˆท๏ผˆไฝฟ็”จ JOIN FETCH ้ฟๅ…ๆ‡’ๅŠ ่ฝฝ้—ฎ้ข˜๏ผ‰ + User user = userRepository.findByUsernameWithRoles(username) .orElse(null); + + // 3. ็”จๆˆทไธๅญ˜ๅœจๅˆ™่ฟ”ๅ›ž้”™่ฏฏ if (user == null) { return RestBean.failure(RestCode.DATA_NOT_FOUND); } - // ่ฝฌๆขไธบ UserVo ่ฟ”ๅ›ž - UserVo userVo = new UserVo(); - userVo.setId(user.getId()); - userVo.setUsername(user.getUsername()); - userVo.setEmail(user.getEmail()); - userVo.setStatus(user.getStatus()); - userVo.setRole(user.getRole().name()); - userVo.setCreatedAt(user.getCreatedAt()); - userVo.setUpdatedAt(user.getUpdatedAt()); + + // 4. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + UserVo userVo = convertToVo(user); return RestBean.success(userVo); } + /** + * ็”จๆˆทๆณจๅ†Œ + * ๆญฅ้ชค๏ผš + * 1. ๆ ก้ชŒ็”จๆˆทๅๆ˜ฏๅฆๅทฒๅญ˜ๅœจ + * 2. ๆ ก้ชŒ้‚ฎ็ฎฑๆ˜ฏๅฆๅทฒๅญ˜ๅœจ๏ผˆไธๅŒบๅˆ†ๅคงๅฐๅ†™๏ผ‰ + * 3. ๅˆ›ๅปบ็”จๆˆทๅฎžไฝ“ + * 4. ๅˆ†้…้ป˜่ฎค่ง’่‰ฒ๏ผˆROLE_USER๏ผ‰ + * 5. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * 6. ็”Ÿๆˆ Access Token ๅ’Œ Refresh Token + * + * ๆณจๆ„๏ผš + * - ๅฏ†็ ไฝฟ็”จ BCrypt ๅŠ ๅฏ†ๅญ˜ๅ‚จ + * - ๆ–ฐ็”จๆˆท้ป˜่ฎคๅˆ†้… ROLE_USER ่ง’่‰ฒ + * - ่ฟ”ๅ›ž Token ๅฏ็›ดๆŽฅ็”จไบŽๅŽ็ปญ่ฏทๆฑ‚ + * + * @param userDto ็”จๆˆทๆณจๅ†Œไฟกๆฏ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซ Token ๅ’Œ็”จๆˆทไฟกๆฏ๏ผ‰ + */ @Override - public RestBean register(UserDto userDto) { - String normalizedEmail = userDto.getEmail() == null ? null : userDto.getEmail().trim().toLowerCase(Locale.ROOT); - // ๆฃ€ๆŸฅ็”จๆˆทๅๆ˜ฏๅฆๅญ˜ๅœจ + @Transactional(rollbackFor = Exception.class) + public RestBean register(UserDto userDto) { + // 1. ๆ ‡ๅ‡†ๅŒ–้‚ฎ็ฎฑ๏ผˆ่ฝฌๅฐๅ†™๏ผŒๅŽป้™ค้ฆ–ๅฐพ็ฉบๆ ผ๏ผ‰ + String normalizedEmail = userDto.getEmail() == null ? null + : userDto.getEmail().trim().toLowerCase(Locale.ROOT); + + // 2. ๆฃ€ๆŸฅ็”จๆˆทๅๆ˜ฏๅฆๅทฒๅญ˜ๅœจ if (userRepository.existsByUsername(userDto.getUsername())) { return RestBean.failure(400, "็”จๆˆทๅๅทฒ่ขซไฝฟ็”จ", null); } - // ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅญ˜ๅœจ + + // 3. ๆฃ€ๆŸฅ้‚ฎ็ฎฑๆ˜ฏๅฆๅทฒๅญ˜ๅœจ๏ผˆไธๅŒบๅˆ†ๅคงๅฐๅ†™๏ผ‰ if (userRepository.existsByEmailIgnoreCase(normalizedEmail)) { return RestBean.failure(400, "้‚ฎ็ฎฑๅทฒ่ขซไฝฟ็”จ", null); } + // 4. ๅˆ›ๅปบ็”จๆˆทๅฎžไฝ“ User user = new User(); user.setUsername(userDto.getUsername()); + // 4.1 ๅฏ†็ ไฝฟ็”จ BCrypt ๅŠ ๅฏ† user.setPassword(passwordEncoder.encode(userDto.getPassword())); user.setEmail(normalizedEmail); - user.setRole(Role.USER); // ๆ–ฐๆณจๅ†Œ็”จๆˆท้ป˜่ฎคไธบๆ™ฎ้€š็”จๆˆท - // ้ป˜่ฎค็Šถๆ€ไธบ1๏ผˆๆญฃๅธธ๏ผ‰๏ผŒๅทฒๅœจๅฎžไฝ“็ฑปไธญ่ฎพ็ฝฎ + // 5. ๅˆ†้…้ป˜่ฎค่ง’่‰ฒ๏ผˆROLE_USER๏ผ‰ + SysRole userRole = roleRepository.findByRoleCode("ROLE_USER") + .orElseThrow(() -> new RuntimeException("้ป˜่ฎค่ง’่‰ฒ ROLE_USER ๆœชๆ‰พๅˆฐ")); + user.setRoles(Set.of(userRole)); + + // 6. ไฟๅญ˜็”จๆˆทๅˆฐๆ•ฐๆฎๅบ“ userRepository.save(user); - // ็”Ÿๆˆtoken - String token = jwtUtil.generateToken(user.getId(), user.getUsername()); - return RestBean.success(token); + // 7. ็”Ÿๆˆ Access Token + String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername()); + + // 8. ็”Ÿๆˆ Refresh Token + String refreshToken = tokenService.createRefreshToken(user.getId(), null, null); + + // 9. ๆž„ๅปบ็™ปๅฝ•ๅ“ๅบ” + LoginResponseVo response = LoginResponseVo.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(jwtUtil.getExpirationSeconds()) + .userInfo(LoginResponseVo.UserInfoVo.builder() + .id(user.getId()) + .username(user.getUsername()) + .email(user.getEmail()) + .build()) + .build(); + + // 10. ๅ‘้€ๆณจๅ†Œๆถˆๆฏๅˆฐ MQ๏ผˆๅผ‚ๆญฅๅค„็†๏ผ‰ + // messageProducer.sendRegisterMessage(user.getId(), user.getUsername(), user.getEmail()); + + log.info("็”จๆˆทๆณจๅ†ŒๆˆๅŠŸ - userId: {}, username: {}", user.getId(), user.getUsername()); + return RestBean.success(response); } + /** + * ็”จๆˆท็™ปๅฝ• + * ๆญฅ้ชค๏ผš + * 1. ๆ นๆฎ็”จๆˆทๅๆŸฅๆ‰พ็”จๆˆท + * 2. ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + * 3. ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ็ฆ็”จ + * 4. ้ชŒ่ฏๅฏ†็  + * 5. ่ฎฐๅฝ•็™ปๅฝ•ๅคฑ่ดฅ๏ผˆๅฆ‚ๆžœๅฏ†็ ้”™่ฏฏ๏ผ‰ + * 6. ็™ปๅฝ•ๆˆๅŠŸๅŽ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + * 7. ็”Ÿๆˆ Access Token ๅ’Œ Refresh Token + * 8. ๅ‘้€็™ปๅฝ•ๆถˆๆฏๅˆฐ MQ + * + * ๅฎ‰ๅ…จๆœบๅˆถ๏ผš + * - ่ฟž็ปญๅคฑ่ดฅ 5 ๆฌก้”ๅฎš่ดฆๆˆท 30 ๅˆ†้’Ÿ + * - ๅฏ†็ ้”™่ฏฏไผš่ฎฐๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ + * - ็™ปๅฝ•ๆˆๅŠŸๅŽ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + * + * @param userDto ็™ปๅฝ•ไฟกๆฏ + * @return ็™ปๅฝ•ๅ“ๅบ”๏ผˆๅŒ…ๅซ Token ๅ’Œ็”จๆˆทไฟกๆฏ๏ผ‰ + */ @Override - public RestBean login(UserDto userDto) { - // ๆŸฅๆ‰พ็”จๆˆท + public RestBean login(UserDto userDto) { + // 1. ๆŸฅๆ‰พ็”จๆˆท User user = userRepository.findByUsername(userDto.getUsername()) .orElse(null); + // 2. ็”จๆˆทไธๅญ˜ๅœจ if (user == null) { - return RestBean.failure(RestCode.DATA_NOT_FOUND, "็”จๆˆทไธๅญ˜ๅœจ"); + return RestBean.failure(RestCode.DATA_NOT_FOUND.getCode(), "็”จๆˆทไธๅญ˜ๅœจ", null); } - // ๆฃ€ๆŸฅ็”จๆˆท็Šถๆ€ + // 3. ๆฃ€ๆŸฅ่ดฆๆˆทๆ˜ฏๅฆ่ขซ้”ๅฎš + if (user.isLocked()) { + Long remainingMinutes = loginAttemptService.getRemainingLockTimeMinutes(userDto.getUsername()); + return RestBean.failure(RestCode.USER_LOCKED.getCode(), + String.format("่ดฆๆˆทๅทฒ่ขซ้”ๅฎš๏ผŒ่ฏทๅœจ %d ๅˆ†้’ŸๅŽ้‡่ฏ•", remainingMinutes), null); + } + + // 4. ๆฃ€ๆŸฅ็”จๆˆท็Šถๆ€๏ผˆๆ˜ฏๅฆ่ขซ็ฆ็”จ๏ผ‰ if (!user.isEnabled()) { - return RestBean.failure(403, "็”จๆˆทๅทฒ่ขซ็ฆ็”จ", null); + return RestBean.failure(RestCode.USER_DISABLED.getCode(), "็”จๆˆทๅทฒ่ขซ็ฆ็”จ", null); } - // ้ชŒ่ฏๅฏ†็  + // 5. ้ชŒ่ฏๅฏ†็  if (!passwordEncoder.matches(userDto.getPassword(), user.getPassword())) { - return RestBean.failure(RestCode.USERNAME_OR_PASSWORD_ERROR, "็”จๆˆทๅๆˆ–ๅฏ†็ ้”™่ฏฏ"); + // 5.1 ่ฎฐๅฝ•ๅคฑ่ดฅๅฐ่ฏ• + boolean shouldLock = loginAttemptService.recordFailedAttempt(userDto.getUsername()); + if (shouldLock) { + // 5.2 ่ดฆๆˆท่ขซ้”ๅฎš + return RestBean.failure(RestCode.USER_LOCKED.getCode(), + String.format("็™ปๅฝ•ๅคฑ่ดฅๆฌกๆ•ฐ่ฟ‡ๅคš๏ผŒ่ดฆๆˆทๅทฒ่ขซ้”ๅฎš %d ๅˆ†้’Ÿ", 30), null); + } + // 5.3 ๅฏ†็ ้”™่ฏฏ๏ผŒ่ฟ”ๅ›ž้€š็”จ้”™่ฏฏไฟกๆฏ๏ผˆ้˜ฒๆญข็”จๆˆทๅๆžšไธพ๏ผ‰ + return RestBean.failure(RestCode.USERNAME_OR_PASSWORD_ERROR.getCode(), "็”จๆˆทๅๆˆ–ๅฏ†็ ้”™่ฏฏ", null); } - // ็”Ÿๆˆtoken - String token = jwtUtil.generateToken(user.getId(), user.getUsername()); - return RestBean.success(token); + // 6. ็™ปๅฝ•ๆˆๅŠŸ๏ผŒ้‡็ฝฎๅคฑ่ดฅ่ฎกๆ•ฐ + loginAttemptService.resetFailedAttempts(userDto.getUsername()); + + // 7. ็”Ÿๆˆ Access Token + String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername()); + + // 8. ็”Ÿๆˆ Refresh Token + String refreshToken = tokenService.createRefreshToken(user.getId(), null, null); + + // 9. ๆž„ๅปบ็™ปๅฝ•ๅ“ๅบ” + LoginResponseVo response = LoginResponseVo.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .expiresIn(jwtUtil.getExpirationSeconds()) + .userInfo(LoginResponseVo.UserInfoVo.builder() + .id(user.getId()) + .username(user.getUsername()) + .email(user.getEmail()) + .build()) + .build(); + + // 10. ๅ‘้€็™ปๅฝ•ๆถˆๆฏๅˆฐ MQ๏ผˆๅผ‚ๆญฅๅค„็†๏ผŒๅฆ‚่ฎฐๅฝ•็™ปๅฝ•ๆ—ฅๅฟ—๏ผ‰ + // messageProducer.sendLoginMessage(user.getId(), user.getUsername(), ipAddress); + + log.info("็”จๆˆท็™ปๅฝ•ๆˆๅŠŸ - userId: {}, username: {}", user.getId(), user.getUsername()); + return RestBean.success(response); } + /** + * ๅˆ†้กตๆŸฅ่ฏข็”จๆˆทๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๆž„ๅปบๅˆ†้กตๅ‚ๆ•ฐ๏ผˆ้กต็ ไปŽ 0 ๅผ€ๅง‹๏ผ‰ + * 2. ๆŒ‰ๅˆ›ๅปบๆ—ถ้—ดๅ€’ๅบๆŽ’ๅบ + * 3. ๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏขๆกไปถ + * 4. ๆ‰ง่กŒๅˆ†้กตๆŸฅ่ฏข + * 5. ่ฝฌๆข็ป“ๆžœไธบ่ง†ๅ›พๅฏน่ฑก + * + * ๆŸฅ่ฏขๆกไปถ๏ผš + * - ๅ…ณ้”ฎ่ฏ๏ผšๅŒน้…็”จๆˆทๅๆˆ–้‚ฎ็ฎฑ + * - ็Šถๆ€๏ผš็ฒพ็กฎๅŒน้… + * + * @param queryDto ๆŸฅ่ฏขๆกไปถ + * @return ๅˆ†้กต็”จๆˆทๅˆ—่กจ + */ @Override public RestBean> getUserList(UserQueryDto queryDto) { try { + // 1. ๆž„ๅปบๅˆ†้กตๅ‚ๆ•ฐ + // - ้กต็ ไปŽ 0 ๅผ€ๅง‹๏ผˆๅ‰็ซฏไผ ๅ…ฅ็š„้กต็ ้œ€่ฆ -1๏ผ‰ + // - ๆฏ้กตๅคงๅฐ่‡ณๅฐ‘ไธบ 1 Pageable pageable = PageRequest.of( Math.max(queryDto.getPage() - 1, 0), Math.max(queryDto.getSize(), 1), Sort.by(Sort.Direction.DESC, "createdAt") ); + // 2. ๆ‰ง่กŒๅˆ†้กตๆŸฅ่ฏข Page userPage = userRepository.findAll(buildSpecification(queryDto), pageable); + + // 3. ่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑกๅˆ—่กจ List records = userPage.getContent().stream() .map(this::convertToVo) .collect(Collectors.toList()); + // 4. ๆž„ๅปบๅˆ†้กต็ป“ๆžœ return RestBean.success(PageResult.of( records, userPage.getTotalElements(), @@ -138,58 +338,127 @@ public class UserServiceImpl implements UserService { } } + /** + * ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„็Šถๆ€๏ผˆ็ฆๆญข๏ผ‰ + * 3. ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ + * 4. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * ๆณจๆ„๏ผš + * - ไธ่ƒฝ็ฆ็”จๅฝ“ๅ‰็™ปๅฝ•็š„็”จๆˆท + * - ็Šถๆ€ๅ˜ๆ›ด้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ•ๆ‰่ƒฝๅฎŒๅ…จ็”Ÿๆ•ˆ + * + * @param userId ็”จๆˆทID + * @param updateDto ็Šถๆ€ๆ›ดๆ–ฐ่ฏทๆฑ‚ + * @return ๆ›ดๆ–ฐๅŽ็š„็”จๆˆทไฟกๆฏ + */ @Override + @Transactional(rollbackFor = Exception.class) public RestBean updateUserStatus(Long userId, UserStatusUpdateDto updateDto) { try { + // 1. ๆŸฅ่ฏข็”จๆˆท User user = userRepository.findById(userId).orElse(null); if (user == null) { return RestBean.failure(RestCode.DATA_NOT_FOUND, null); } + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท Long currentUserId = SecurityUtils.getUserId(); if (currentUserId != null && currentUserId.equals(userId)) { return RestBean.failure(403, "ไธๅ…่ฎธไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„ๅฏ็”จ็Šถๆ€", null); } + // 3. ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ user.setStatus(updateDto.getStatus()); - return RestBean.success(convertToVo(userRepository.save(user))); + User savedUser = userRepository.save(user); + + log.info("็”จๆˆท็Šถๆ€ๆ›ดๆ–ฐๆˆๅŠŸ - userId: {}, status: {}", userId, updateDto.getStatus()); + return RestBean.success(convertToVo(savedUser)); } catch (Exception e) { log.error("ๆ›ดๆ–ฐ็”จๆˆท็Šถๆ€ๅคฑ่ดฅ, userId={}", userId, e); return RestBean.failure(RestCode.SYSTEM_ERROR, null); } } + /** + * ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒ + * ๆญฅ้ชค๏ผš + * 1. ๆŸฅ่ฏข็”จๆˆทๆ˜ฏๅฆๅญ˜ๅœจ + * 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ๏ผˆ็ฆๆญข๏ผ‰ + * 3. ้ชŒ่ฏๆ‰€ๆœ‰่ง’่‰ฒIDๆ˜ฏๅฆๅญ˜ๅœจ + * 4. ๆธ…็ฉบ็”จๆˆทๅŽŸๆœ‰่ง’่‰ฒ + * 5. ๆทปๅŠ ๆ–ฐ่ง’่‰ฒ + * 6. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + * + * ๆณจๆ„๏ผš + * - ไธ่ƒฝไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ + * - ่ง’่‰ฒๅ˜ๆ›ดๅŽ้œ€่ฆ้‡ๆ–ฐ็™ปๅฝ•ๆ‰่ƒฝ็”Ÿๆ•ˆ + * - ๆธ…็ฉบๅŽŸๆœ‰่ง’่‰ฒๅ†ๆทปๅŠ ๆ–ฐ่ง’่‰ฒ๏ผˆ้ฟๅ…้‡ๅค๏ผ‰ + * + * @param userId ็”จๆˆทID + * @param updateDto ่ง’่‰ฒๆ›ดๆ–ฐ่ฏทๆฑ‚ + * @return ๆ›ดๆ–ฐๅŽ็š„็”จๆˆทไฟกๆฏ + */ @Override + @Transactional(rollbackFor = Exception.class) public RestBean updateUserRole(Long userId, UserRoleUpdateDto updateDto) { try { + // 1. ๆŸฅ่ฏข็”จๆˆท User user = userRepository.findById(userId).orElse(null); if (user == null) { return RestBean.failure(RestCode.DATA_NOT_FOUND, null); } + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท Long currentUserId = SecurityUtils.getUserId(); if (currentUserId != null && currentUserId.equals(userId)) { return RestBean.failure(403, "ไธๅ…่ฎธไฟฎๆ”นๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„่ง’่‰ฒ", null); } - String roleValue = updateDto.getRole().trim().toUpperCase(); - if (!"ADMIN".equals(roleValue) && !"USER".equals(roleValue)) { - return RestBean.failure(400, "่ง’่‰ฒไธๅˆๆณ•", null); + // 3. ๆ นๆฎ่ง’่‰ฒIDๆŸฅ่ฏขๆ‰€ๆœ‰่ง’่‰ฒ + Set roles = new HashSet<>(); + for (Long roleId : updateDto.getRoleIds()) { + SysRole role = roleRepository.findById(roleId) + .orElseThrow(() -> new RuntimeException("่ง’่‰ฒไธๅญ˜ๅœจ: " + roleId)); + roles.add(role); } - Role role = Role.valueOf(roleValue); - user.setRole(role); - return RestBean.success(convertToVo(userRepository.save(user))); + // 4. ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒ๏ผˆๆธ…็ฉบๅŽๆทปๅŠ ๏ผ‰ + user.getRoles().clear(); + user.getRoles().addAll(roles); + + // 5. ไฟๅญ˜ๅˆฐๆ•ฐๆฎๅบ“ + User savedUser = userRepository.save(user); + + log.info("็”จๆˆท่ง’่‰ฒๆ›ดๆ–ฐๆˆๅŠŸ - userId: {}, roleIds: {}", userId, updateDto.getRoleIds()); + return RestBean.success(convertToVo(savedUser)); } catch (Exception e) { log.error("ๆ›ดๆ–ฐ็”จๆˆท่ง’่‰ฒๅคฑ่ดฅ, userId={}", userId, e); return RestBean.failure(RestCode.SYSTEM_ERROR, null); } } + /** + * ๆž„ๅปบๅŠจๆ€ๆŸฅ่ฏขๆกไปถ + * ๆญฅ้ชค๏ผš + * 1. ๆ”ฏๆŒๆŒ‰ๅ…ณ้”ฎ่ฏๆจก็ณŠๆœ็ดข๏ผˆ็”จๆˆทๅๆˆ–้‚ฎ็ฎฑ๏ผ‰ + * 2. ๆ”ฏๆŒๆŒ‰็Šถๆ€็ฒพ็กฎ็ญ›้€‰ + * 3. ่ฟ”ๅ›ž Specification ๅฏน่ฑก + * + * ๆณจๆ„๏ผš + * - ่ง’่‰ฒๆŸฅ่ฏข้œ€่ฆไฝฟ็”จ JOIN๏ผŒๆญคๅค„็ฎ€ๅŒ–ๅค„็† + * - ๅฏไปฅๆ นๆฎ้œ€่ฆๆ‰ฉๅฑ•ๆ›ดๅคšๆŸฅ่ฏขๆกไปถ + * + * @param queryDto ๆŸฅ่ฏขๆกไปถๅฏน่ฑก + * @return ๅŠจๆ€ๆŸฅ่ฏข Specification + */ private Specification buildSpecification(UserQueryDto queryDto) { return (root, query, cb) -> { List predicates = new ArrayList<>(); + // 1. ๅ…ณ้”ฎ่ฏๆจก็ณŠๆœ็ดข๏ผˆ็”จๆˆทๅๆˆ–้‚ฎ็ฎฑ๏ผ‰ if (StringUtils.hasText(queryDto.getKeyword())) { String keyword = "%" + queryDto.getKeyword().trim() + "%"; predicates.add(cb.or( @@ -198,10 +467,7 @@ public class UserServiceImpl implements UserService { )); } - if (StringUtils.hasText(queryDto.getRole())) { - predicates.add(cb.equal(root.get("role"), Role.fromString(queryDto.getRole()))); - } - + // 2. ็Šถๆ€็ฒพ็กฎ็ญ›้€‰ if (queryDto.getStatus() != null) { predicates.add(cb.equal(root.get("status"), queryDto.getStatus())); } @@ -210,13 +476,29 @@ public class UserServiceImpl implements UserService { }; } + /** + * ๅฐ†็”จๆˆทๅฎžไฝ“่ฝฌๆขไธบ่ง†ๅ›พๅฏน่ฑก + * ๆญฅ้ชค๏ผš + * 1. ๅคๅˆถๅŸบๆœฌไฟกๆฏ๏ผˆIDใ€็”จๆˆทๅใ€้‚ฎ็ฎฑใ€็Šถๆ€๏ผ‰ + * 2. ่ฝฌๆข่ง’่‰ฒไธบ่ง’่‰ฒ็ผ–็ ้›†ๅˆ + * 3. ๅคๅˆถๆ—ถ้—ดไฟกๆฏ + * + * @param user ็”จๆˆทๅฎžไฝ“ + * @return ็”จๆˆท่ง†ๅ›พๅฏน่ฑก + */ private UserVo convertToVo(User user) { UserVo userVo = new UserVo(); userVo.setId(user.getId()); userVo.setUsername(user.getUsername()); userVo.setEmail(user.getEmail()); userVo.setStatus(user.getStatus()); - userVo.setRole(user.getRole().name()); + + // 1. ่ฝฌๆข่ง’่‰ฒไธบๅญ—็ฌฆไธฒ้›†ๅˆ๏ผˆๅช่ฟ”ๅ›ž่ง’่‰ฒ็ผ–็ ๏ผ‰ + Set roleCodes = user.getRoles().stream() + .map(SysRole::getRoleCode) + .collect(Collectors.toSet()); + userVo.setRoles(roleCodes); + userVo.setCreatedAt(user.getCreatedAt()); userVo.setUpdatedAt(user.getUpdatedAt()); return userVo; diff --git a/src/main/java/com/aisi/template/utils/JwtUtil.java b/src/main/java/com/aisi/template/utils/JwtUtil.java index ca7b086..e73b135 100644 --- a/src/main/java/com/aisi/template/utils/JwtUtil.java +++ b/src/main/java/com/aisi/template/utils/JwtUtil.java @@ -8,37 +8,167 @@ import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date; +import java.util.UUID; +/** + * JWT ๅทฅๅ…ท็ฑป + * ็”จไบŽ็”Ÿๆˆๅ’Œ้ชŒ่ฏ JWT Token + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. Token ็”Ÿๆˆ๏ผš็”ŸๆˆๅŒ…ๅซ็”จๆˆทไฟกๆฏ็š„ JWT Token + * 2. Token ่งฃๆž๏ผš่งฃๆž Token ๅนถๆๅ–ไฟกๆฏ + * 3. Token ้ชŒ่ฏ๏ผš้ชŒ่ฏ Token ็ญพๅๅ’Œๆœ‰ๆ•ˆๆœŸ + * + * Token ็ป“ๆž„๏ผš + * - Header๏ผš็ฎ—ๆณ•็ฑปๅž‹ๅ’Œ Token ็ฑปๅž‹ + * - Payload๏ผš็”จๆˆทไฟกๆฏใ€็ญพๅ‘ๆ—ถ้—ดใ€่ฟ‡ๆœŸๆ—ถ้—ดใ€jti๏ผˆJWT ID๏ผ‰ + * - Signature๏ผšไฝฟ็”จๅฏ†้’ฅ็ญพๅ็š„็ญพๅ้ƒจๅˆ† + * + * @author Claude + * @since 2024-04-09 + */ @Component public class JwtUtil { + /** + * JWT ๅฏ†้’ฅ + * ไปŽ้…็ฝฎๆ–‡ไปถ่ฏปๅ– + * HS512 ็ฎ—ๆณ•่ฆๆฑ‚ๅฏ†้’ฅ่‡ณๅฐ‘ 64 ๅญ—็ฌฆ๏ผˆ512 ไฝ๏ผ‰ + */ @Value("${jwt.secret}") private String secret; - private final long expiration = 1000 * 60 * 60 *24; // 24ๅฐๆ—ถ + /** + * Access Token ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * ไปŽ้…็ฝฎๆ–‡ไปถ่ฏปๅ–๏ผŒ้ป˜่ฎค 3600 ็ง’๏ผˆ1 ๅฐๆ—ถ๏ผ‰ + */ + @Value("${jwt.access-token-expiration:3600}") // Default 1 hour + private long accessTokenExpirationSeconds; + /** + * ่Žทๅ–็ญพๅๅฏ†้’ฅ + * ่ฏดๆ˜Ž๏ผš + * - HS512 ็ฎ—ๆณ•่ฆๆฑ‚ๅฏ†้’ฅ่‡ณๅฐ‘ 512 ไฝ๏ผˆ64 ๅญ—่Š‚๏ผ‰ + * - ๅฆ‚ๆžœๅฏ†้’ฅไธ่ถณ 64 ๅญ—็ฌฆ๏ผŒๆŠ›ๅ‡บๅผ‚ๅธธ + * + * @return ็ญพๅๅฏ†้’ฅ + */ private Key getSigningKey() { // HS512 ่ฆๆฑ‚ๅฏ†้’ฅ่‡ณๅฐ‘ 512 ไฝ = 64 ๅญ—่Š‚ if (secret.length() < 64) { - throw new IllegalArgumentException("JWT secret must be at least 64 characters long for HS512"); + throw new IllegalArgumentException("JWT ๅฏ†้’ฅๅฟ…้กป่‡ณๅฐ‘ 64 ไธชๅญ—็ฌฆ๏ผˆHS512 ็ฎ—ๆณ•่ฆๆฑ‚๏ผ‰"); } return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } - public String generateToken(Long userId,String username) { + /** + * ไธบ็”จๆˆท็”Ÿๆˆ Access Token + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆๅ”ฏไธ€็š„ jti๏ผˆJWT ID๏ผ‰ + * 2. ่ฎพ็ฝฎ็”จๆˆทไฟกๆฏ๏ผˆ็”จๆˆทๅใ€็”จๆˆทID๏ผ‰ + * 3. ่ฎพ็ฝฎ็ญพๅ‘ๆ—ถ้—ดๅ’Œ่ฟ‡ๆœŸๆ—ถ้—ด + * 4. ไฝฟ็”จๅฏ†้’ฅ็ญพๅ + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @return JWT Token + */ + public String generateToken(Long userId, String username) { + return generateToken(userId, username, accessTokenExpirationSeconds); + } + + /** + * ็”Ÿๆˆ่‡ชๅฎšไน‰่ฟ‡ๆœŸๆ—ถ้—ด็š„ Token + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆๅ”ฏไธ€็š„ jti๏ผˆJWT ID๏ผ‰๏ผŒ็”จไบŽ้ป‘ๅๅ•่ฟฝ่ธช + * 2. ่ฎก็ฎ—่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆๆฏซ็ง’๏ผ‰ + * 3. ๆž„ๅปบ Token + * + * @param userId ็”จๆˆทID + * @param username ็”จๆˆทๅ + * @param expirationSeconds ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * @return JWT Token + */ + public String generateToken(Long userId, String username, long expirationSeconds) { + // 1. ็”Ÿๆˆๅ”ฏไธ€็š„ jti๏ผˆJWT ID๏ผ‰ + String jti = UUID.randomUUID().toString(); + long expirationMillis = expirationSeconds * 1000; + + // 2. ๆž„ๅปบ Token return Jwts.builder() - .setSubject(username) - .claim("id", userId) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + expiration)) - .signWith(getSigningKey(), SignatureAlgorithm.HS512) + .setSubject(username) // ไธป้ข˜๏ผš็”จๆˆทๅ + .claim("id", userId) // ่‡ชๅฎšไน‰ๅฃฐๆ˜Ž๏ผš็”จๆˆทID + .claim("jti", jti) // ่‡ชๅฎšไน‰ๅฃฐๆ˜Ž๏ผšJWT ID๏ผˆ็”จไบŽ้ป‘ๅๅ•๏ผ‰ + .setIssuedAt(new Date()) // ็ญพๅ‘ๆ—ถ้—ด๏ผšๅฝ“ๅ‰ๆ—ถ้—ด + .setExpiration(new Date(System.currentTimeMillis() + expirationMillis)) // ่ฟ‡ๆœŸๆ—ถ้—ด + .signWith(getSigningKey(), SignatureAlgorithm.HS512) // ็ญพๅ็ฎ—ๆณ•ๅ’Œๅฏ†้’ฅ .compact(); } + /** + * ไปŽ Token ไธญๆๅ–็”จๆˆทๅ + * + * @param token JWT Token + * @return ็”จๆˆทๅ + */ public String extractUsername(String token) { return parseClaims(token).getBody().getSubject(); } + /** + * ไปŽ Token ไธญๆๅ–็”จๆˆทID + * + * @param token JWT Token + * @return ็”จๆˆทID + */ + public Long extractUserId(String token) { + Claims claims = parseClaims(token).getBody(); + return claims.get("id", Long.class); + } + + /** + * ไปŽ Token ไธญๆๅ– JWT ID๏ผˆjti๏ผ‰ + * ่ฏดๆ˜Ž๏ผš + * - jti ๆ˜ฏ JWT ็š„ๅ”ฏไธ€ๆ ‡่ฏ† + * - ็”จไบŽๅฐ† Token ๅŠ ๅ…ฅ้ป‘ๅๅ•๏ผˆ็™ปๅ‡บๅŠŸ่ƒฝ๏ผ‰ + * + * @param token JWT Token + * @return JWT ID + */ + public String extractJti(String token) { + Claims claims = parseClaims(token).getBody(); + return claims.get("jti", String.class); + } + + /** + * ไปŽ Token ไธญๆๅ–่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param token JWT Token + * @return ่ฟ‡ๆœŸๆ—ถ้—ด + */ + public Date extractExpiration(String token) { + return parseClaims(token).getBody().getExpiration(); + } + + /** + * ่Žทๅ– Access Token ็š„่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * + * @return ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + */ + public long getExpirationSeconds() { + return accessTokenExpirationSeconds; + } + + /** + * ้ชŒ่ฏ Token ็ญพๅๅ’Œ็ป“ๆž„ + * ่ฏดๆ˜Ž๏ผš + * - ้ชŒ่ฏ็ญพๅๆ˜ฏๅฆๆญฃ็กฎ + * - ้ชŒ่ฏ Token ็ป“ๆž„ๆ˜ฏๅฆๅฎŒๆ•ด + * - ไธ้ชŒ่ฏ่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param token JWT Token + * @return true ่กจ็คบ Token ๆœ‰ๆ•ˆ + */ public boolean validateToken(String token) { try { parseClaims(token); @@ -48,6 +178,31 @@ public class JwtUtil { } } + /** + * ๆฃ€ๆŸฅ Token ๆ˜ฏๅฆ่ฟ‡ๆœŸ + * + * @param token JWT Token + * @return true ่กจ็คบ Token ๅทฒ่ฟ‡ๆœŸ + */ + public boolean isTokenExpired(String token) { + try { + Date expiration = extractExpiration(token); + return expiration.before(new Date()); + } catch (JwtException e) { + return true; + } + } + + /** + * ่งฃๆžๅนถ้ชŒ่ฏ JWT Token + * ่ฏดๆ˜Ž๏ผš + * - ้ชŒ่ฏ็ญพๅ + * - ่งฃๆž Claims + * + * @param token JWT Token + * @return ่งฃๆžๅŽ็š„ JWT ๅฏน่ฑก๏ผˆๅŒ…ๅซ Claims๏ผ‰ + * @throws JwtException ๅฆ‚ๆžœ Token ๆ— ๆ•ˆ + */ private Jws parseClaims(String token) { return Jwts.parserBuilder() .setSigningKey(getSigningKey()) diff --git a/src/main/java/com/aisi/template/utils/RedisCache.java b/src/main/java/com/aisi/template/utils/RedisCache.java new file mode 100644 index 0000000..e80e00a --- /dev/null +++ b/src/main/java/com/aisi/template/utils/RedisCache.java @@ -0,0 +1,762 @@ +package com.aisi.template.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Redis ็ผ“ๅญ˜ๅทฅๅ…ท็ฑป + * ๆไพ›ๅ„็งๅœบๆ™ฏไธ‹็š„็ผ“ๅญ˜ๆ“ไฝœๆ–นๆณ• + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * 1. ๅฏน่ฑก็ผ“ๅญ˜๏ผš็ผ“ๅญ˜ๅฎžไฝ“ๅฏน่ฑก๏ผŒๅ‡ๅฐ‘ๆ•ฐๆฎๅบ“ๆŸฅ่ฏข + * 2. ๅˆ—่กจ็ผ“ๅญ˜๏ผš็ผ“ๅญ˜ๅˆ—่กจๆ•ฐๆฎ + * 3. ้›†ๅˆ็ผ“ๅญ˜๏ผš็ผ“ๅญ˜ๅŽป้‡ๆ•ฐๆฎ + * 4. ๅ“ˆๅธŒ็ผ“ๅญ˜๏ผš็ผ“ๅญ˜ๅฏน่ฑกๅญ—ๆฎต + * 5. ่ฎกๆ•ฐๅ™จ๏ผšๆ–‡็ซ ้˜…่ฏปๆ•ฐใ€็‚น่ตžๆ•ฐ็ญ‰ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisCache { + + /** + * Redis ๆจกๆฟ + */ + private final RedisTemplate redisTemplate; + + // ==================== 1. String ็ฑปๅž‹ๆ“ไฝœ ==================== + + /** + * ่ฎพ็ฝฎ็ผ“ๅญ˜๏ผˆๆฐธไธ่ฟ‡ๆœŸ๏ผ‰ + * ๆณจๆ„๏ผš็”Ÿไบง็Žฏๅขƒๅปบ่ฎฎ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด๏ผŒ้˜ฒๆญขๅ†…ๅญ˜ๆบขๅ‡บ + * + * @param key ้”ฎ + * @param value ๅ€ผ + */ + public void set(String key, Object value) { + try { + redisTemplate.opsForValue().set(key, value); + log.debug("่ฎพ็ฝฎ็ผ“ๅญ˜ๆˆๅŠŸ - key: {}", key); + } catch (Exception e) { + log.error("่ฎพ็ฝฎ็ผ“ๅญ˜ๅคฑ่ดฅ - key: {}", key, e); + } + } + + /** + * ่ฎพ็ฝฎ็ผ“ๅญ˜๏ผˆๆŒ‡ๅฎš่ฟ‡ๆœŸๆ—ถ้—ด๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅฐ†ๅ€ผๅบๅˆ—ๅŒ–ๅŽๅญ˜ๅ…ฅ Redis + * 2. ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param timeout ่ฟ‡ๆœŸๆ—ถ้—ด + * @param timeUnit ๆ—ถ้—ดๅ•ไฝ + */ + public void set(String key, Object value, long timeout, TimeUnit timeUnit) { + try { + redisTemplate.opsForValue().set(key, value, timeout, timeUnit); + log.debug("่ฎพ็ฝฎ็ผ“ๅญ˜ๆˆๅŠŸ - key: {}, timeout: {} {}", key, timeout, timeUnit); + } catch (Exception e) { + log.error("่ฎพ็ฝฎ็ผ“ๅญ˜ๅคฑ่ดฅ - key: {}", key, e); + } + } + + /** + * ่ฎพ็ฝฎ็ผ“ๅญ˜๏ผˆไฝฟ็”จ Duration๏ผ‰ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param duration ่ฟ‡ๆœŸๆ—ถ้—ด + */ + public void set(String key, Object value, Duration duration) { + try { + redisTemplate.opsForValue().set(key, value, duration); + log.debug("่ฎพ็ฝฎ็ผ“ๅญ˜ๆˆๅŠŸ - key: {}, duration: {}", key, duration); + } catch (Exception e) { + log.error("่ฎพ็ฝฎ็ผ“ๅญ˜ๅคฑ่ดฅ - key: {}", key, e); + } + } + + /** + * ่Žทๅ–็ผ“ๅญ˜ + * ๆญฅ้ชค๏ผš + * 1. ไปŽ Redis ่Žทๅ–ๅ€ผ + * 2. ๅๅบๅˆ—ๅŒ–ไธบๅฏน่ฑก + * + * @param key ้”ฎ + * @return ๅ€ผ๏ผŒไธๅญ˜ๅœจ่ฟ”ๅ›ž null + */ + public Object get(String key) { + try { + Object value = redisTemplate.opsForValue().get(key); + log.debug("่Žทๅ–็ผ“ๅญ˜ - key: {}, found: {}", key, value != null); + return value; + } catch (Exception e) { + log.error("่Žทๅ–็ผ“ๅญ˜ๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ่Žทๅ–็ผ“ๅญ˜๏ผˆๆŒ‡ๅฎš็ฑปๅž‹๏ผ‰ + * + * @param key ้”ฎ + * @param type ่ฟ”ๅ›žๅ€ผ็ฑปๅž‹ + * @return ๅ€ผ + */ + @SuppressWarnings("unchecked") + public T get(String key, Class type) { + try { + Object value = redisTemplate.opsForValue().get(key); + if (value != null && type.isInstance(value)) { + return (T) value; + } + return null; + } catch (Exception e) { + log.error("่Žทๅ–็ผ“ๅญ˜ๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ๅˆ ้™ค็ผ“ๅญ˜ + * + * @param key ้”ฎ + * @return ๆ˜ฏๅฆๅˆ ้™คๆˆๅŠŸ + */ + public boolean delete(String key) { + try { + Boolean result = redisTemplate.delete(key); + log.debug("ๅˆ ้™ค็ผ“ๅญ˜ - key: {}, result: {}", key, result); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("ๅˆ ้™ค็ผ“ๅญ˜ๅคฑ่ดฅ - key: {}", key, e); + return false; + } + } + + /** + * ๆ‰น้‡ๅˆ ้™ค็ผ“ๅญ˜ + * ๆญฅ้ชค๏ผš + * 1. ้ๅކๆ‰€ๆœ‰้”ฎ + * 2. ไธ€ๆฌกๆ€งๅˆ ้™ค + * + * @param keys ้”ฎ้›†ๅˆ + * @return ๅˆ ้™ค็š„ๆ•ฐ้‡ + */ + public long delete(Collection keys) { + try { + Long count = redisTemplate.delete(keys); + log.debug("ๆ‰น้‡ๅˆ ้™ค็ผ“ๅญ˜ - count: {}", count); + return count != null ? count : 0; + } catch (Exception e) { + log.error("ๆ‰น้‡ๅˆ ้™ค็ผ“ๅญ˜ๅคฑ่ดฅ", e); + return 0; + } + } + + /** + * ๅˆคๆ–ญ้”ฎๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param key ้”ฎ + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ + public boolean hasKey(String key) { + try { + Boolean result = redisTemplate.hasKey(key); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("ๅˆคๆ–ญ้”ฎๆ˜ฏๅฆๅญ˜ๅœจๅคฑ่ดฅ - key: {}", key, e); + return false; + } + } + + /** + * ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param key ้”ฎ + * @param timeout ่ฟ‡ๆœŸๆ—ถ้—ด + * @param unit ๆ—ถ้—ดๅ•ไฝ + * @return ๆ˜ฏๅฆ่ฎพ็ฝฎๆˆๅŠŸ + */ + public boolean expire(String key, long timeout, TimeUnit unit) { + try { + Boolean result = redisTemplate.expire(key, timeout, unit); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ดๅคฑ่ดฅ - key: {}", key, e); + return false; + } + } + + /** + * ่Žทๅ–่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param key ้”ฎ + * @param unit ๆ—ถ้—ดๅ•ไฝ + * @return ๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ด๏ผŒ-1 ่กจ็คบๆฐธไธ่ฟ‡ๆœŸ๏ผŒ-2 ่กจ็คบ้”ฎไธๅญ˜ๅœจ + */ + public long getExpire(String key, TimeUnit unit) { + try { + Long expire = redisTemplate.getExpire(key, unit); + return expire != null ? expire : -2; + } catch (Exception e) { + log.error("่Žทๅ–่ฟ‡ๆœŸๆ—ถ้—ดๅคฑ่ดฅ - key: {}", key, e); + return -2; + } + } + + /** + * ่‡ชๅขžๆ“ไฝœ๏ผˆๅŽŸๅญๆ€ง๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš่ฎกๆ•ฐๅ™จใ€็‚น่ตžๆ•ฐใ€้˜…่ฏปๆ•ฐ็ญ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅŽŸๅญๆ€ง่‡ชๅขž + * 2. ่ฟ”ๅ›ž่‡ชๅขžๅŽ็š„ๅ€ผ + * + * @param key ้”ฎ + * @param delta ๅขž้‡ + * @return ่‡ชๅขžๅŽ็š„ๅ€ผ + */ + public long increment(String key, long delta) { + try { + Long result = redisTemplate.opsForValue().increment(key, delta); + log.debug("่‡ชๅขžๆ“ไฝœ - key: {}, delta: {}, result: {}", key, delta, result); + return result != null ? result : 0; + } catch (Exception e) { + log.error("่‡ชๅขžๆ“ไฝœๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ่‡ชๅ‡ๆ“ไฝœ๏ผˆๅŽŸๅญๆ€ง๏ผ‰ + * + * @param key ้”ฎ + * @param delta ๅ‡้‡ + * @return ่‡ชๅ‡ๅŽ็š„ๅ€ผ + */ + public long decrement(String key, long delta) { + try { + Long result = redisTemplate.opsForValue().decrement(key, delta); + log.debug("่‡ชๅ‡ๆ“ไฝœ - key: {}, delta: {}, result: {}", key, delta, result); + return result != null ? result : 0; + } catch (Exception e) { + log.error("่‡ชๅ‡ๆ“ไฝœๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + // ==================== 2. Hash ็ฑปๅž‹ๆ“ไฝœ ==================== + + /** + * ่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎต + * ไฝฟ็”จๅœบๆ™ฏ๏ผš็ผ“ๅญ˜ๅฏน่ฑก็š„ๅ•ไธชๅญ—ๆฎต + * ๆญฅ้ชค๏ผš + * 1. ๅฐ†ๅญ—ๆฎตๅ’Œๅ€ผๅญ˜ๅ…ฅๅ“ˆๅธŒ่กจ + * 2. ้€‚ๅˆ้ƒจๅˆ†ๆ›ดๆ–ฐๅฏน่ฑกๅญ—ๆฎต + * + * @param key ้”ฎ + * @param field ๅญ—ๆฎต + * @param value ๅ€ผ + */ + public void hSet(String key, String field, Object value) { + try { + redisTemplate.opsForHash().put(key, field, value); + log.debug("่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎต - key: {}, field: {}", key, field); + } catch (Exception e) { + log.error("่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎตๅคฑ่ดฅ - key: {}, field: {}", key, field, e); + } + } + + /** + * ่Žทๅ–ๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param field ๅญ—ๆฎต + * @return ๅ€ผ + */ + public Object hGet(String key, String field) { + try { + return redisTemplate.opsForHash().get(key, field); + } catch (Exception e) { + log.error("่Žทๅ–ๅ“ˆๅธŒๅญ—ๆฎตๅคฑ่ดฅ - key: {}, field: {}", key, field, e); + return null; + } + } + + /** + * ๆ‰น้‡่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param map ๅญ—ๆฎต-ๅ€ผๆ˜ ๅฐ„ + */ + public void hSetAll(String key, Map map) { + try { + redisTemplate.opsForHash().putAll(key, map); + log.debug("ๆ‰น้‡่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎต - key: {}", key); + } catch (Exception e) { + log.error("ๆ‰น้‡่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎตๅคฑ่ดฅ - key: {}", key, e); + } + } + + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @return ๅญ—ๆฎต-ๅ€ผๆ˜ ๅฐ„ + */ + public Map hGetAll(String key) { + try { + return redisTemplate.opsForHash().entries(key); + } catch (Exception e) { + log.error("่Žทๅ–ๆ‰€ๆœ‰ๅ“ˆๅธŒๅญ—ๆฎตๅคฑ่ดฅ - key: {}", key, e); + return Map.of(); + } + } + + /** + * ๅˆ ้™คๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param fields ๅญ—ๆฎต้›†ๅˆ + * @return ๅˆ ้™ค็š„ๆ•ฐ้‡ + */ + public long hDelete(String key, Object... fields) { + try { + return redisTemplate.opsForHash().delete(key, fields); + } catch (Exception e) { + log.error("ๅˆ ้™คๅ“ˆๅธŒๅญ—ๆฎตๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ๅˆคๆ–ญๅ“ˆๅธŒๅญ—ๆฎตๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param key ้”ฎ + * @param field ๅญ—ๆฎต + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ + public boolean hExists(String key, String field) { + try { + return redisTemplate.opsForHash().hasKey(key, field); + } catch (Exception e) { + log.error("ๅˆคๆ–ญๅ“ˆๅธŒๅญ—ๆฎตๆ˜ฏๅฆๅญ˜ๅœจๅคฑ่ดฅ - key: {}, field: {}", key, field, e); + return false; + } + } + + /** + * ๅ“ˆๅธŒๅญ—ๆฎต่‡ชๅขž + * + * @param key ้”ฎ + * @param field ๅญ—ๆฎต + * @param delta ๅขž้‡ + * @return ่‡ชๅขžๅŽ็š„ๅ€ผ + */ + public long hIncrement(String key, String field, long delta) { + try { + return redisTemplate.opsForHash().increment(key, field, delta); + } catch (Exception e) { + log.error("ๅ“ˆๅธŒๅญ—ๆฎต่‡ชๅขžๅคฑ่ดฅ - key: {}, field: {}", key, field, e); + return 0; + } + } + + // ==================== 3. List ็ฑปๅž‹ๆ“ไฝœ ==================== + + /** + * ไปŽๅทฆไพงๆŽจๅ…ฅๅˆ—่กจ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆถˆๆฏ้˜Ÿๅˆ—ใ€ๆœ€ๆ–ฐๆถˆๆฏๅˆ—่กจ + * ๆญฅ้ชค๏ผš + * 1. ๅฐ†ๅ…ƒ็ด ๆŽจๅ…ฅๅˆ—่กจๅทฆไพง + * 2. ่ฟ”ๅ›žๅฝ“ๅ‰ๅˆ—่กจ้•ฟๅบฆ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆŽจๅ…ฅๅŽ็š„ๅˆ—่กจ้•ฟๅบฆ + */ + public long lLeftPush(String key, Object value) { + try { + Long size = redisTemplate.opsForList().leftPush(key, value); + log.debug("ไปŽๅทฆไพงๆŽจๅ…ฅๅˆ—่กจ - key: {}, size: {}", key, size); + return size != null ? size : 0; + } catch (Exception e) { + log.error("ไปŽๅทฆไพงๆŽจๅ…ฅๅˆ—่กจๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ไปŽๅณไพงๆŽจๅ…ฅๅˆ—่กจ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆŽจๅ…ฅๅŽ็š„ๅˆ—่กจ้•ฟๅบฆ + */ + public long lRightPush(String key, Object value) { + try { + Long size = redisTemplate.opsForList().rightPush(key, value); + log.debug("ไปŽๅณไพงๆŽจๅ…ฅๅˆ—่กจ - key: {}, size: {}", key, size); + return size != null ? size : 0; + } catch (Exception e) { + log.error("ไปŽๅณไพงๆŽจๅ…ฅๅˆ—่กจๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ไปŽๅทฆไพงๅผนๅ‡บๅˆ—่กจๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ๅผนๅ‡บ็š„ๅ…ƒ็ด  + */ + public Object lLeftPop(String key) { + try { + return redisTemplate.opsForList().leftPop(key); + } catch (Exception e) { + log.error("ไปŽๅทฆไพงๅผนๅ‡บๅˆ—่กจๅ…ƒ็ด ๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ไปŽๅณไพงๅผนๅ‡บๅˆ—่กจๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ๅผนๅ‡บ็š„ๅ…ƒ็ด  + */ + public Object lRightPop(String key) { + try { + return redisTemplate.opsForList().rightPop(key); + } catch (Exception e) { + log.error("ไปŽๅณไพงๅผนๅ‡บๅˆ—่กจๅ…ƒ็ด ๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ่Žทๅ–ๅˆ—่กจ่Œƒๅ›ด + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๅˆ†้กตๆŸฅ่ฏขๅˆ—่กจๆ•ฐๆฎ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๆŒ‡ๅฎš่Œƒๅ›ด็š„ๅ…ƒ็ด  + * 2. ๆ”ฏๆŒ่ดŸ็ดขๅผ•๏ผˆ-1 ่กจ็คบๆœ€ๅŽไธ€ไธชๅ…ƒ็ด ๏ผ‰ + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹็ดขๅผ• + * @param end ็ป“ๆŸ็ดขๅผ• + * @return ๅ…ƒ็ด ๅˆ—่กจ + */ + public List lRange(String key, long start, long end) { + try { + return redisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + log.error("่Žทๅ–ๅˆ—่กจ่Œƒๅ›ดๅคฑ่ดฅ - key: {}", key, e); + return List.of(); + } + } + + /** + * ่Žทๅ–ๅˆ—่กจ้•ฟๅบฆ + * + * @param key ้”ฎ + * @return ๅˆ—่กจ้•ฟๅบฆ + */ + public long lSize(String key) { + try { + Long size = redisTemplate.opsForList().size(key); + return size != null ? size : 0; + } catch (Exception e) { + log.error("่Žทๅ–ๅˆ—่กจ้•ฟๅบฆๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ็งป้™คๅˆ—่กจๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param count ็งป้™คๆ•ฐ้‡๏ผˆ>0 ไปŽๅทฆๅพ€ๅณ๏ผŒ<0 ไปŽๅณๅพ€ๅทฆ๏ผŒ=0 ๅ…จ้ƒจ๏ผ‰ + * @param value ่ฆ็งป้™ค็š„ๅ€ผ + * @return ๅฎž้™…็งป้™ค็š„ๆ•ฐ้‡ + */ + public long lRemove(String key, long count, Object value) { + try { + Long removed = redisTemplate.opsForList().remove(key, count, value); + return removed != null ? removed : 0; + } catch (Exception e) { + log.error("็งป้™คๅˆ—่กจๅ…ƒ็ด ๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + // ==================== 4. Set ็ฑปๅž‹ๆ“ไฝœ ==================== + + /** + * ๆทปๅŠ ๅˆฐ้›†ๅˆ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆ ‡็ญพ็ณป็ปŸใ€ๅ…ฑๅŒๅฅฝๅ‹ใ€ๅŽป้‡ + * ๆญฅ้ชค๏ผš + * 1. ๆทปๅŠ ๅ…ƒ็ด ๅˆฐ้›†ๅˆ + * 2. ่‡ชๅŠจๅŽป้‡ + * + * @param key ้”ฎ + * @param values ๅ€ผ้›†ๅˆ + * @return ๆทปๅŠ ็š„ๅ…ƒ็ด ๆ•ฐ้‡๏ผˆไธๅŒ…ๅซๅทฒๅญ˜ๅœจ็š„๏ผ‰ + */ + public long sAdd(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().add(key, values); + log.debug("ๆทปๅŠ ๅˆฐ้›†ๅˆ - key: {}, count: {}", key, count); + return count != null ? count : 0; + } catch (Exception e) { + log.error("ๆทปๅŠ ๅˆฐ้›†ๅˆๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ่Žทๅ–้›†ๅˆๆ‰€ๆœ‰ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ๅ…ƒ็ด ้›†ๅˆ + */ + public Set sMembers(String key) { + try { + return redisTemplate.opsForSet().members(key); + } catch (Exception e) { + log.error("่Žทๅ–้›†ๅˆๅ…ƒ็ด ๅคฑ่ดฅ - key: {}", key, e); + return Set.of(); + } + } + + /** + * ๅˆคๆ–ญๅ…ƒ็ด ๆ˜ฏๅฆๅœจ้›†ๅˆไธญ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ + public boolean sIsMember(String key, Object value) { + try { + Boolean result = redisTemplate.opsForSet().isMember(key, value); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("ๅˆคๆ–ญๅ…ƒ็ด ๆ˜ฏๅฆๅœจ้›†ๅˆไธญๅคฑ่ดฅ - key: {}", key, e); + return false; + } + } + + /** + * ่Žทๅ–้›†ๅˆๅคงๅฐ + * + * @param key ้”ฎ + * @return ้›†ๅˆๅคงๅฐ + */ + public long sSize(String key) { + try { + Long size = redisTemplate.opsForSet().size(key); + return size != null ? size : 0; + } catch (Exception e) { + log.error("่Žทๅ–้›†ๅˆๅคงๅฐๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ็งป้™ค้›†ๅˆๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param values ่ฆ็งป้™ค็š„ๅ€ผ + * @return ๅฎž้™…็งป้™ค็š„ๆ•ฐ้‡ + */ + public long sRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForSet().remove(key, values); + return count != null ? count : 0; + } catch (Exception e) { + log.error("็งป้™ค้›†ๅˆๅ…ƒ็ด ๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ้›†ๅˆไบค้›† + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๅ…ฑๅŒๅฅฝๅ‹ใ€ๅ…ฑๅŒๆ ‡็ญพ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅคšไธช้›†ๅˆ็š„ไบค้›† + * 2. ่ฟ”ๅ›žๅ…ฑๆœ‰็š„ๅ…ƒ็ด  + * + * @param key1 ็ฌฌไธ€ไธช้›†ๅˆ้”ฎ + * @param key2 ็ฌฌไบŒไธช้›†ๅˆ้”ฎ + * @return ไบค้›†ๅ…ƒ็ด  + */ + public Set sIntersect(String key1, String key2) { + try { + return redisTemplate.opsForSet().intersect(key1, key2); + } catch (Exception e) { + log.error("่Žทๅ–้›†ๅˆไบค้›†ๅคฑ่ดฅ - key1: {}, key2: {}", key1, key2, e); + return Set.of(); + } + } + + /** + * ้›†ๅˆๅนถ้›† + * + * @param key1 ็ฌฌไธ€ไธช้›†ๅˆ้”ฎ + * @param key2 ็ฌฌไบŒไธช้›†ๅˆ้”ฎ + * @return ๅนถ้›†ๅ…ƒ็ด  + */ + public Set sUnion(String key1, String key2) { + try { + return redisTemplate.opsForSet().union(key1, key2); + } catch (Exception e) { + log.error("่Žทๅ–้›†ๅˆๅนถ้›†ๅคฑ่ดฅ - key1: {}, key2: {}", key1, key2, e); + return Set.of(); + } + } + + // ==================== 5. ZSet ็ฑปๅž‹ๆ“ไฝœ๏ผˆๆœ‰ๅบ้›†ๅˆ๏ผ‰==================== + + /** + * ๆทปๅŠ ๅˆฐๆœ‰ๅบ้›†ๅˆ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆŽ’่กŒๆฆœใ€ๆƒ้‡้˜Ÿๅˆ— + * ๆญฅ้ชค๏ผš + * 1. ๆทปๅŠ ๅ…ƒ็ด ๅŠๅˆ†ๆ•ฐ + * 2. ๆŒ‰ๅˆ†ๆ•ฐ่‡ชๅŠจๆŽ’ๅบ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param score ๅˆ†ๆ•ฐ + * @return ๆทปๅŠ ๆ˜ฏๅฆๆˆๅŠŸ๏ผˆๆ–ฐๅขž่ฟ”ๅ›ž true๏ผŒๆ›ดๆ–ฐ่ฟ”ๅ›ž false๏ผ‰ + */ + public boolean zAdd(String key, Object value, double score) { + try { + Boolean result = redisTemplate.opsForZSet().add(key, value, score); + log.debug("ๆทปๅŠ ๅˆฐๆœ‰ๅบ้›†ๅˆ - key: {}, value: {}, score: {}", key, value, score); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("ๆทปๅŠ ๅˆฐๆœ‰ๅบ้›†ๅˆๅคฑ่ดฅ - key: {}", key, e); + return false; + } + } + + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆ่Œƒๅ›ด๏ผˆๆŒ‰ๅˆ†ๆ•ฐ๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆŽ’่กŒๆฆœๅˆ†้กตๆŸฅ่ฏข + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๆŒ‡ๅฎšๅˆ†ๆ•ฐ่Œƒๅ›ด็š„ๅ…ƒ็ด  + * 2. ๆŒ‰ๅˆ†ๆ•ฐๅ‡ๅบๆŽ’ๅˆ— + * + * @param key ้”ฎ + * @param min ๆœ€ๅฐๅˆ†ๆ•ฐ + * @param max ๆœ€ๅคงๅˆ†ๆ•ฐ + * @return ๅ…ƒ็ด ้›†ๅˆ + */ + public Set zRangeByScore(String key, double min, double max) { + try { + return redisTemplate.opsForZSet().rangeByScore(key, min, max); + } catch (Exception e) { + log.error("่Žทๅ–ๆœ‰ๅบ้›†ๅˆ่Œƒๅ›ดๅคฑ่ดฅ - key: {}", key, e); + return Set.of(); + } + } + + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆๆŽ’ๅ๏ผˆๅ€’ๅบ๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๆŽ’่กŒๆฆœๆŸฅ่ฏข๏ผˆๅˆ†ๆ•ฐ้ซ˜็š„ๆŽ’ๅ้ ๅ‰๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅ…ƒ็ด ็š„ๆŽ’ๅ๏ผˆไปŽ0ๅผ€ๅง‹๏ผ‰ + * 2. ๆŒ‰ๅˆ†ๆ•ฐๅ€’ๅบๆŽ’ๅˆ— + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆŽ’ๅ๏ผŒไธๅญ˜ๅœจ่ฟ”ๅ›ž null + */ + public Long zReverseRank(String key, Object value) { + try { + return redisTemplate.opsForZSet().reverseRank(key, value); + } catch (Exception e) { + log.error("่Žทๅ–ๆœ‰ๅบ้›†ๅˆๆŽ’ๅๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ่Žทๅ–ๅ…ƒ็ด ๅˆ†ๆ•ฐ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๅˆ†ๆ•ฐ + */ + public Double zScore(String key, Object value) { + try { + return redisTemplate.opsForZSet().score(key, value); + } catch (Exception e) { + log.error("่Žทๅ–ๅ…ƒ็ด ๅˆ†ๆ•ฐๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ๅขžๅŠ ๅ…ƒ็ด ๅˆ†ๆ•ฐ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param delta ๅขž้‡ + * @return ๅขžๅŠ ๅŽ็š„ๅˆ†ๆ•ฐ + */ + public Double zIncrementScore(String key, Object value, double delta) { + try { + return redisTemplate.opsForZSet().incrementScore(key, value, delta); + } catch (Exception e) { + log.error("ๅขžๅŠ ๅ…ƒ็ด ๅˆ†ๆ•ฐๅคฑ่ดฅ - key: {}", key, e); + return null; + } + } + + /** + * ็งป้™คๆœ‰ๅบ้›†ๅˆๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param values ่ฆ็งป้™ค็š„ๅ€ผ + * @return ๅฎž้™…็งป้™ค็š„ๆ•ฐ้‡ + */ + public long zRemove(String key, Object... values) { + try { + Long count = redisTemplate.opsForZSet().remove(key, values); + return count != null ? count : 0; + } catch (Exception e) { + log.error("็งป้™คๆœ‰ๅบ้›†ๅˆๅ…ƒ็ด ๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } + + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆๅคงๅฐ + * + * @param key ้”ฎ + * @return ้›†ๅˆๅคงๅฐ + */ + public long zSize(String key) { + try { + Long size = redisTemplate.opsForZSet().size(key); + return size != null ? size : 0; + } catch (Exception e) { + log.error("่Žทๅ–ๆœ‰ๅบ้›†ๅˆๅคงๅฐๅคฑ่ดฅ - key: {}", key, e); + return 0; + } + } +} diff --git a/src/main/java/com/aisi/template/utils/RedisLock.java b/src/main/java/com/aisi/template/utils/RedisLock.java new file mode 100644 index 0000000..653f292 --- /dev/null +++ b/src/main/java/com/aisi/template/utils/RedisLock.java @@ -0,0 +1,301 @@ +package com.aisi.template.utils; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Redis ๅˆ†ๅธƒๅผ้”ๅทฅๅ…ท็ฑป + * ๅŸบไบŽ Redis SETNX + EXPIRE ๅฎž็Žฐ๏ผŒๆ”ฏๆŒๅฏ้‡ๅ…ฅ้” + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * 1. ้˜ฒๆญข้‡ๅคๆไบค๏ผˆๅฆ‚่กจๅ•้‡ๅคๆไบค๏ผ‰ + * 2. ๅบ“ๅญ˜ๆ‰ฃๅ‡๏ผˆ้˜ฒๆญข่ถ…ๅ–๏ผ‰ + * 3. ๅฎšๆ—ถไปปๅŠกๅˆ†ๅธƒๅผๆ‰ง่กŒ๏ผˆ้˜ฒๆญข้‡ๅคๆ‰ง่กŒ๏ผ‰ + * 4. ้™ๆตๅœบๆ™ฏ + * + * @author Claude + * @since 2024-04-09 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisLock { + + /** + * Redis ๆจกๆฟ + */ + private final RedisTemplate redisTemplate; + + /** + * ้”ๅ‰็ผ€ + */ + private static final String LOCK_PREFIX = "lock:"; + + /** + * ้”็š„ๅ€ผๅ‰็ผ€ + */ + private static final String LOCK_VALUE_PREFIX = "uuid:"; + + /** + * ้ป˜่ฎค้”่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + */ + private static final long DEFAULT_EXPIRE_TIME = 30; + + /** + * ่Žทๅ–้”็š„ Lua ่„šๆœฌ + * SETNX + EXPIRE ๅŽŸๅญๆ“ไฝœ + */ + private static final String LOCK_SCRIPT = + "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " + + "redis.call('expire', KEYS[1], ARGV[2]) " + + "return 1 " + + "else " + + "return 0 " + + "end"; + + /** + * ้‡Šๆ”พ้”็š„ Lua ่„šๆœฌ + * ็กฎไฟๅชๆœ‰้”็š„ๆŒๆœ‰่€…ๆ‰่ƒฝ้‡Šๆ”พ้” + */ + private static final String UNLOCK_SCRIPT = + "if redis.call('get', KEYS[1]) == ARGV[1] then " + + "return redis.call('del', KEYS[1]) " + + "else " + + "return 0 " + + "end"; + + /** + * ๅฐ่ฏ•่Žทๅ–้” + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆๅ”ฏไธ€็š„้”ๆ ‡่ฏ†๏ผˆUUID๏ผ‰ + * 2. ๆ‰ง่กŒ Lua ่„šๆœฌๅฐ่ฏ•่Žทๅ–้” + * 3. ๅฆ‚ๆžœ่Žทๅ–ๆˆๅŠŸ๏ผŒ่ฟ”ๅ›ž้”ๆ ‡่ฏ†๏ผ›ๅฆๅˆ™่ฟ”ๅ›ž null + * + * @param lockKey ้”็š„้”ฎๅ + * @return ้”ๆ ‡่ฏ†๏ผŒๅฆ‚ๆžœ่Žทๅ–ๅคฑ่ดฅ่ฟ”ๅ›ž null + */ + public String tryLock(String lockKey) { + return tryLock(lockKey, DEFAULT_EXPIRE_TIME); + } + + /** + * ๅฐ่ฏ•่Žทๅ–้”๏ผˆๆŒ‡ๅฎš่ฟ‡ๆœŸๆ—ถ้—ด๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ็”Ÿๆˆๅ”ฏไธ€็š„้”ๆ ‡่ฏ†๏ผˆUUID๏ผ‰ + * 2. ๆ‰ง่กŒ Lua ่„šๆœฌๅฐ่ฏ•่Žทๅ–้”๏ผŒๅนถ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + * 3. ๅฆ‚ๆžœ่Žทๅ–ๆˆๅŠŸ๏ผŒ่ฟ”ๅ›ž้”ๆ ‡่ฏ†๏ผ›ๅฆๅˆ™่ฟ”ๅ›ž null + * + * @param lockKey ้”็š„้”ฎๅ + * @param expireTime ้”็š„่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * @return ้”ๆ ‡่ฏ†๏ผŒๅฆ‚ๆžœ่Žทๅ–ๅคฑ่ดฅ่ฟ”ๅ›ž null + */ + public String tryLock(String lockKey, long expireTime) { + // 1. ็”Ÿๆˆๅ”ฏไธ€็š„้”ๆ ‡่ฏ† + String lockValue = LOCK_VALUE_PREFIX + UUID.randomUUID().toString(); + String fullLockKey = LOCK_PREFIX + lockKey; + + try { + // 2. ๆ‰ง่กŒ Lua ่„šๆœฌ่Žทๅ–้” + // KEYS[1]: ้”็š„ๅฎŒๆ•ด้”ฎๅ + // ARGV[1]: ้”็š„ๅ€ผ๏ผˆUUID๏ผ‰ + // ARGV[2]: ่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + Long result = redisTemplate.execute( + RedisScript.of(LOCK_SCRIPT, Long.class), + Collections.singletonList(fullLockKey), + lockValue, + String.valueOf(expireTime) + ); + + // 3. ๅˆคๆ–ญๆ˜ฏๅฆ่Žทๅ–ๆˆๅŠŸ + if (result != null && result == 1) { + log.info("่Žทๅ–้”ๆˆๅŠŸ - lockKey: {}, lockValue: {}", lockKey, lockValue); + return lockValue; + } else { + log.warn("่Žทๅ–้”ๅคฑ่ดฅ - lockKey: {}", lockKey); + return null; + } + } catch (Exception e) { + log.error("่Žทๅ–้”ๅผ‚ๅธธ - lockKey: {}", lockKey, e); + return null; + } + } + + /** + * ๅฐ่ฏ•่Žทๅ–้”๏ผˆๅธฆ็ญ‰ๅพ…ๆ—ถ้—ด๏ผ‰ + * ๆญฅ้ชค๏ผš + * 1. ๅœจ็ญ‰ๅพ…ๆ—ถ้—ดๅ†…ๅพช็Žฏๅฐ่ฏ•่Žทๅ–้” + * 2. ๆฏๆฌก้—ด้š” 100 ๆฏซ็ง’้‡่ฏ• + * 3. ่ถ…่ฟ‡็ญ‰ๅพ…ๆ—ถ้—ดๅˆ™ๆ”พๅผƒ + * + * @param lockKey ้”็š„้”ฎๅ + * @param expireTime ้”็š„่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * @param waitTime ็ญ‰ๅพ…ๆ—ถ้—ด๏ผˆๆฏซ็ง’๏ผ‰ + * @return ้”ๆ ‡่ฏ†๏ผŒๅฆ‚ๆžœ่Žทๅ–ๅคฑ่ดฅ่ฟ”ๅ›ž null + */ + public String tryLock(String lockKey, long expireTime, long waitTime) { + long startTime = System.currentTimeMillis(); + long timeout = startTime + waitTime; + String lockValue = null; + + // 1. ๅœจ็ญ‰ๅพ…ๆ—ถ้—ดๅ†…ๅพช็Žฏๅฐ่ฏ• + while (System.currentTimeMillis() < timeout) { + // 2. ๅฐ่ฏ•่Žทๅ–้” + lockValue = tryLock(lockKey, expireTime); + if (lockValue != null) { + return lockValue; + } + + // 3. ็Ÿญๆš‚ไผ‘็œ ๅŽ้‡่ฏ• + try { + TimeUnit.MILLISECONDS.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } + } + + log.warn("่Žทๅ–้”่ถ…ๆ—ถ - lockKey: {}, waitTime: {}ms", lockKey, waitTime); + return null; + } + + /** + * ้‡Šๆ”พ้” + * ๆญฅ้ชค๏ผš + * 1. ้ชŒ่ฏ้”็š„ๅ€ผๆ˜ฏๅฆๅŒน้…๏ผˆ็กฎไฟๅชๆœ‰ๆŒๆœ‰่€…ๆ‰่ƒฝ้‡Šๆ”พ๏ผ‰ + * 2. ๅฆ‚ๆžœๅŒน้…ๅˆ™ๅˆ ้™ค้”๏ผŒๅฆๅˆ™ไธๅšๆ“ไฝœ + * + * @param lockKey ้”็š„้”ฎๅ + * @param lockValue ้”็š„ๆ ‡่ฏ†๏ผˆtryLock ่ฟ”ๅ›ž็š„ๅ€ผ๏ผ‰ + * @return ๆ˜ฏๅฆ้‡Šๆ”พๆˆๅŠŸ + */ + public boolean unlock(String lockKey, String lockValue) { + if (lockValue == null) { + return false; + } + + String fullLockKey = LOCK_PREFIX + lockKey; + + try { + // 1. ๆ‰ง่กŒ Lua ่„šๆœฌ้‡Šๆ”พ้” + // KEYS[1]: ้”็š„ๅฎŒๆ•ด้”ฎๅ + // ARGV[1]: ้”็š„ๅ€ผ๏ผˆ็”จไบŽ้ชŒ่ฏ๏ผ‰ + Long result = redisTemplate.execute( + RedisScript.of(UNLOCK_SCRIPT, Long.class), + Collections.singletonList(fullLockKey), + lockValue + ); + + // 2. ๅˆคๆ–ญๆ˜ฏๅฆ้‡Šๆ”พๆˆๅŠŸ + if (result != null && result == 1) { + log.info("้‡Šๆ”พ้”ๆˆๅŠŸ - lockKey: {}, lockValue: {}", lockKey, lockValue); + return true; + } else { + log.warn("้‡Šๆ”พ้”ๅคฑ่ดฅ๏ผŒ้”ไธๅญ˜ๅœจๆˆ–ๅทฒ่ฟ‡ๆœŸ - lockKey: {}, lockValue: {}", lockKey, lockValue); + return false; + } + } catch (Exception e) { + log.error("้‡Šๆ”พ้”ๅผ‚ๅธธ - lockKey: {}, lockValue: {}", lockKey, lockValue, e); + return false; + } + } + + /** + * ๅผบๅˆถ้‡Šๆ”พ้”๏ผˆไธ้ชŒ่ฏๆŒๆœ‰่€…๏ผ‰ + * ๆณจๆ„๏ผš่ฟ™ไธชๆ–นๆณ•ไผšๅผบๅˆถๅˆ ้™ค้”๏ผŒๅฏ่ƒฝๅฏผ่‡ดๅ…ถไป–ๆŒๆœ‰่€…็š„้”่ขซ่ฏฏๅˆ  + * ไป…ๅœจ็‰นๆฎŠๅœบๆ™ฏไฝฟ็”จ๏ผŒๅฆ‚ๆธ…็†ๆญป้” + * + * @param lockKey ้”็š„้”ฎๅ + * @return ๆ˜ฏๅฆๅˆ ้™คๆˆๅŠŸ + */ + public boolean forceUnlock(String lockKey) { + String fullLockKey = LOCK_PREFIX + lockKey; + try { + Boolean result = redisTemplate.delete(fullLockKey); + log.warn("ๅผบๅˆถ้‡Šๆ”พ้” - lockKey: {}, result: {}", lockKey, result); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("ๅผบๅˆถ้‡Šๆ”พ้”ๅผ‚ๅธธ - lockKey: {}", lockKey, e); + return false; + } + } + + /** + * ๆฃ€ๆŸฅ้”ๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param lockKey ้”็š„้”ฎๅ + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ + public boolean isLocked(String lockKey) { + String fullLockKey = LOCK_PREFIX + lockKey; + try { + Boolean result = redisTemplate.hasKey(fullLockKey); + return Boolean.TRUE.equals(result); + } catch (Exception e) { + log.error("ๆฃ€ๆŸฅ้”็Šถๆ€ๅผ‚ๅธธ - lockKey: {}", lockKey, e); + return false; + } + } + + /** + * ่Žทๅ–้”็š„ๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param lockKey ้”็š„้”ฎๅ + * @return ๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰๏ผŒ-1 ่กจ็คบไธๅญ˜ๅœจๆˆ–ๅทฒ่ฟ‡ๆœŸ + */ + public long getLockExpireTime(String lockKey) { + String fullLockKey = LOCK_PREFIX + lockKey; + try { + Long expireTime = redisTemplate.getExpire(fullLockKey, TimeUnit.SECONDS); + return expireTime != null ? expireTime : -1; + } catch (Exception e) { + log.error("่Žทๅ–้”่ฟ‡ๆœŸๆ—ถ้—ดๅผ‚ๅธธ - lockKey: {}", lockKey, e); + return -1; + } + } + + /** + * ็ปญๆœŸ้”๏ผˆๅปถ้•ฟ้”็š„่ฟ‡ๆœŸๆ—ถ้—ด๏ผ‰ + * ๆณจๆ„๏ผš้œ€่ฆ้ชŒ่ฏๆŒๆœ‰่€… + * + * @param lockKey ้”็š„้”ฎๅ + * @param lockValue ้”็š„ๆ ‡่ฏ† + * @param addTime ๅขžๅŠ ็š„ๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * @return ๆ˜ฏๅฆ็ปญๆœŸๆˆๅŠŸ + */ + public boolean renewLock(String lockKey, String lockValue, long addTime) { + if (lockValue == null) { + return false; + } + + String fullLockKey = LOCK_PREFIX + lockKey; + + try { + // 1. ๅ…ˆ้ชŒ่ฏ้”็š„ๅ€ผ + String currentValue = (String) redisTemplate.opsForValue().get(fullLockKey); + if (!lockValue.equals(currentValue)) { + log.warn("็ปญๆœŸๅคฑ่ดฅ๏ผŒ้”็š„ๅ€ผไธๅŒน้… - lockKey: {}", lockKey); + return false; + } + + // 2. ็ปญๆœŸ้” + Boolean result = redisTemplate.expire(fullLockKey, addTime, TimeUnit.SECONDS); + if (Boolean.TRUE.equals(result)) { + log.info("็ปญๆœŸ้”ๆˆๅŠŸ - lockKey: {}, addTime: {}s", lockKey, addTime); + return true; + } else { + log.warn("็ปญๆœŸ้”ๅคฑ่ดฅ - lockKey: {}", lockKey); + return false; + } + } catch (Exception e) { + log.error("็ปญๆœŸ้”ๅผ‚ๅธธ - lockKey: {}", lockKey, e); + return false; + } + } +} diff --git a/src/main/java/com/aisi/template/utils/RedisUtils.java b/src/main/java/com/aisi/template/utils/RedisUtils.java index 1bb4827..fa3c0e9 100644 --- a/src/main/java/com/aisi/template/utils/RedisUtils.java +++ b/src/main/java/com/aisi/template/utils/RedisUtils.java @@ -1,6 +1,7 @@ package com.aisi.template.utils; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Component; @@ -11,69 +12,187 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +/** + * Redis ๅทฅๅ…ท็ฑป + * ๆไพ›ๅ„็ง Redis ๆ•ฐๆฎ็ฑปๅž‹็š„ไพฟๆทๆ“ไฝœๆ–นๆณ• + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. String ็ฑปๅž‹๏ผšๅญ—็ฌฆไธฒใ€ๅฏน่ฑก็ผ“ๅญ˜ + * 2. Hash ็ฑปๅž‹๏ผšๅฏน่ฑกๅญ—ๆฎต็ผ“ๅญ˜ + * 3. List ็ฑปๅž‹๏ผš้˜Ÿๅˆ—ใ€ๅˆ—่กจ + * 4. Set ็ฑปๅž‹๏ผšๅŽป้‡ใ€ไบค้›†ใ€ๅนถ้›† + * 5. ZSet ็ฑปๅž‹๏ผšๆŽ’่กŒๆฆœ + * 6. Bitmap๏ผšไฝๅ›พๆ“ไฝœ + * 7. HyperLogLog๏ผšๅŸบๆ•ฐ็ปŸ่ฎก + * + * @author Claude + * @since 2024-04-09 + */ @Component @RequiredArgsConstructor public class RedisUtils { + /** + * Redis ๆจกๆฟ + */ private final RedisTemplate redisTemplate; - // ======================== ้€š็”จ ======================== + /** + * ๅญ—็ฌฆไธฒ Redis ๆจกๆฟ๏ผŒ็”จไบŽๆ•ฐๅ€ผ่‡ชๅขž็ญ‰ๅŽŸ็”Ÿๅ‘ฝไปค + */ + private final StringRedisTemplate stringRedisTemplate; + // ======================== 1. ้€š็”จๆ“ไฝœ ======================== + + /** + * ๅˆ ้™ค้”ฎ + * + * @param key ้”ฎ + * @return ๆ˜ฏๅฆๅˆ ้™คๆˆๅŠŸ + */ public Boolean delete(String key) { return redisTemplate.delete(key); } + /** + * ๆ‰น้‡ๅˆ ้™ค้”ฎ + * + * @param keys ้”ฎ้›†ๅˆ + * @return ๅˆ ้™ค็š„ๆ•ฐ้‡ + */ public Long delete(Collection keys) { return redisTemplate.delete(keys); } + /** + * ๅˆคๆ–ญ้”ฎๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param key ้”ฎ + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } + /** + * ่ฎพ็ฝฎ่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param key ้”ฎ + * @param timeout ่ฟ‡ๆœŸๆ—ถ้—ด + * @param unit ๆ—ถ้—ดๅ•ไฝ + * @return ๆ˜ฏๅฆ่ฎพ็ฝฎๆˆๅŠŸ + */ public Boolean expire(String key, long timeout, TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } + /** + * ่Žทๅ–่ฟ‡ๆœŸๆ—ถ้—ด + * + * @param key ้”ฎ + * @return ๅ‰ฉไฝ™่ฟ‡ๆœŸๆ—ถ้—ด๏ผˆ็ง’๏ผ‰๏ผŒ-1 ่กจ็คบๆฐธไธ่ฟ‡ๆœŸ๏ผŒ-2 ่กจ็คบ้”ฎไธๅญ˜ๅœจ + */ public Long getExpire(String key) { return redisTemplate.getExpire(key); } + /** + * ๅœจๆŒ‡ๅฎšๆ—ถ้—ดๆˆณ่ฟ‡ๆœŸ + * + * @param key ้”ฎ + * @param timestamp ๆ—ถ้—ดๆˆณ๏ผˆๆฏซ็ง’๏ผ‰ + * @return ๆ˜ฏๅฆ่ฎพ็ฝฎๆˆๅŠŸ + */ public Boolean expireAt(String key, long timestamp) { return redisTemplate.expireAt(key, new java.util.Date(timestamp)); } + /** + * ๆŸฅๆ‰พๅŒน้…ๆจกๅผ็š„้”ฎ + * + * @param pattern ๆจกๅผ๏ผˆๅฆ‚๏ผšuser:*๏ผ‰ + * @return ๅŒน้…็š„้”ฎ้›†ๅˆ + */ public Set keys(String pattern) { return redisTemplate.keys(pattern); } + /** + * ้‡ๅ‘ฝๅ้”ฎ + * + * @param oldKey ๆ—ง้”ฎๅ + * @param newKey ๆ–ฐ้”ฎๅ + * @return ๆ˜ฏๅฆ้‡ๅ‘ฝๅๆˆๅŠŸ + */ public Boolean rename(String oldKey, String newKey) { redisTemplate.rename(oldKey, newKey); return true; } + /** + * ่Žทๅ–้”ฎ็š„ๅ‰ฉไฝ™็”Ÿๅญ˜ๆ—ถ้—ด๏ผˆ็ง’๏ผ‰ + * + * @param key ้”ฎ + * @return ๅ‰ฉไฝ™็ง’ๆ•ฐ + */ public Long ttl(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } - // ======================== String ======================== + // ======================== 2. String ็ฑปๅž‹ๆ“ไฝœ ======================== + /** + * ่ฎพ็ฝฎ้”ฎๅ€ผ + * + * @param key ้”ฎ + * @param value ๅ€ผ + */ public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } + /** + * ่ฎพ็ฝฎ้”ฎๅ€ผ๏ผˆๅธฆ่ฟ‡ๆœŸๆ—ถ้—ด๏ผ‰ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param timeout ่ฟ‡ๆœŸๆ—ถ้—ด + * @param unit ๆ—ถ้—ดๅ•ไฝ + */ public void set(String key, Object value, long timeout, TimeUnit unit) { redisTemplate.opsForValue().set(key, value, timeout, unit); } + /** + * ๅฆ‚ๆžœ้”ฎไธๅญ˜ๅœจๅˆ™่ฎพ็ฝฎ๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰ + * ไฝฟ็”จๅœบๆ™ฏ๏ผšๅˆ†ๅธƒๅผ้” + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param timeout ่ฟ‡ๆœŸๆ—ถ้—ด + * @param unit ๆ—ถ้—ดๅ•ไฝ + */ public void setIfAbsent(String key, Object value, long timeout, TimeUnit unit) { redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit); } + /** + * ่Žทๅ–้”ฎๅ€ผ + * + * @param key ้”ฎ + * @return ๅ€ผ + */ public Object get(String key) { return redisTemplate.opsForValue().get(key); } + /** + * ่Žทๅ–้”ฎๅ€ผ๏ผˆๆŒ‡ๅฎš็ฑปๅž‹๏ผ‰ + * + * @param key ้”ฎ + * @param clazz ็ฑปๅž‹ + * @return ๅ€ผ + */ public T get(String key, Class clazz) { Object value = redisTemplate.opsForValue().get(key); if (value == null) { @@ -82,255 +201,680 @@ public class RedisUtils { return clazz.cast(value); } + /** + * ่Žทๅ–ๅนถ่ฎพ็ฝฎๆ–ฐๅ€ผ + * + * @param key ้”ฎ + * @param value ๆ–ฐๅ€ผ + * @return ๆ—งๅ€ผ + */ public String getAndSet(String key, Object value) { Object old = redisTemplate.opsForValue().getAndSet(key, value); return old != null ? old.toString() : null; } + /** + * ่‡ชๅขž๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰ + * + * @param key ้”ฎ + * @return ่‡ชๅขžๅŽ็š„ๅ€ผ + */ public Long increment(String key) { - return redisTemplate.opsForValue().increment(key); + return stringRedisTemplate.opsForValue().increment(key); } + /** + * ่‡ชๅขžๆŒ‡ๅฎšๅขž้‡๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰ + * + * @param key ้”ฎ + * @param delta ๅขž้‡ + * @return ่‡ชๅขžๅŽ็š„ๅ€ผ + */ public Long increment(String key, long delta) { - return redisTemplate.opsForValue().increment(key, delta); + return stringRedisTemplate.opsForValue().increment(key, delta); } + /** + * ่‡ชๅ‡๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰ + * + * @param key ้”ฎ + * @return ่‡ชๅ‡ๅŽ็š„ๅ€ผ + */ public Long decrement(String key) { - return redisTemplate.opsForValue().decrement(key); + return stringRedisTemplate.opsForValue().decrement(key); } + /** + * ่‡ชๅ‡ๆŒ‡ๅฎšๅ‡้‡๏ผˆๅŽŸๅญๆ“ไฝœ๏ผ‰ + * + * @param key ้”ฎ + * @param delta ๅ‡้‡ + * @return ่‡ชๅ‡ๅŽ็š„ๅ€ผ + */ public Long decrement(String key, long delta) { - return redisTemplate.opsForValue().decrement(key, delta); + return stringRedisTemplate.opsForValue().decrement(key, delta); } + /** + * ่Žทๅ–ๅญ—็ฌฆไธฒ้•ฟๅบฆ + * + * @param key ้”ฎ + * @return ๅญ—็ฌฆไธฒ้•ฟๅบฆ + */ public Long strLength(String key) { return redisTemplate.opsForValue().size(key); } - // ======================== Hash ======================== + // ======================== 3. Hash ็ฑปๅž‹ๆ“ไฝœ ======================== + /** + * ่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param hashKey ๅ“ˆๅธŒ้”ฎ + * @param value ๅ€ผ + */ public void hSet(String key, String hashKey, Object value) { redisTemplate.opsForHash().put(key, hashKey, value); } + /** + * ๆ‰น้‡่ฎพ็ฝฎๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param map ๅ“ˆๅธŒ้”ฎๅ€ผๆ˜ ๅฐ„ + */ public void hSetAll(String key, Map map) { redisTemplate.opsForHash().putAll(key, map); } + /** + * ่Žทๅ–ๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param hashKey ๅ“ˆๅธŒ้”ฎ + * @return ๅ€ผ + */ public Object hGet(String key, String hashKey) { return redisTemplate.opsForHash().get(key, hashKey); } + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @return ๅ“ˆๅธŒ้”ฎๅ€ผๆ˜ ๅฐ„ + */ public Map hGetAll(String key) { return redisTemplate.opsForHash().entries(key); } + /** + * ๅˆ ้™คๅ“ˆๅธŒๅญ—ๆฎต + * + * @param key ้”ฎ + * @param hashKeys ๅ“ˆๅธŒ้”ฎ้›†ๅˆ + */ public void hDelete(String key, Object... hashKeys) { redisTemplate.opsForHash().delete(key, hashKeys); } + /** + * ๅˆคๆ–ญๅ“ˆๅธŒๅญ—ๆฎตๆ˜ฏๅฆๅญ˜ๅœจ + * + * @param key ้”ฎ + * @param hashKey ๅ“ˆๅธŒ้”ฎ + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ public Boolean hHasKey(String key, String hashKey) { return redisTemplate.opsForHash().hasKey(key, hashKey); } + /** + * ่Žทๅ–ๅ“ˆๅธŒๅคงๅฐ + * + * @param key ้”ฎ + * @return ๅ“ˆๅธŒๅคงๅฐ + */ public Long hSize(String key) { return redisTemplate.opsForHash().size(key); } + /** + * ๅ“ˆๅธŒๅญ—ๆฎต่‡ชๅขž + * + * @param key ้”ฎ + * @param hashKey ๅ“ˆๅธŒ้”ฎ + * @param delta ๅขž้‡ + * @return ่‡ชๅขžๅŽ็š„ๅ€ผ + */ public Long hIncrement(String key, String hashKey, long delta) { return redisTemplate.opsForHash().increment(key, hashKey, delta); } + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๅ“ˆๅธŒ้”ฎ + * + * @param key ้”ฎ + * @return ๅ“ˆๅธŒ้”ฎ้›†ๅˆ + */ public Set hKeys(String key) { return redisTemplate.opsForHash().keys(key); } + /** + * ่Žทๅ–ๆ‰€ๆœ‰ๅ“ˆๅธŒๅ€ผ + * + * @param key ้”ฎ + * @return ๅ“ˆๅธŒๅ€ผๅˆ—่กจ + */ public List hValues(String key) { return redisTemplate.opsForHash().values(key); } - // ======================== List ======================== + // ======================== 4. List ็ฑปๅž‹ๆ“ไฝœ ======================== + /** + * ไปŽๅทฆไพงๆŽจๅ…ฅๅˆ—่กจ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๅˆ—่กจ้•ฟๅบฆ + */ public Long lPush(String key, Object value) { return redisTemplate.opsForList().leftPush(key, value); } + /** + * ไปŽๅทฆไพงๆ‰น้‡ๆŽจๅ…ฅๅˆ—่กจ + * + * @param key ้”ฎ + * @param values ๅ€ผๆ•ฐ็ป„ + * @return ๅˆ—่กจ้•ฟๅบฆ + */ public Long lPushAll(String key, Object... values) { return redisTemplate.opsForList().leftPushAll(key, values); } + /** + * ไปŽๅณไพงๆŽจๅ…ฅๅˆ—่กจ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๅˆ—่กจ้•ฟๅบฆ + */ public Long rPush(String key, Object value) { return redisTemplate.opsForList().rightPush(key, value); } + /** + * ไปŽๅณไพงๆ‰น้‡ๆŽจๅ…ฅๅˆ—่กจ + * + * @param key ้”ฎ + * @param values ๅ€ผๆ•ฐ็ป„ + * @return ๅˆ—่กจ้•ฟๅบฆ + */ public Long rPushAll(String key, Object... values) { return redisTemplate.opsForList().rightPushAll(key, values); } + /** + * ไปŽๅทฆไพงๅผนๅ‡บๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ๅผนๅ‡บ็š„ๅ…ƒ็ด  + */ public Object lPop(String key) { return redisTemplate.opsForList().leftPop(key); } + /** + * ไปŽๅทฆไพงๆ‰น้‡ๅผนๅ‡บๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param count ๆ•ฐ้‡ + * @return ๅผนๅ‡บ็š„ๅ…ƒ็ด ๅˆ—่กจ + */ public List lPop(String key, long count) { return redisTemplate.opsForList().leftPop(key, count); } + /** + * ไปŽๅณไพงๅผนๅ‡บๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ๅผนๅ‡บ็š„ๅ…ƒ็ด  + */ public Object rPop(String key) { return redisTemplate.opsForList().rightPop(key); } + /** + * ไปŽๅณไพงๆ‰น้‡ๅผนๅ‡บๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param count ๆ•ฐ้‡ + * @return ๅผนๅ‡บ็š„ๅ…ƒ็ด ๅˆ—่กจ + */ public List rPop(String key, long count) { return redisTemplate.opsForList().rightPop(key, count); } + /** + * ่Žทๅ–ๅˆ—่กจๆŒ‡ๅฎš็ดขๅผ•็š„ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param index ็ดขๅผ•๏ผˆ0 ่กจ็คบ็ฌฌไธ€ไธช๏ผ‰ + * @return ๅ…ƒ็ด  + */ public Object lIndex(String key, long index) { return redisTemplate.opsForList().index(key, index); } + /** + * ่Žทๅ–ๅˆ—่กจ้•ฟๅบฆ + * + * @param key ้”ฎ + * @return ๅˆ—่กจ้•ฟๅบฆ + */ public Long lSize(String key) { return redisTemplate.opsForList().size(key); } + /** + * ่Žทๅ–ๅˆ—่กจ่Œƒๅ›ด + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹็ดขๅผ• + * @param end ็ป“ๆŸ็ดขๅผ• + * @return ๅ…ƒ็ด ๅˆ—่กจ + */ public List lRange(String key, long start, long end) { return redisTemplate.opsForList().range(key, start, end); } + /** + * ่ฃๅ‰ชๅˆ—่กจ๏ผˆๅชไฟ็•™ๆŒ‡ๅฎš่Œƒๅ›ด็š„ๅ…ƒ็ด ๏ผ‰ + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹็ดขๅผ• + * @param end ็ป“ๆŸ็ดขๅผ• + */ public void lTrim(String key, long start, long end) { redisTemplate.opsForList().trim(key, start, end); } + /** + * ่ฎพ็ฝฎๅˆ—่กจๆŒ‡ๅฎš็ดขๅผ•็š„ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param index ็ดขๅผ• + * @param value ๅ€ผ + */ public void lSet(String key, long index, Object value) { redisTemplate.opsForList().set(key, index, value); } + /** + * ็งป้™คๅˆ—่กจๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param count ็งป้™คๆ•ฐ้‡๏ผˆ>0 ไปŽๅทฆๅพ€ๅณ๏ผŒ<0 ไปŽๅณๅพ€ๅทฆ๏ผŒ=0 ๅ…จ้ƒจ๏ผ‰ + * @param value ่ฆ็งป้™ค็š„ๅ€ผ + * @return ๅฎž้™…็งป้™ค็š„ๆ•ฐ้‡ + */ public Long lRemove(String key, long count, Object value) { return redisTemplate.opsForList().remove(key, count, value); } - // ======================== Set ======================== + // ======================== 5. Set ็ฑปๅž‹ๆ“ไฝœ ======================== + /** + * ๆทปๅŠ ๅˆฐ้›†ๅˆ + * + * @param key ้”ฎ + * @param values ๅ€ผๆ•ฐ็ป„ + * @return ๆทปๅŠ ็š„ๅ…ƒ็ด ๆ•ฐ้‡๏ผˆไธๅŒ…ๅซๅทฒๅญ˜ๅœจ็š„๏ผ‰ + */ public Long sAdd(String key, Object... values) { return redisTemplate.opsForSet().add(key, values); } + /** + * ไปŽ้›†ๅˆ็งป้™คๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param values ๅ€ผๆ•ฐ็ป„ + * @return ๅฎž้™…็งป้™ค็š„ๆ•ฐ้‡ + */ public Long sRemove(String key, Object... values) { return redisTemplate.opsForSet().remove(key, values); } + /** + * ่Žทๅ–้›†ๅˆๆ‰€ๆœ‰ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ๅ…ƒ็ด ้›†ๅˆ + */ public Set sMembers(String key) { return redisTemplate.opsForSet().members(key); } + /** + * ๅˆคๆ–ญๅ…ƒ็ด ๆ˜ฏๅฆๅœจ้›†ๅˆไธญ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆ˜ฏๅฆๅญ˜ๅœจ + */ public Boolean sIsMember(String key, Object value) { return redisTemplate.opsForSet().isMember(key, value); } + /** + * ่Žทๅ–้›†ๅˆๅคงๅฐ + * + * @param key ้”ฎ + * @return ้›†ๅˆๅคงๅฐ + */ public Long sSize(String key) { return redisTemplate.opsForSet().size(key); } + /** + * ้šๆœบ่Žทๅ–้›†ๅˆไธญ็š„ไธ€ไธชๅ…ƒ็ด  + * + * @param key ้”ฎ + * @return ้šๆœบๅ…ƒ็ด  + */ public Object sRandomMember(String key) { return redisTemplate.opsForSet().randomMember(key); } + /** + * ้šๆœบ่Žทๅ–้›†ๅˆไธญ็š„ๅคšไธชๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param count ๆ•ฐ้‡ + * @return ้šๆœบๅ…ƒ็ด ้›†ๅˆ + */ public Set sRandomMembers(String key, long count) { return redisTemplate.opsForSet().distinctRandomMembers(key, count); } + /** + * ้›†ๅˆไบค้›† + * + * @param key1 ็ฌฌไธ€ไธช้›†ๅˆ้”ฎ + * @param key2 ็ฌฌไบŒไธช้›†ๅˆ้”ฎ + * @return ไบค้›†ๅ…ƒ็ด  + */ public Set sIntersect(String key1, String key2) { return redisTemplate.opsForSet().intersect(key1, key2); } + /** + * ้›†ๅˆๅนถ้›† + * + * @param key1 ็ฌฌไธ€ไธช้›†ๅˆ้”ฎ + * @param key2 ็ฌฌไบŒไธช้›†ๅˆ้”ฎ + * @return ๅนถ้›†ๅ…ƒ็ด  + */ public Set sUnion(String key1, String key2) { return redisTemplate.opsForSet().union(key1, key2); } + /** + * ้›†ๅˆๅทฎ้›†๏ผˆkey1 - key2๏ผ‰ + * + * @param key1 ็ฌฌไธ€ไธช้›†ๅˆ้”ฎ + * @param key2 ็ฌฌไบŒไธช้›†ๅˆ้”ฎ + * @return ๅทฎ้›†ๅ…ƒ็ด  + */ public Set sDifference(String key1, String key2) { return redisTemplate.opsForSet().difference(key1, key2); } - // ======================== ZSet๏ผˆๆœ‰ๅบ้›†ๅˆ๏ผ‰======================== + // ======================== 6. ZSet ็ฑปๅž‹ๆ“ไฝœ๏ผˆๆœ‰ๅบ้›†ๅˆ๏ผ‰==================== + /** + * ๆทปๅŠ ๅˆฐๆœ‰ๅบ้›†ๅˆ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @param score ๅˆ†ๆ•ฐ + * @return ๆ˜ฏๅฆๆทปๅŠ ๆˆๅŠŸ + */ public Boolean zAdd(String key, Object value, double score) { return redisTemplate.opsForZSet().add(key, value, score); } + /** + * ๆ‰น้‡ๆทปๅŠ ๅˆฐๆœ‰ๅบ้›†ๅˆ + * + * @param key ้”ฎ + * @param tuples ๅˆ†ๆ•ฐ-ๅ€ผๅฏน้›†ๅˆ + * @return ๆทปๅŠ ็š„ๆ•ฐ้‡ + */ public Long zAdd(String key, Set> tuples) { return redisTemplate.opsForZSet().add(key, tuples); } + /** + * ไปŽๆœ‰ๅบ้›†ๅˆ็งป้™คๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param values ่ฆ็งป้™ค็š„ๅ€ผ + * @return ็งป้™ค็š„ๆ•ฐ้‡ + */ public Long zRemove(String key, Object... values) { return redisTemplate.opsForZSet().remove(key, values); } + /** + * ่Žทๅ–ๅ…ƒ็ด ๆŽ’ๅ๏ผˆๅ‡ๅบ๏ผ‰ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆŽ’ๅ๏ผˆไปŽ 0 ๅผ€ๅง‹๏ผ‰๏ผŒไธๅญ˜ๅœจ่ฟ”ๅ›ž null + */ public Long zRank(String key, Object value) { return redisTemplate.opsForZSet().rank(key, value); } + /** + * ่Žทๅ–ๅ…ƒ็ด ๆŽ’ๅ๏ผˆ้™ๅบ๏ผ‰ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๆŽ’ๅ๏ผˆไปŽ 0 ๅผ€ๅง‹๏ผ‰๏ผŒไธๅญ˜ๅœจ่ฟ”ๅ›ž null + */ public Long zReverseRank(String key, Object value) { return redisTemplate.opsForZSet().reverseRank(key, value); } + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆ่Œƒๅ›ด๏ผˆๅ‡ๅบ๏ผ‰ + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹็ดขๅผ• + * @param end ็ป“ๆŸ็ดขๅผ• + * @return ๅ…ƒ็ด ้›†ๅˆ + */ public Set zRange(String key, long start, long end) { return redisTemplate.opsForZSet().range(key, start, end); } + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆ่Œƒๅ›ด๏ผˆ้™ๅบ๏ผ‰ + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹็ดขๅผ• + * @param end ็ป“ๆŸ็ดขๅผ• + * @return ๅ…ƒ็ด ้›†ๅˆ + */ public Set zReverseRange(String key, long start, long end) { return redisTemplate.opsForZSet().reverseRange(key, start, end); } + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆ่Œƒๅ›ด๏ผˆๅธฆๅˆ†ๆ•ฐ๏ผŒๅ‡ๅบ๏ผ‰ + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹็ดขๅผ• + * @param end ็ป“ๆŸ็ดขๅผ• + * @return ๅˆ†ๆ•ฐ-ๅ€ผๅฏน้›†ๅˆ + */ public Set> zRangeWithScores(String key, long start, long end) { return redisTemplate.opsForZSet().rangeWithScores(key, start, end); } + /** + * ่Žทๅ–ๆŒ‡ๅฎšๅˆ†ๆ•ฐ่Œƒๅ›ด็š„ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param min ๆœ€ๅฐๅˆ†ๆ•ฐ + * @param max ๆœ€ๅคงๅˆ†ๆ•ฐ + * @return ๅ…ƒ็ด ้›†ๅˆ + */ public Set zRangeByScore(String key, double min, double max) { return redisTemplate.opsForZSet().rangeByScore(key, min, max); } + /** + * ่Žทๅ–ๆŒ‡ๅฎšๅˆ†ๆ•ฐ่Œƒๅ›ด็š„ๅ…ƒ็ด ๏ผˆๅˆ†้กต๏ผ‰ + * + * @param key ้”ฎ + * @param min ๆœ€ๅฐๅˆ†ๆ•ฐ + * @param max ๆœ€ๅคงๅˆ†ๆ•ฐ + * @param offset ๅ็งป้‡ + * @param count ๆ•ฐ้‡ + * @return ๅ…ƒ็ด ้›†ๅˆ + */ public Set zRangeByScore(String key, double min, double max, long offset, long count) { return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count); } + /** + * ็ปŸ่ฎกๆŒ‡ๅฎšๅˆ†ๆ•ฐ่Œƒๅ›ดๅ†…็š„ๅ…ƒ็ด ๆ•ฐ้‡ + * + * @param key ้”ฎ + * @param min ๆœ€ๅฐๅˆ†ๆ•ฐ + * @param max ๆœ€ๅคงๅˆ†ๆ•ฐ + * @return ๅ…ƒ็ด ๆ•ฐ้‡ + */ public Long zCount(String key, double min, double max) { return redisTemplate.opsForZSet().count(key, min, max); } + /** + * ่Žทๅ–ๆœ‰ๅบ้›†ๅˆๅคงๅฐ + * + * @param key ้”ฎ + * @return ้›†ๅˆๅคงๅฐ + */ public Long zSize(String key) { return redisTemplate.opsForZSet().size(key); } + /** + * ่Žทๅ–ๅ…ƒ็ด ๅˆ†ๆ•ฐ + * + * @param key ้”ฎ + * @param value ๅ€ผ + * @return ๅˆ†ๆ•ฐ + */ public Double zScore(String key, Object value) { return redisTemplate.opsForZSet().score(key, value); } + /** + * ็งป้™คๆŒ‡ๅฎšๆŽ’ๅ่Œƒๅ›ด็š„ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param start ๅผ€ๅง‹ๆŽ’ๅ + * @param end ็ป“ๆŸๆŽ’ๅ + * @return ็งป้™ค็š„ๆ•ฐ้‡ + */ public Long zRemoveRange(String key, long start, long end) { return redisTemplate.opsForZSet().removeRange(key, start, end); } + /** + * ็งป้™คๆŒ‡ๅฎšๅˆ†ๆ•ฐ่Œƒๅ›ด็š„ๅ…ƒ็ด  + * + * @param key ้”ฎ + * @param min ๆœ€ๅฐๅˆ†ๆ•ฐ + * @param max ๆœ€ๅคงๅˆ†ๆ•ฐ + * @return ็งป้™ค็š„ๆ•ฐ้‡ + */ public Long zRemoveRangeByScore(String key, double min, double max) { return redisTemplate.opsForZSet().removeRangeByScore(key, min, max); } - // ======================== Bitmap ======================== + // ======================== 7. Bitmap ็ฑปๅž‹ๆ“ไฝœ ======================== + /** + * ่ฎพ็ฝฎไฝๅ›พ็š„ๆŒ‡ๅฎšไฝ + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็”จๆˆท็ญพๅˆฐ็ปŸ่ฎก + * - ๅœจ็บฟ็Šถๆ€็ปŸ่ฎก + * - ๅธƒ้š†่ฟ‡ๆปคๅ™จ + * + * @param key ้”ฎ + * @param offset ๅ็งป้‡๏ผˆไฝไฝ็ฝฎ๏ผ‰ + * @param value ๅ€ผ๏ผˆtrue=1๏ผŒfalse=0๏ผ‰ + * @return ่ฎพ็ฝฎๅ‰็š„ๅ€ผ + */ public Boolean setBit(String key, long offset, boolean value) { return redisTemplate.opsForValue().setBit(key, offset, value); } + /** + * ่Žทๅ–ไฝๅ›พ็š„ๆŒ‡ๅฎšไฝ + * + * @param key ้”ฎ + * @param offset ๅ็งป้‡๏ผˆไฝไฝ็ฝฎ๏ผ‰ + * @return ไฝๅ€ผ๏ผˆtrue=1๏ผŒfalse=0๏ผ‰ + */ public Boolean getBit(String key, long offset) { return redisTemplate.opsForValue().getBit(key, offset); } - // ======================== HyperLogLog ======================== + // ======================== 8. HyperLogLog ็ฑปๅž‹ๆ“ไฝœ ======================== + /** + * ๆทปๅŠ ๅ…ƒ็ด ๅˆฐ HyperLogLog + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ็ปŸ่ฎก็‹ฌ็ซ‹่ฎฟๅฎขๆ•ฐ๏ผˆUV๏ผ‰ + * - ็ปŸ่ฎก็‹ฌ็ซ‹ๅ…ƒ็ด ๆ•ฐ๏ผˆๅŸบๆ•ฐ็ปŸ่ฎก๏ผ‰ + * - ไผ˜็‚น๏ผšๅ†…ๅญ˜ๅ ็”จๅฐ๏ผŒ้€‚ๅˆๅคงๆ•ฐๆฎ้‡ + * + * @param key ้”ฎ + * @param values ๅ€ผๆ•ฐ็ป„ + * @return ๆทปๅŠ ๅŽ็š„ๅŸบๆ•ฐ + */ public Long pfAdd(String key, Object... values) { return redisTemplate.opsForHyperLogLog().add(key, values); } + /** + * ็ปŸ่ฎกๅŸบๆ•ฐ + * + * @param keys ้”ฎๆ•ฐ็ป„๏ผˆๅฏ็ปŸ่ฎกๅคšไธช้”ฎ็š„ๅนถ้›†ๅŸบๆ•ฐ๏ผ‰ + * @return ๅŸบๆ•ฐ + */ public Long pfCount(String... keys) { return redisTemplate.opsForHyperLogLog().size(keys); } + /** + * ๅˆๅนถๅคšไธช HyperLogLog + * + * @param destination ็›ฎๆ ‡้”ฎ + * @param sourceKeys ๆบ้”ฎๆ•ฐ็ป„ + */ public void pfMerge(String destination, String... sourceKeys) { redisTemplate.opsForHyperLogLog().union(destination, sourceKeys); } diff --git a/src/main/java/com/aisi/template/utils/SecurityUtils.java b/src/main/java/com/aisi/template/utils/SecurityUtils.java index af9825e..2f4d42c 100644 --- a/src/main/java/com/aisi/template/utils/SecurityUtils.java +++ b/src/main/java/com/aisi/template/utils/SecurityUtils.java @@ -6,26 +6,56 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +/** + * Security ๅทฅๅ…ท็ฑป + * ๆไพ›่Žทๅ–ๅฝ“ๅ‰็™ปๅฝ•็”จๆˆทไฟกๆฏ็š„ไพฟๆทๆ–นๆณ• + * + * ไธป่ฆๅŠŸ่ƒฝ๏ผš + * 1. ่Žทๅ–ๅฝ“ๅ‰่ฎค่ฏๅฏน่ฑก + * 2. ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทๅ + * 3. ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทID + * + * ไฝฟ็”จๅœบๆ™ฏ๏ผš + * - ๅœจ Service ๅฑ‚่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * - ๅœจๆ‹ฆๆˆชๅ™จไธญ่Žทๅ–ๅฝ“ๅ‰็”จๆˆทไฟกๆฏ + * - ๅœจๆ—ฅๅฟ—ไธญ่ฎฐๅฝ•ๆ“ไฝœ็”จๆˆท + * + * @author Claude + * @since 2024-04-09 + */ @Component public class SecurityUtils { /** - * ่Žทๅ–ๅฝ“ๅ‰ Authentication + * ่Žทๅ–ๅฝ“ๅ‰ Authentication ๅฏน่ฑก + * ่ฏดๆ˜Ž๏ผš + * - Authentication ๅŒ…ๅซ่ฎค่ฏไฟกๆฏๅ’Œ็”จๆˆท่ฏฆๆƒ… + * - ๅฆ‚ๆžœ็”จๆˆทๆœช่ฎค่ฏ๏ผŒ่ฟ”ๅ›ž null + * + * @return ๅฝ“ๅ‰่ฎค่ฏๅฏน่ฑก */ - public static Authentication getAuthentication(){ + public static Authentication getAuthentication() { return SecurityContextHolder.getContext().getAuthentication(); } /** - * ่Žทๅ–็™ปๅฝ•ๅ + * ่Žทๅ–ๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„็”จๆˆทๅ + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅฝ“ๅ‰่ฎค่ฏๅฏน่ฑก + * 2. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅทฒ่ฎค่ฏ + * 3. ไปŽ Principal ไธญๆๅ–็”จๆˆทๅ + * + * @return ็”จๆˆทๅ๏ผŒๆœช็™ปๅฝ•่ฟ”ๅ›ž null */ - public static String getUsername(){ + public static String getUsername() { + // 1. ่Žทๅ–ๅฝ“ๅ‰่ฎค่ฏๅฏน่ฑก Authentication auth = getAuthentication(); if (auth != null && auth.isAuthenticated()) { Object principal = auth.getPrincipal(); + // 2. ๅˆคๆ–ญ Principal ็ฑปๅž‹ๅนถๆๅ–็”จๆˆทๅ if (principal instanceof CustomUserDetails user) { - return user.getUsername(); - }else if (principal instanceof String username) { + return user.getUsername(); + } else if (principal instanceof String username) { return username; } } @@ -33,14 +63,21 @@ public class SecurityUtils { } /** - * ่Žทๅ–็”จๆˆทId + * ่Žทๅ–ๅฝ“ๅ‰็™ปๅฝ•็”จๆˆท็š„็”จๆˆทID + * ๆญฅ้ชค๏ผš + * 1. ่Žทๅ–ๅฝ“ๅ‰่ฎค่ฏๅฏน่ฑก + * 2. ๆฃ€ๆŸฅ็”จๆˆทๆ˜ฏๅฆๅทฒ่ฎค่ฏ + * 3. ไปŽ CustomUserDetails ไธญๆๅ–็”จๆˆทID + * + * @return ็”จๆˆทID๏ผŒๆœช็™ปๅฝ•่ฟ”ๅ›ž null */ public static Long getUserId() { + // 1. ่Žทๅ–ๅฝ“ๅ‰่ฎค่ฏๅฏน่ฑก Authentication auth = getAuthentication(); + // 2. ๆฃ€ๆŸฅๆ˜ฏๅฆๅทฒ่ฎค่ฏไธ” Principal ็ฑปๅž‹ไธบ CustomUserDetails if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof CustomUserDetails user) { return user.getId(); } return null; } - } diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml index 19f5936..980386f 100644 --- a/src/main/resources/application-dev.yaml +++ b/src/main/resources/application-dev.yaml @@ -10,12 +10,17 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: none show-sql: false properties: hibernate: format_sql: true - database-platform: org.hibernate.dialect.MySQLDialect + flyway: + enabled: true + baseline-on-migrate: true + baseline-version: 0 + locations: classpath:db/migration + encoding: UTF-8 data: redis: host: ${REDIS_HOST:localhost} @@ -23,6 +28,8 @@ spring: password: ${REDIS_PASSWORD:} database: ${REDIS_DB:0} timeout: 5000ms + repositories: + enabled: false lettuce: pool: max-active: 8 @@ -44,12 +51,19 @@ spring: jwt: secret: ${JWT_SECRET:templateSecretKeyForJWT2024MustBeLongEnoughForHS512AlgorithmPleaseReplaceInProduction!!} + access-token-expiration: ${JWT_ACCESS_EXPIRATION:3600} # 1 hour in seconds + refresh-token-expiration: ${JWT_REFRESH_EXPIRATION:604800} # 7 days in seconds app: password-reset: code-expire-minutes: ${PASSWORD_RESET_EXPIRE_MINUTES:10} request-cooldown-seconds: ${PASSWORD_RESET_COOLDOWN_SECONDS:60} max-attempts: ${PASSWORD_RESET_MAX_ATTEMPTS:5} + login: + max-attempts: ${LOGIN_MAX_ATTEMPTS:5} + lock-duration-minutes: ${LOGIN_LOCK_DURATION_MINUTES:30} + cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000} logging: level: diff --git a/src/main/resources/application-rocketmq.yaml b/src/main/resources/application-rocketmq.yaml new file mode 100644 index 0000000..7b61a82 --- /dev/null +++ b/src/main/resources/application-rocketmq.yaml @@ -0,0 +1,32 @@ +# RocketMQ ้…็ฝฎ +rocketmq: + # NameServer ๅœฐๅ€๏ผˆๅ•ๆœบ๏ผ‰ + name-server: localhost:9876 + # ็”Ÿไบง่€…้…็ฝฎ + producer: + # ็”Ÿไบง่€…็ป„ๅ + group: user-producer-group + # ๅ‘้€ๆถˆๆฏ่ถ…ๆ—ถๆ—ถ้—ด๏ผˆๆฏซ็ง’๏ผ‰ + send-message-timeout: 3000 + # ๆถˆๆฏๆœ€ๅคงๅคงๅฐ๏ผˆๅญ—่Š‚๏ผ‰ + max-message-size: 4194304 + # ๅคฑ่ดฅ้‡่ฏ•ๆฌกๆ•ฐ + retry-times-when-send-failed: 2 + # ๅผ‚ๆญฅๅ‘้€ๅคฑ่ดฅ้‡่ฏ•ๆฌกๆ•ฐ + retry-times-when-send-async-failed: 2 + # ๅŽ‹็ผฉ้˜ˆๅ€ผ๏ผˆๅญ—่Š‚๏ผ‰ + compress-message-body-threshold: 4096 + # ็”จๆˆทไธป้ข˜ๅ็งฐ + user-topic: user-topic + # ๆถˆ่ดน่€…้…็ฝฎ + consumer: + # ๆถˆ่ดน่€…็ป„ๅ + group: user-consumer-group + # ๆถˆ่ดน็บฟ็จ‹ๆ•ฐ๏ผˆๆœ€ๅฐ๏ผ‰ + consume-thread-min: 5 + # ๆถˆ่ดน็บฟ็จ‹ๆ•ฐ๏ผˆๆœ€ๅคง๏ผ‰ + consume-thread-max: 10 + # ๆถˆๆฏๆœ€ๅคง้‡่ฏ•ๆฌกๆ•ฐ + max-reconsume-times: 3 + # ๆถˆๆฏ่ถ…ๆ—ถๆ—ถ้—ด๏ผˆๅˆ†้’Ÿ๏ผ‰ + consume-timeout: 15 diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a59ea95..90f3a98 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -5,6 +5,11 @@ spring: active: dev config: import: optional:file:.env[.properties] + # ้ป˜่ฎคๆŽ’้™ค RocketMQ ่‡ชๅŠจ้…็ฝฎ๏ผŒๅฏ็”จๆ—ถ้€š่ฟ‡ profile ๆฟ€ๆดป + # ๅฏ็”จๆ–นๅผ๏ผšspring.profiles.active=dev,rocketmq + autoconfigure: + exclude: + - org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration server: port: 8080 diff --git a/src/main/resources/db/migration/V1__baseline_schema.sql b/src/main/resources/db/migration/V1__baseline_schema.sql new file mode 100644 index 0000000..75382cd --- /dev/null +++ b/src/main/resources/db/migration/V1__baseline_schema.sql @@ -0,0 +1,148 @@ +-- V1: Baseline schema for new template projects. +-- Flyway runs this file automatically on the first startup. + +CREATE TABLE IF NOT EXISTS users ( + `id` bigint NOT NULL AUTO_INCREMENT, `email` varchar(255) DEFAULT NULL, `password` varchar(255) NOT NULL, + `username` varchar(50) NOT NULL, `status` tinyint NOT NULL DEFAULT 1 COMMENT '1=enabled 0=disabled', + `failed_login_count` int NOT NULL DEFAULT 0, `locked_until` datetime DEFAULT NULL, + `password_changed_at` datetime DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY uk_users_username (`username`), UNIQUE KEY uk_users_email (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS password_reset_codes ( + `id` bigint NOT NULL AUTO_INCREMENT, `email` varchar(255) NOT NULL, `code_hash` varchar(64) NOT NULL, + `expires_at` datetime NOT NULL, `used` bit(1) NOT NULL DEFAULT b'0', `attempt_count` int NOT NULL DEFAULT 0, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), KEY `idx_password_reset_email` (`email`), KEY `idx_password_reset_expires_at` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS refresh_tokens ( + `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `token_hash` varchar(128) NOT NULL, + `device_info` varchar(255) DEFAULT NULL, `ip_address` varchar(45) DEFAULT NULL, `expires_at` datetime NOT NULL, + `revoked` tinyint(1) NOT NULL DEFAULT b'0', `revoked_at` datetime DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY `uk_refresh_tokens_hash` (`token_hash`), KEY `idx_refresh_tokens_user_id` (`user_id`), + KEY `idx_refresh_tokens_expires_at` (`expires_at`), KEY `idx_refresh_tokens_revoked` (`revoked`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Refresh token storage'; + +CREATE TABLE IF NOT EXISTS sys_role ( + `id` bigint NOT NULL AUTO_INCREMENT, `role_code` varchar(50) NOT NULL, `role_name` varchar(100) NOT NULL, + `description` varchar(500) DEFAULT NULL, `sort_order` int NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 1, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY `uk_sys_role_code` (`role_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='System role table'; + +CREATE TABLE IF NOT EXISTS sys_permission ( + `id` bigint NOT NULL AUTO_INCREMENT, `permission_code` varchar(100) NOT NULL, `permission_name` varchar(100) NOT NULL, + `resource` varchar(50) NOT NULL, `action` varchar(50) NOT NULL, `description` varchar(500) DEFAULT NULL, + `status` tinyint NOT NULL DEFAULT 1, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY `uk_sys_permission_code` (`permission_code`), + KEY `idx_sys_permission_resource` (`resource`), KEY `idx_sys_permission_action` (`action`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='System permission table'; + +CREATE TABLE IF NOT EXISTS sys_menu ( + `id` bigint NOT NULL AUTO_INCREMENT, `parent_id` bigint NOT NULL DEFAULT 0, `menu_name` varchar(50) NOT NULL, + `menu_type` tinyint NOT NULL, `menu_path` varchar(200) DEFAULT NULL, `component` varchar(200) DEFAULT NULL, + `icon` varchar(100) DEFAULT NULL, `sort_order` int NOT NULL DEFAULT 0, `visible` tinyint NOT NULL DEFAULT 1, + `status` tinyint NOT NULL DEFAULT 1, `permission_code` varchar(100) DEFAULT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), KEY `idx_sys_menu_parent_id` (`parent_id`), KEY `idx_sys_menu_type` (`menu_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='System menu table'; + +CREATE TABLE IF NOT EXISTS sys_user_role ( + `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `role_id` bigint NOT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY `uk_sys_user_role` (`user_id`, `role_id`), KEY `idx_sys_user_role_user` (`user_id`), KEY `idx_sys_user_role_role` (`role_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='User-Role mapping table'; + +CREATE TABLE IF NOT EXISTS sys_role_permission ( + `id` bigint NOT NULL AUTO_INCREMENT, `role_id` bigint NOT NULL, `permission_id` bigint NOT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY `uk_sys_role_permission` (`role_id`, `permission_id`), KEY `idx_sys_role_permission_role` (`role_id`), KEY `idx_sys_role_permission_perm` (`permission_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Role-Permission mapping table'; + +CREATE TABLE IF NOT EXISTS sys_role_menu ( + `id` bigint NOT NULL AUTO_INCREMENT, `role_id` bigint NOT NULL, `menu_id` bigint NOT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), UNIQUE KEY `uk_sys_role_menu` (`role_id`, `menu_id`), KEY `idx_sys_role_menu_role` (`role_id`), KEY `idx_sys_role_menu_menu` (`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Role-Menu mapping table'; + +CREATE TABLE IF NOT EXISTS sys_audit_log ( + `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint DEFAULT NULL, `username` varchar(50) DEFAULT NULL, + `action` varchar(50) NOT NULL, `resource` varchar(100) NOT NULL, `resource_id` varchar(50) DEFAULT NULL, + `description` varchar(500) DEFAULT NULL, `request_method` varchar(10) DEFAULT NULL, `request_uri` varchar(500) DEFAULT NULL, + `ip_address` varchar(45) DEFAULT NULL, `user_agent` varchar(500) DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 1, + `error_message` varchar(1000) DEFAULT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), KEY `idx_audit_log_user_id` (`user_id`), KEY `idx_audit_log_action` (`action`), + KEY `idx_audit_log_resource` (`resource`), KEY `idx_audit_log_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='System audit log table'; + +INSERT INTO sys_role (`role_code`, `role_name`, `description`, `sort_order`) VALUES +('ROLE_SUPER_ADMIN', 'Super Administrator', 'Super administrator with all permissions', 1), +('ROLE_ADMIN', 'Administrator', 'Administrator with management permissions', 2), +('ROLE_USER', 'User', 'Regular user with basic permissions', 3); + +INSERT INTO sys_permission (`permission_code`, `permission_name`, `resource`, `action`, `description`) VALUES +('user:create', 'Create User', 'user', 'create', 'Create new user'), +('user:read', 'View User', 'user', 'read', 'View user details'), +('user:update', 'Update User', 'user', 'update', 'Update user information'), +('user:delete', 'Delete User', 'user', 'delete', 'Delete user'), +('user:list', 'List Users', 'user', 'list', 'List all users'), +('user:reset-password', 'Reset Password', 'user', 'reset-password', 'Reset user password'), +('role:create', 'Create Role', 'role', 'create', 'Create new role'), +('role:read', 'View Role', 'role', 'read', 'View role details'), +('role:update', 'Update Role', 'role', 'update', 'Update role information'), +('role:delete', 'Delete Role', 'role', 'delete', 'Delete role'), +('role:list', 'List Roles', 'role', 'list', 'List all roles'), +('role:assign-permission', 'Assign Permissions', 'role', 'assign-permission', 'Assign permissions to role'), +('permission:list', 'List Permissions', 'permission', 'list', 'List all permissions'), +('menu:create', 'Create Menu', 'menu', 'create', 'Create new menu'), +('menu:read', 'View Menu', 'menu', 'read', 'View menu details'), +('menu:update', 'Update Menu', 'menu', 'update', 'Update menu information'), +('menu:delete', 'Delete Menu', 'menu', 'delete', 'Delete menu'), +('menu:list', 'List Menus', 'menu', 'list', 'List all menus'); + +INSERT INTO sys_menu (`parent_id`, `menu_name`, `menu_type`, `menu_path`, `component`, `icon`, `sort_order`, `permission_code`) VALUES +(0, 'System Management', 1, '/system', NULL, 'setting', 1, NULL), +(1, 'User Management', 2, '/system/users', 'system/user/index', 'user', 1, 'user:list'), +(2, 'Add User', 3, NULL, NULL, NULL, 1, 'user:create'), +(2, 'Edit User', 3, NULL, NULL, NULL, 2, 'user:update'), +(2, 'Delete User', 3, NULL, NULL, NULL, 3, 'user:delete'), +(2, 'Reset Password', 3, NULL, NULL, NULL, 4, 'user:reset-password'), +(1, 'Role Management', 2, '/system/roles', 'system/role/index', 'team', 2, 'role:list'), +(7, 'Add Role', 3, NULL, NULL, NULL, 1, 'role:create'), +(7, 'Edit Role', 3, NULL, NULL, NULL, 2, 'role:update'), +(7, 'Delete Role', 3, NULL, NULL, NULL, 3, 'role:delete'), +(7, 'Assign Permissions', 3, NULL, NULL, NULL, 4, 'role:assign-permission'), +(1, 'Menu Management', 2, '/system/menus', 'system/menu/index', 'menu', 3, 'menu:list'), +(12, 'Add Menu', 3, NULL, NULL, NULL, 1, 'menu:create'), +(12, 'Edit Menu', 3, NULL, NULL, NULL, 2, 'menu:update'), +(12, 'Delete Menu', 3, NULL, NULL, NULL, 3, 'menu:delete'); + +INSERT INTO sys_role_permission (`role_id`, `permission_id`) +SELECT r.id, p.id FROM sys_role r JOIN sys_permission p +WHERE r.role_code = 'ROLE_SUPER_ADMIN'; + +INSERT INTO sys_role_permission (`role_id`, `permission_id`) +SELECT r.id, p.id FROM sys_role r JOIN sys_permission p +WHERE r.role_code = 'ROLE_ADMIN' + AND p.permission_code IN ('user:list', 'user:read', 'user:update', 'user:reset-password', 'role:list', 'role:read', 'permission:list'); + +INSERT INTO sys_role_permission (`role_id`, `permission_id`) +SELECT r.id, p.id FROM sys_role r JOIN sys_permission p +WHERE r.role_code = 'ROLE_USER' AND p.permission_code IN ('user:read'); + +INSERT INTO sys_role_menu (`role_id`, `menu_id`) +SELECT r.id, m.id FROM sys_role r JOIN sys_menu m +WHERE r.role_code = 'ROLE_SUPER_ADMIN'; + +INSERT INTO sys_role_menu (`role_id`, `menu_id`) +SELECT r.id, m.id FROM sys_role r JOIN sys_menu m +WHERE r.role_code = 'ROLE_ADMIN' + AND m.menu_name IN ('System Management', 'User Management', 'Role Management', 'Menu Management'); + +INSERT INTO sys_role_menu (`role_id`, `menu_id`) +SELECT r.id, m.id FROM sys_role r JOIN sys_menu m +WHERE r.role_code = 'ROLE_USER' AND m.menu_name = 'User Management'; diff --git a/src/main/resources/db/migration/V2__example_auto_migration.sql b/src/main/resources/db/migration/V2__example_auto_migration.sql new file mode 100644 index 0000000..b789da3 --- /dev/null +++ b/src/main/resources/db/migration/V2__example_auto_migration.sql @@ -0,0 +1,15 @@ +-- V2: Example Flyway migration. +-- Keep this small file to show template users how automatic migrations work. +-- Add future schema changes as V3__xxx.sql, V4__xxx.sql, ... + +CREATE TABLE IF NOT EXISTS app_schema_version_note ( + `id` bigint NOT NULL AUTO_INCREMENT, + `version` varchar(50) NOT NULL, + `description` varchar(255) NOT NULL, + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_app_schema_version_note_version` (`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Flyway migration example note'; + +INSERT INTO app_schema_version_note (`version`, `description`) +VALUES ('V2', 'Example migration applied by Flyway automatically'); diff --git a/src/main/resources/sql/init.sql b/src/main/resources/sql/init.sql deleted file mode 100644 index c97ca8c..0000000 --- a/src/main/resources/sql/init.sql +++ /dev/null @@ -1,35 +0,0 @@ --- ============================================ --- Template database initialization script --- ============================================ - -CREATE TABLE IF NOT EXISTS users ( - `id` bigint NOT NULL AUTO_INCREMENT, - `email` varchar(255) DEFAULT NULL, - `password` varchar(255) NOT NULL, - `username` varchar(50) NOT NULL, - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1=enabled 0=disabled', - `role` VARCHAR(20) NOT NULL DEFAULT 'USER' COMMENT 'Role: USER or ADMIN', - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY uk_users_username (`username`), - UNIQUE KEY uk_users_email (`email`), - KEY idx_users_role (`role`) -) ENGINE=InnoDB - DEFAULT CHARSET=utf8mb4 - COLLATE=utf8mb4_0900_ai_ci; - -CREATE TABLE IF NOT EXISTS password_reset_codes ( - `id` bigint NOT NULL AUTO_INCREMENT, - `email` varchar(255) NOT NULL, - `code_hash` varchar(64) NOT NULL, - `expires_at` datetime NOT NULL, - `used` bit(1) NOT NULL DEFAULT b'0', - `attempt_count` int NOT NULL DEFAULT 0, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - KEY `idx_password_reset_email` (`email`), - KEY `idx_password_reset_expires_at` (`expires_at`) -) ENGINE=InnoDB - DEFAULT CHARSET=utf8mb4 - COLLATE=utf8mb4_0900_ai_ci;