├── .gitignore ├── README.md ├── imgs ├── 1653056228879.png ├── 1653057830540.png ├── 1653057872536.png ├── 1653059409865.png ├── 1653060176932.png ├── 1653060204933.png ├── 1653060237073.png ├── 1653060337562.png ├── 1653060497599.png ├── 1653060588190.png ├── 1653066005825.png ├── 1653066208144.png ├── 1653067054461.png ├── 1653067706666.png ├── 1653068196656.png ├── 1653068821351.png ├── 1653068874258.png ├── 1653069893050.png ├── 1653319261433.png ├── 1653319410188.png ├── 1653319432723.png ├── 1653319474181.png ├── 1653320764547.png ├── 1653320822964.png ├── 1653322097736.png ├── 1653322190155.png ├── 1653322491532.png ├── 1653322506393.png ├── 1653322857620.png ├── 1653323595206.png ├── 1653325798637.png ├── 1653325871232.png ├── 1653325929549.png ├── 1653326156516.png ├── 1653327124561.png ├── 1653327884526.png ├── 1653328022622.png ├── 1653328288627.png ├── 1653328663897.png ├── 1653357522914.png ├── 1653357860001.png ├── 1653360138640.png ├── 1653360308731.png ├── 1653360675507.png ├── 1653360807133.png ├── 1653360864839.png ├── 1653362612286.png ├── 1653363100502.png ├── 1653363172079.png ├── 1653365145124.png ├── 1653365839526.png ├── 1653366238564.png ├── 1653368335155.png ├── 1653368562591.png ├── 1653369268550.png ├── 1653370271627.png ├── 1653371854389.png ├── 1653373434815.png ├── 1653373887844.png ├── 1653373908620.png ├── 1653374044740.png ├── 1653374296906.png ├── 1653381972377.png ├── 1653381992018.png ├── 1653382219377.png ├── 1653382304000.png ├── 1653382669900.png ├── 1653382694491.png ├── 1653382793857.png ├── 1653382830810.png ├── 1653383810643.png ├── 1653385920025.png ├── 1653387398820.png ├── 1653387764938.png ├── 1653392181413.png ├── 1653392211969.png ├── 1653392218531.png ├── 1653392247274.png ├── 1653392438917.png ├── 1653392583285.png ├── 1653393304844.png ├── 1653546070602.png ├── 1653546736063.png ├── 1653548087334.png ├── 1653553093967.png ├── 1653553277681.png ├── 1653553998403.png ├── 1653554055048.png ├── 1653560899680.png ├── 1653560952106.png ├── 1653560975828.png ├── 1653560986599.png ├── 1653561657295.png ├── 1653562234886.png ├── 1653574526668.png ├── 1653574787557.png ├── 1653574849336.png ├── 1653575176451.png ├── 1653575506373.png ├── 1653577108429.png ├── 1653577280060.png ├── 1653577301737.png ├── 1653577349691.png ├── 1653577445413.png ├── 1653577643629.png ├── 1653577659166.png ├── 1653577689129.png ├── 1653577801668.png ├── 1653577984924.png ├── 1653578211854.png ├── 1653578560691.png ├── 1653578992639.png ├── 1653579931626.png ├── 1653581590453.png ├── 1653805077118.png ├── 1653805203758.png ├── 1653806140822.png ├── 1653806253817.png ├── 1653806706296.png ├── 1653806949217.png ├── 1653806973212.png ├── 1653808641260.png ├── 1653808993693.png ├── 1653809450816.png ├── 1653809875208.png ├── 1653812346852.png ├── 1653813047671.png ├── 1653813422676.png ├── 1653813462834.png ├── 1653819799739.png ├── 1653819821591.png ├── 1653821271347.png ├── 1653822021827.png ├── 1653822036941.png ├── 1653823145495.png ├── 1653824105810.png ├── 1653824498278.png ├── 1653824543977.png ├── 1653833954301.png ├── 1653833970361.png ├── 1653834439027.png ├── 1653834455899.png ├── 1653835784444.png ├── 1653836416586.png ├── 1653836578970.png ├── 1653837988985.png ├── 1653838053608.png ├── 1656079017728.png ├── 1656080546603.png ├── 1656082824939.png ├── image-20220523212915666.png ├── image-20220523214414123.png ├── image-20220523220950421.png ├── image-20220523221428827.png └── 避震器.gif ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── xydp │ │ ├── HmDianPingApplication.java │ │ ├── config │ │ ├── MQConfig.java │ │ ├── MvcConfig.java │ │ ├── MybatisConfig.java │ │ ├── RedisConfig.java │ │ └── WebExceptionAdvice.java │ │ ├── controller │ │ ├── BlogCommentsController.java │ │ ├── BlogController.java │ │ ├── FollowController.java │ │ ├── ShopController.java │ │ ├── ShopTypeController.java │ │ ├── TokenController.java │ │ ├── UploadController.java │ │ ├── UserController.java │ │ ├── VoucherController.java │ │ └── VoucherOrderController.java │ │ ├── dto │ │ ├── LoginFormDTO.java │ │ ├── Result.java │ │ ├── ScrollResult.java │ │ ├── SeckillOrderDTO.java │ │ └── UserDTO.java │ │ ├── entity │ │ ├── Blog.java │ │ ├── BlogComments.java │ │ ├── Follow.java │ │ ├── SeckillVoucher.java │ │ ├── Shop.java │ │ ├── ShopType.java │ │ ├── User.java │ │ ├── UserInfo.java │ │ ├── Voucher.java │ │ └── VoucherOrder.java │ │ ├── exception │ │ └── BusinessException.java │ │ ├── interceptor │ │ ├── LoginInterceptor.java │ │ └── RefreshTokenInterceptor.java │ │ ├── mapper │ │ ├── BlogCommentsMapper.java │ │ ├── BlogMapper.java │ │ ├── FollowMapper.java │ │ ├── SeckillVoucherMapper.java │ │ ├── ShopMapper.java │ │ ├── ShopTypeMapper.java │ │ ├── UserInfoMapper.java │ │ ├── UserMapper.java │ │ ├── VoucherMapper.java │ │ └── VoucherOrderMapper.java │ │ ├── mq │ │ ├── MqReceiver.java │ │ └── MqSender.java │ │ ├── prevent │ │ ├── Prevent.java │ │ ├── PreventAop.java │ │ └── RepeatSubmit.java │ │ ├── service │ │ ├── IBlogCommentsService.java │ │ ├── IBlogService.java │ │ ├── IFollowService.java │ │ ├── ISeckillVoucherService.java │ │ ├── IShopService.java │ │ ├── IShopTypeService.java │ │ ├── ITokenService.java │ │ ├── IUserInfoService.java │ │ ├── IUserService.java │ │ ├── IVoucherOrderService.java │ │ ├── IVoucherService.java │ │ └── impl │ │ │ ├── BlogCommentsServiceImpl.java │ │ │ ├── BlogServiceImpl.java │ │ │ ├── FollowServiceImpl.java │ │ │ ├── ITokenServiceImpl.java │ │ │ ├── SeckillVoucherServiceImpl.java │ │ │ ├── ShopServiceImpl.java │ │ │ ├── ShopTypeServiceImpl.java │ │ │ ├── UserInfoServiceImpl.java │ │ │ ├── UserServiceImpl.java │ │ │ ├── VoucherOrderServiceImpl.java │ │ │ └── VoucherServiceImpl.java │ │ └── utils │ │ ├── CacheClient.java │ │ ├── CommonUtils.java │ │ ├── ILock.java │ │ ├── IPUtils.java │ │ ├── MQConstants.java │ │ ├── PasswordEncoder.java │ │ ├── RedisConstants.java │ │ ├── RedisData.java │ │ ├── RedisIdWorker.java │ │ ├── RegexPatterns.java │ │ ├── RegexUtils.java │ │ ├── SimpleRedisLock.java │ │ ├── SystemConstants.java │ │ └── UserHolder.java └── resources │ ├── application.yaml │ ├── db │ └── hmdp.sql │ ├── mapper │ └── VoucherMapper.xml │ ├── reentrantTryLock.lua │ ├── reentrantUnlock.lua │ ├── seckill.lua │ ├── seckillBackup.lua │ ├── tokens.txt │ └── unlock.lua └── test └── java └── com └── xydp ├── HmDianPingApplicationTests.java ├── RedissonTest.java └── VoucherOrderControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD 2 | ### Java template 3 | # Compiled class file 4 | *.class 5 | 6 | # Log file 7 | *.log 8 | 9 | # BlueJ files 10 | *.ctxt 11 | 12 | # Mobile Tools for Java (J2ME) 13 | .mtj.tmp/ 14 | 15 | # Package Files # 16 | *.jar 17 | *.war 18 | *.nar 19 | *.ear 20 | *.zip 21 | *.tar.gz 22 | *.rar 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | replay_pid* 27 | 28 | ======= 29 | HELP.md 30 | target/ 31 | !.mvn/wrapper/maven-wrapper.jar 32 | !**/src/main/** 33 | !**/src/test/** 34 | 35 | ### STS ### 36 | .apt_generated 37 | .classpath 38 | .factorypath 39 | .project 40 | .settings 41 | .springBeans 42 | .sts4-cache 43 | 44 | ### IntelliJ IDEA ### 45 | .idea 46 | *.iws 47 | *.iml 48 | *.ipr 49 | 50 | ### NetBeans ### 51 | /nbproject/private/ 52 | /nbbuild/ 53 | /dist/ 54 | /nbdist/ 55 | /.nb-gradle/ 56 | build/ 57 | logs/ 58 | 59 | ### VS Code ### 60 | .vscode/ 61 | >>>>>>> cd93a7acbb1b85a5ac2bbeb09bac07112589a4e3 62 | -------------------------------------------------------------------------------- /imgs/1653056228879.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653056228879.png -------------------------------------------------------------------------------- /imgs/1653057830540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653057830540.png -------------------------------------------------------------------------------- /imgs/1653057872536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653057872536.png -------------------------------------------------------------------------------- /imgs/1653059409865.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653059409865.png -------------------------------------------------------------------------------- /imgs/1653060176932.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653060176932.png -------------------------------------------------------------------------------- /imgs/1653060204933.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653060204933.png -------------------------------------------------------------------------------- /imgs/1653060237073.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653060237073.png -------------------------------------------------------------------------------- /imgs/1653060337562.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653060337562.png -------------------------------------------------------------------------------- /imgs/1653060497599.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653060497599.png -------------------------------------------------------------------------------- /imgs/1653060588190.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653060588190.png -------------------------------------------------------------------------------- /imgs/1653066005825.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653066005825.png -------------------------------------------------------------------------------- /imgs/1653066208144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653066208144.png -------------------------------------------------------------------------------- /imgs/1653067054461.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653067054461.png -------------------------------------------------------------------------------- /imgs/1653067706666.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653067706666.png -------------------------------------------------------------------------------- /imgs/1653068196656.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653068196656.png -------------------------------------------------------------------------------- /imgs/1653068821351.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653068821351.png -------------------------------------------------------------------------------- /imgs/1653068874258.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653068874258.png -------------------------------------------------------------------------------- /imgs/1653069893050.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653069893050.png -------------------------------------------------------------------------------- /imgs/1653319261433.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653319261433.png -------------------------------------------------------------------------------- /imgs/1653319410188.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653319410188.png -------------------------------------------------------------------------------- /imgs/1653319432723.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653319432723.png -------------------------------------------------------------------------------- /imgs/1653319474181.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653319474181.png -------------------------------------------------------------------------------- /imgs/1653320764547.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653320764547.png -------------------------------------------------------------------------------- /imgs/1653320822964.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653320822964.png -------------------------------------------------------------------------------- /imgs/1653322097736.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653322097736.png -------------------------------------------------------------------------------- /imgs/1653322190155.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653322190155.png -------------------------------------------------------------------------------- /imgs/1653322491532.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653322491532.png -------------------------------------------------------------------------------- /imgs/1653322506393.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653322506393.png -------------------------------------------------------------------------------- /imgs/1653322857620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653322857620.png -------------------------------------------------------------------------------- /imgs/1653323595206.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653323595206.png -------------------------------------------------------------------------------- /imgs/1653325798637.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653325798637.png -------------------------------------------------------------------------------- /imgs/1653325871232.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653325871232.png -------------------------------------------------------------------------------- /imgs/1653325929549.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653325929549.png -------------------------------------------------------------------------------- /imgs/1653326156516.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653326156516.png -------------------------------------------------------------------------------- /imgs/1653327124561.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653327124561.png -------------------------------------------------------------------------------- /imgs/1653327884526.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653327884526.png -------------------------------------------------------------------------------- /imgs/1653328022622.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653328022622.png -------------------------------------------------------------------------------- /imgs/1653328288627.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653328288627.png -------------------------------------------------------------------------------- /imgs/1653328663897.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653328663897.png -------------------------------------------------------------------------------- /imgs/1653357522914.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653357522914.png -------------------------------------------------------------------------------- /imgs/1653357860001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653357860001.png -------------------------------------------------------------------------------- /imgs/1653360138640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653360138640.png -------------------------------------------------------------------------------- /imgs/1653360308731.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653360308731.png -------------------------------------------------------------------------------- /imgs/1653360675507.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653360675507.png -------------------------------------------------------------------------------- /imgs/1653360807133.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653360807133.png -------------------------------------------------------------------------------- /imgs/1653360864839.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653360864839.png -------------------------------------------------------------------------------- /imgs/1653362612286.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653362612286.png -------------------------------------------------------------------------------- /imgs/1653363100502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653363100502.png -------------------------------------------------------------------------------- /imgs/1653363172079.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653363172079.png -------------------------------------------------------------------------------- /imgs/1653365145124.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653365145124.png -------------------------------------------------------------------------------- /imgs/1653365839526.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653365839526.png -------------------------------------------------------------------------------- /imgs/1653366238564.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653366238564.png -------------------------------------------------------------------------------- /imgs/1653368335155.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653368335155.png -------------------------------------------------------------------------------- /imgs/1653368562591.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653368562591.png -------------------------------------------------------------------------------- /imgs/1653369268550.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653369268550.png -------------------------------------------------------------------------------- /imgs/1653370271627.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653370271627.png -------------------------------------------------------------------------------- /imgs/1653371854389.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653371854389.png -------------------------------------------------------------------------------- /imgs/1653373434815.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653373434815.png -------------------------------------------------------------------------------- /imgs/1653373887844.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653373887844.png -------------------------------------------------------------------------------- /imgs/1653373908620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653373908620.png -------------------------------------------------------------------------------- /imgs/1653374044740.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653374044740.png -------------------------------------------------------------------------------- /imgs/1653374296906.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653374296906.png -------------------------------------------------------------------------------- /imgs/1653381972377.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653381972377.png -------------------------------------------------------------------------------- /imgs/1653381992018.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653381992018.png -------------------------------------------------------------------------------- /imgs/1653382219377.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653382219377.png -------------------------------------------------------------------------------- /imgs/1653382304000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653382304000.png -------------------------------------------------------------------------------- /imgs/1653382669900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653382669900.png -------------------------------------------------------------------------------- /imgs/1653382694491.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653382694491.png -------------------------------------------------------------------------------- /imgs/1653382793857.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653382793857.png -------------------------------------------------------------------------------- /imgs/1653382830810.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653382830810.png -------------------------------------------------------------------------------- /imgs/1653383810643.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653383810643.png -------------------------------------------------------------------------------- /imgs/1653385920025.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653385920025.png -------------------------------------------------------------------------------- /imgs/1653387398820.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653387398820.png -------------------------------------------------------------------------------- /imgs/1653387764938.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653387764938.png -------------------------------------------------------------------------------- /imgs/1653392181413.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653392181413.png -------------------------------------------------------------------------------- /imgs/1653392211969.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653392211969.png -------------------------------------------------------------------------------- /imgs/1653392218531.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653392218531.png -------------------------------------------------------------------------------- /imgs/1653392247274.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653392247274.png -------------------------------------------------------------------------------- /imgs/1653392438917.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653392438917.png -------------------------------------------------------------------------------- /imgs/1653392583285.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653392583285.png -------------------------------------------------------------------------------- /imgs/1653393304844.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653393304844.png -------------------------------------------------------------------------------- /imgs/1653546070602.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653546070602.png -------------------------------------------------------------------------------- /imgs/1653546736063.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653546736063.png -------------------------------------------------------------------------------- /imgs/1653548087334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653548087334.png -------------------------------------------------------------------------------- /imgs/1653553093967.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653553093967.png -------------------------------------------------------------------------------- /imgs/1653553277681.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653553277681.png -------------------------------------------------------------------------------- /imgs/1653553998403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653553998403.png -------------------------------------------------------------------------------- /imgs/1653554055048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653554055048.png -------------------------------------------------------------------------------- /imgs/1653560899680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653560899680.png -------------------------------------------------------------------------------- /imgs/1653560952106.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653560952106.png -------------------------------------------------------------------------------- /imgs/1653560975828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653560975828.png -------------------------------------------------------------------------------- /imgs/1653560986599.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653560986599.png -------------------------------------------------------------------------------- /imgs/1653561657295.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653561657295.png -------------------------------------------------------------------------------- /imgs/1653562234886.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653562234886.png -------------------------------------------------------------------------------- /imgs/1653574526668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653574526668.png -------------------------------------------------------------------------------- /imgs/1653574787557.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653574787557.png -------------------------------------------------------------------------------- /imgs/1653574849336.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653574849336.png -------------------------------------------------------------------------------- /imgs/1653575176451.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653575176451.png -------------------------------------------------------------------------------- /imgs/1653575506373.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653575506373.png -------------------------------------------------------------------------------- /imgs/1653577108429.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577108429.png -------------------------------------------------------------------------------- /imgs/1653577280060.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577280060.png -------------------------------------------------------------------------------- /imgs/1653577301737.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577301737.png -------------------------------------------------------------------------------- /imgs/1653577349691.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577349691.png -------------------------------------------------------------------------------- /imgs/1653577445413.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577445413.png -------------------------------------------------------------------------------- /imgs/1653577643629.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577643629.png -------------------------------------------------------------------------------- /imgs/1653577659166.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577659166.png -------------------------------------------------------------------------------- /imgs/1653577689129.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577689129.png -------------------------------------------------------------------------------- /imgs/1653577801668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577801668.png -------------------------------------------------------------------------------- /imgs/1653577984924.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653577984924.png -------------------------------------------------------------------------------- /imgs/1653578211854.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653578211854.png -------------------------------------------------------------------------------- /imgs/1653578560691.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653578560691.png -------------------------------------------------------------------------------- /imgs/1653578992639.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653578992639.png -------------------------------------------------------------------------------- /imgs/1653579931626.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653579931626.png -------------------------------------------------------------------------------- /imgs/1653581590453.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653581590453.png -------------------------------------------------------------------------------- /imgs/1653805077118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653805077118.png -------------------------------------------------------------------------------- /imgs/1653805203758.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653805203758.png -------------------------------------------------------------------------------- /imgs/1653806140822.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653806140822.png -------------------------------------------------------------------------------- /imgs/1653806253817.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653806253817.png -------------------------------------------------------------------------------- /imgs/1653806706296.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653806706296.png -------------------------------------------------------------------------------- /imgs/1653806949217.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653806949217.png -------------------------------------------------------------------------------- /imgs/1653806973212.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653806973212.png -------------------------------------------------------------------------------- /imgs/1653808641260.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653808641260.png -------------------------------------------------------------------------------- /imgs/1653808993693.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653808993693.png -------------------------------------------------------------------------------- /imgs/1653809450816.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653809450816.png -------------------------------------------------------------------------------- /imgs/1653809875208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653809875208.png -------------------------------------------------------------------------------- /imgs/1653812346852.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653812346852.png -------------------------------------------------------------------------------- /imgs/1653813047671.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653813047671.png -------------------------------------------------------------------------------- /imgs/1653813422676.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653813422676.png -------------------------------------------------------------------------------- /imgs/1653813462834.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653813462834.png -------------------------------------------------------------------------------- /imgs/1653819799739.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653819799739.png -------------------------------------------------------------------------------- /imgs/1653819821591.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653819821591.png -------------------------------------------------------------------------------- /imgs/1653821271347.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653821271347.png -------------------------------------------------------------------------------- /imgs/1653822021827.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653822021827.png -------------------------------------------------------------------------------- /imgs/1653822036941.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653822036941.png -------------------------------------------------------------------------------- /imgs/1653823145495.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653823145495.png -------------------------------------------------------------------------------- /imgs/1653824105810.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653824105810.png -------------------------------------------------------------------------------- /imgs/1653824498278.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653824498278.png -------------------------------------------------------------------------------- /imgs/1653824543977.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653824543977.png -------------------------------------------------------------------------------- /imgs/1653833954301.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653833954301.png -------------------------------------------------------------------------------- /imgs/1653833970361.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653833970361.png -------------------------------------------------------------------------------- /imgs/1653834439027.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653834439027.png -------------------------------------------------------------------------------- /imgs/1653834455899.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653834455899.png -------------------------------------------------------------------------------- /imgs/1653835784444.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653835784444.png -------------------------------------------------------------------------------- /imgs/1653836416586.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653836416586.png -------------------------------------------------------------------------------- /imgs/1653836578970.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653836578970.png -------------------------------------------------------------------------------- /imgs/1653837988985.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653837988985.png -------------------------------------------------------------------------------- /imgs/1653838053608.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1653838053608.png -------------------------------------------------------------------------------- /imgs/1656079017728.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1656079017728.png -------------------------------------------------------------------------------- /imgs/1656080546603.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1656080546603.png -------------------------------------------------------------------------------- /imgs/1656082824939.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/1656082824939.png -------------------------------------------------------------------------------- /imgs/image-20220523212915666.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/image-20220523212915666.png -------------------------------------------------------------------------------- /imgs/image-20220523214414123.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/image-20220523214414123.png -------------------------------------------------------------------------------- /imgs/image-20220523220950421.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/image-20220523220950421.png -------------------------------------------------------------------------------- /imgs/image-20220523221428827.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/image-20220523221428827.png -------------------------------------------------------------------------------- /imgs/避震器.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viego1999/xy-dianping/f6fc3d03026939593f7f15d172a595449f5c8e78/imgs/避震器.gif -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.3.12.RELEASE 9 | 10 | 11 | com.xydp 12 | xy-dianping 13 | 0.0.1-SNAPSHOT 14 | xy-dianping 15 | Demo project for Spring Boot 16 | 17 | 1.8 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-data-redis 23 | 24 | 25 | org.springframework.data 26 | spring-data-redis 27 | 28 | 29 | lettuce-core 30 | io.lettuce 31 | 32 | 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-amqp 37 | 38 | 39 | org.springframework.data 40 | spring-data-redis 41 | 2.7.0 42 | 43 | 44 | io.lettuce 45 | lettuce-core 46 | 6.1.8.RELEASE 47 | 48 | 49 | org.apache.commons 50 | commons-pool2 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-web 55 | 56 | 57 | 58 | mysql 59 | mysql-connector-java 60 | runtime 61 | 5.1.47 62 | 63 | 64 | org.projectlombok 65 | lombok 66 | true 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-starter-test 71 | test 72 | 73 | 74 | com.baomidou 75 | mybatis-plus-boot-starter 76 | 3.4.3 77 | 78 | 79 | 80 | cn.hutool 81 | hutool-all 82 | 5.7.17 83 | 84 | 85 | 86 | org.redisson 87 | redisson 88 | 3.13.6 89 | 90 | 91 | 92 | org.aspectj 93 | aspectjweaver 94 | 95 | 96 | org.junit.jupiter 97 | junit-jupiter-api 98 | 5.7.2 99 | test 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.springframework.boot 107 | spring-boot-maven-plugin 108 | 109 | 110 | 111 | org.projectlombok 112 | lombok 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/HmDianPingApplication.java: -------------------------------------------------------------------------------- 1 | package com.xydp; 2 | 3 | import org.mybatis.spring.annotation.MapperScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 7 | 8 | @EnableAspectJAutoProxy(exposeProxy = true) 9 | @MapperScan("com.xydp.mapper") 10 | @SpringBootApplication 11 | public class HmDianPingApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(HmDianPingApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/config/MQConfig.java: -------------------------------------------------------------------------------- 1 | package com.xydp.config; 2 | 3 | import com.xydp.utils.MQConstants; 4 | import org.springframework.amqp.core.*; 5 | import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; 6 | import org.springframework.amqp.support.converter.MessageConverter; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | /** 11 | * 消息队列配置类: 12 | *

13 | * 实际上,RabbitMQ 支持两种模式: 14 | *

    15 | *
  1. 工作队列模式:即只有 Provider、Consumer 和 Queue,此时绑定的是默认的 Exchange
  2. 16 | *
  3. 17 | * Pub/Sub模式:即有 Provider、Consumer、Queue 和 Exchange,其中又分为: 18 | * 23 | *
  4. 24 | *
25 | * 本质上都一样,主要角色都有 Provider、Consumer、Queue、Exchange、RoutingKey。 26 | *

27 | * 28 | * @author Wuxy 29 | * @version 1.0 30 | * @ClassName MQConfig 31 | * @since 2023/1/13 23:42 32 | */ 33 | @Configuration 34 | public class MQConfig { 35 | 36 | @Bean 37 | public MessageConverter messageConverter() { 38 | return new Jackson2JsonMessageConverter(); 39 | } 40 | 41 | @Bean 42 | public Queue seckillQueue() { 43 | return QueueBuilder.durable(MQConstants.SECKILL_QUEUE).build(); 44 | } 45 | 46 | @Bean 47 | public Exchange seckillExchange() { 48 | return ExchangeBuilder.directExchange(MQConstants.SECKILL_EXCHANGE).durable(true).build(); 49 | } 50 | 51 | @Bean 52 | public Binding bindingSeckill(Queue seckillQueue, Exchange seckillExchange) { 53 | return BindingBuilder.bind(seckillQueue).to(seckillExchange).with(MQConstants.SECKILL_ROUTING_KEY).noargs(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/config/MvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.xydp.config; 2 | 3 | import com.xydp.interceptor.LoginInterceptor; 4 | import com.xydp.interceptor.RefreshTokenInterceptor; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.redis.core.StringRedisTemplate; 7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | 10 | import javax.annotation.Resource; 11 | 12 | @Configuration 13 | public class MvcConfig implements WebMvcConfigurer { 14 | 15 | @Resource 16 | private StringRedisTemplate stringRedisTemplate; 17 | 18 | @Override 19 | public void addInterceptors(InterceptorRegistry registry) { 20 | // 登录拦截器 21 | registry.addInterceptor(new LoginInterceptor()) 22 | .excludePathPatterns( 23 | "/shop/**", 24 | "/voucher/**", 25 | "/shop-type/**", 26 | "/upload/**", 27 | "/blog/hot", 28 | "/user/code", 29 | "/user/login" 30 | ).order(1); 31 | // token 刷新拦截器 32 | registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)) 33 | .addPathPatterns("/**") // 拦截所有请求,默认为拦截所有请求 34 | .order(0); // 值越小,越先执行 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/config/MybatisConfig.java: -------------------------------------------------------------------------------- 1 | package com.xydp.config; 2 | 3 | import com.baomidou.mybatisplus.annotation.DbType; 4 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; 5 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class MybatisConfig { 11 | @Bean 12 | public MybatisPlusInterceptor mybatisPlusInterceptor() { 13 | MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); 14 | interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); 15 | return interceptor; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.xydp.config; 2 | 3 | import org.redisson.Redisson; 4 | import org.redisson.api.RedissonClient; 5 | import org.redisson.config.Config; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | /** 10 | * Redisson 客户端类 11 | */ 12 | @Configuration 13 | public class RedisConfig { 14 | @Bean 15 | public RedissonClient redissonClient() { 16 | // 配置类 17 | Config config = new Config(); 18 | // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 19 | // config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456"); 20 | config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // Redis没有密码时不需要设置password参数 21 | // 创建客户端 22 | return Redisson.create(config); 23 | } 24 | 25 | @Bean 26 | public RedissonClient redissonClient2() { 27 | // 配置类 28 | Config config = new Config(); 29 | // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 30 | config.useSingleServer().setAddress("redis://127.0.0.1:6379"); 31 | // 创建客户端 32 | return Redisson.create(config); 33 | } 34 | 35 | @Bean 36 | public RedissonClient redissonClient3() { 37 | // 配置类 38 | Config config = new Config(); 39 | // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 40 | config.useSingleServer().setAddress("redis://127.0.0.1:6379"); 41 | // 创建客户端 42 | return Redisson.create(config); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/config/WebExceptionAdvice.java: -------------------------------------------------------------------------------- 1 | package com.xydp.config; 2 | 3 | import com.xydp.dto.Result; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.web.bind.annotation.ExceptionHandler; 6 | import org.springframework.web.bind.annotation.RestControllerAdvice; 7 | 8 | @Slf4j 9 | @RestControllerAdvice 10 | public class WebExceptionAdvice { 11 | 12 | /** 13 | * 全局 Web 运行时异常统一处理 14 | * 15 | * @param e 异常 16 | * @return 错误信息 17 | */ 18 | @ExceptionHandler(RuntimeException.class) 19 | public Result handleRuntimeException(RuntimeException e) { 20 | log.error(e.toString(), e); 21 | return Result.fail("服务器异常:" + e.getMessage()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/BlogCommentsController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | /** 8 | * 博客评论管理 9 | * 10 | * @author 虎哥 11 | * @since 2021-12-22 12 | */ 13 | @RestController 14 | @RequestMapping("/blog-comments") 15 | public class BlogCommentsController { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/BlogController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 5 | import com.xydp.dto.Result; 6 | import com.xydp.dto.UserDTO; 7 | import com.xydp.entity.Blog; 8 | import com.xydp.service.IBlogService; 9 | import com.xydp.utils.SystemConstants; 10 | import com.xydp.utils.UserHolder; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import javax.annotation.Resource; 14 | import java.util.List; 15 | 16 | /** 17 | * 博客管理 18 | * 19 | * @author 虎哥 20 | * @since 2021-12-22 21 | */ 22 | @RestController 23 | @RequestMapping("/blog") 24 | public class BlogController { 25 | 26 | @Resource 27 | private IBlogService blogService; 28 | 29 | @PostMapping 30 | public Result saveBlog(@RequestBody Blog blog) { 31 | 32 | return blogService.saveBlog(blog); 33 | } 34 | 35 | @PutMapping("/like/{id}") 36 | public Result likeBlog(@PathVariable("id") Long id) { 37 | // 修改点赞数量 update tb_blog set liked = liked + 1 where id = ?; 38 | /*blogService.update() 39 | .setSql("liked = liked + 1").eq("id", id).update(); 40 | return Result.ok();*/ 41 | return blogService.likeBlog(id); 42 | } 43 | 44 | @GetMapping("/of/me") 45 | public Result queryMyBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) { 46 | // 获取登录用户 47 | UserDTO user = UserHolder.getUser(); 48 | // 根据用户查询 49 | Page page = blogService.query() 50 | .eq("user_id", user.getId()).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); 51 | // 获取当前页数据 52 | List records = page.getRecords(); 53 | return Result.ok(records); 54 | } 55 | 56 | @GetMapping("/hot") 57 | public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) { 58 | 59 | return blogService.queryHotBlog(current); 60 | } 61 | 62 | @GetMapping("/{id}") 63 | public Result queryBlogById(@PathVariable("id") Long id) { 64 | 65 | return blogService.queryBlogById(id); 66 | } 67 | 68 | /** 69 | * 查询点赞时间顺序前五的用户 70 | * 71 | * @param id 日志id 72 | * @return Result 73 | */ 74 | @GetMapping("/likes/{id}") 75 | public Result queryBlogLikes(@PathVariable("id") Long id) { 76 | 77 | return blogService.queryBlogLikes(id); 78 | } 79 | 80 | @GetMapping("/of/user") 81 | public Result queryBlogByUserId( 82 | @RequestParam(value = "current", defaultValue = "1") Integer current, 83 | @RequestParam(value = "id") Long id) { 84 | // 根据用户查询 85 | Page page = blogService.query() 86 | .eq("user_id", id) 87 | .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); 88 | // 获取当前页面数据 89 | List records = page.getRecords(); 90 | return Result.ok(records); 91 | } 92 | 93 | @GetMapping("/of/follow") 94 | public Result queryBlogOfFollow( 95 | @RequestParam("lastId") Long max, 96 | @RequestParam(value = "offset", defaultValue = "0") Integer offset) { 97 | return blogService.queryBlogOfFollow(max, offset); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/FollowController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import com.xydp.dto.Result; 5 | import com.xydp.service.IFollowService; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.annotation.Resource; 9 | 10 | /** 11 | * 关注管理 12 | * 13 | * @author 虎哥 14 | * @since 2021-12-22 15 | */ 16 | @RestController 17 | @RequestMapping("/follow") 18 | public class FollowController { 19 | 20 | @Resource 21 | private IFollowService followService; 22 | 23 | /** 24 | * 关注和取关 25 | */ 26 | @PutMapping("/{id}/{isFollow}") 27 | public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) { 28 | 29 | return followService.follow(followUserId, isFollow); 30 | } 31 | 32 | /** 33 | * 判断是否关注 34 | */ 35 | @GetMapping("/or/not/{id}") 36 | public Result isFollow(@PathVariable("id") Long followUserId) { 37 | 38 | return followService.isFollow(followUserId); 39 | } 40 | 41 | /** 42 | * 求 传入id用户 与 当前登录用户 的共同关注列表 43 | */ 44 | @GetMapping("/common/{id}") 45 | public Result followCommons(@PathVariable("id") Long id) { 46 | 47 | return followService.followCommons(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/ShopController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import cn.hutool.core.util.StrUtil; 5 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 6 | import com.xydp.dto.Result; 7 | import com.xydp.entity.Shop; 8 | import com.xydp.service.IShopService; 9 | import com.xydp.utils.SystemConstants; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import javax.annotation.Resource; 13 | 14 | /** 15 | * 店铺管理 16 | * 17 | * @author 虎哥 18 | * @since 2021-12-22 19 | */ 20 | @RestController 21 | @RequestMapping("/shop") 22 | public class ShopController { 23 | 24 | @Resource 25 | public IShopService shopService; 26 | 27 | /** 28 | * 根据id查询商铺信息 29 | * 30 | * @param id 商铺id 31 | * @return 商铺详情数据 32 | */ 33 | @GetMapping("/{id}") 34 | public Result queryShopById(@PathVariable("id") Long id) { 35 | // return Result.ok(shopService.getById(id)); 36 | 37 | return shopService.queryById(id); 38 | } 39 | 40 | /** 41 | * 新增商铺信息 42 | * 43 | * @param shop 商铺数据 44 | * @return 商铺id 45 | */ 46 | @PostMapping 47 | public Result saveShop(@RequestBody Shop shop) { 48 | // 写入数据库 49 | shopService.save(shop); 50 | // 返回店铺id 51 | return Result.ok(shop.getId()); 52 | } 53 | 54 | /** 55 | * 更新商铺信息 56 | * 57 | * @param shop 商铺数据 58 | * @return 无 59 | */ 60 | @PutMapping 61 | public Result updateShop(@RequestBody Shop shop) { 62 | // 写入数据库 63 | // shopService.updateById(shop); 64 | 65 | return shopService.update(shop); 66 | } 67 | 68 | /** 69 | * 根据商铺类型分页查询商铺信息 70 | * 71 | * @param typeId 商铺类型 72 | * @param current 页码 73 | * @return 商铺列表 74 | */ 75 | @GetMapping("/of/type") 76 | public Result queryShopByType( 77 | @RequestParam("typeId") Integer typeId, 78 | @RequestParam(value = "current", defaultValue = "1") Integer current, 79 | @RequestParam(value = "x", required = false) Double x, 80 | @RequestParam(value = "y", required = false) Double y 81 | ) { 82 | return shopService.queryShopByType(typeId, current, x, y); 83 | } 84 | 85 | /** 86 | * 根据商铺名称关键字分页查询商铺信息 87 | * 88 | * @param name 商铺名称关键字 89 | * @param current 页码 90 | * @return 商铺列表 91 | */ 92 | @GetMapping("/of/name") 93 | public Result queryShopByName( 94 | @RequestParam(value = "name", required = false) String name, 95 | @RequestParam(value = "current", defaultValue = "1") Integer current 96 | ) { 97 | // 根据类型分页查询 98 | Page page = shopService.query() 99 | .like(StrUtil.isNotBlank(name), "name", name) 100 | .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); 101 | // 返回数据 102 | return Result.ok(page.getRecords()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/ShopTypeController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import com.xydp.dto.Result; 5 | import com.xydp.service.IShopTypeService; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import javax.annotation.Resource; 11 | 12 | /** 13 | * 店铺类型管理 14 | * 15 | * @author 虎哥 16 | * @since 2021-12-22 17 | */ 18 | @RestController 19 | @RequestMapping("/shop-type") 20 | public class ShopTypeController { 21 | @Resource 22 | private IShopTypeService typeService; 23 | 24 | @GetMapping("list") 25 | public Result queryTypeList() { 26 | // 直接访问数据库写法 27 | /*List typeList = typeService 28 | .query().orderByAsc("sort").list(); 29 | return Result.ok(typeList);*/ 30 | 31 | return typeService.queryTypeList(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/TokenController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | import com.xydp.service.ITokenService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | /** 9 | * @author Wuxy 10 | * @version 1.0 11 | * @ClassName TokenController 12 | * @since 2023/6/3 17:25 13 | */ 14 | @RestController 15 | public class TokenController { 16 | @Autowired 17 | private ITokenService tokenService; 18 | 19 | 20 | @GetMapping("token") 21 | public String getOrderToken() { 22 | 23 | return tokenService.getOrderToken(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/UploadController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.xydp.dto.Result; 6 | import com.xydp.utils.SystemConstants; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.web.bind.annotation.*; 9 | import org.springframework.web.multipart.MultipartFile; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.util.UUID; 14 | 15 | /** 16 | * 上传管理 17 | */ 18 | @Slf4j 19 | @RestController 20 | @RequestMapping("upload") 21 | public class UploadController { 22 | 23 | @PostMapping("blog") 24 | public Result uploadImage(@RequestParam("file") MultipartFile image) { 25 | try { 26 | // 获取原始文件名称 27 | String originalFilename = image.getOriginalFilename(); 28 | // 生成新文件名 29 | String fileName = createNewFileName(originalFilename); 30 | // 保存文件 31 | image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName)); 32 | // 返回结果 33 | log.debug("文件上传成功,{}", fileName); 34 | return Result.ok(fileName); 35 | } catch (IOException e) { 36 | throw new RuntimeException("文件上传失败", e); 37 | } 38 | } 39 | 40 | @GetMapping("/blog/delete") 41 | public Result deleteBlogImg(@RequestParam("name") String filename) { 42 | File file = new File(SystemConstants.IMAGE_UPLOAD_DIR, filename); 43 | if (file.isDirectory()) { 44 | return Result.fail("错误的文件名称"); 45 | } 46 | FileUtil.del(file); 47 | return Result.ok(); 48 | } 49 | 50 | private String createNewFileName(String originalFilename) { 51 | // 获取后缀 52 | String suffix = StrUtil.subAfter(originalFilename, ".", true); 53 | // 生成目录 54 | String name = UUID.randomUUID().toString(); 55 | int hash = name.hashCode(); 56 | int d1 = hash & 0xF; 57 | int d2 = (hash >> 4) & 0xF; 58 | // 判断目录是否存在 59 | File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2)); 60 | if (!dir.exists()) { 61 | dir.mkdirs(); 62 | } 63 | // 生成文件名 64 | return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import cn.hutool.core.bean.BeanUtil; 5 | import com.xydp.dto.LoginFormDTO; 6 | import com.xydp.dto.Result; 7 | import com.xydp.dto.UserDTO; 8 | import com.xydp.entity.User; 9 | import com.xydp.entity.UserInfo; 10 | import com.xydp.service.IUserInfoService; 11 | import com.xydp.service.IUserService; 12 | import com.xydp.utils.UserHolder; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.annotation.Resource; 17 | import javax.servlet.http.HttpSession; 18 | 19 | /** 20 | *用户管理 21 | * 22 | * @author 虎哥 23 | * @since 2021-12-22 24 | */ 25 | @Slf4j 26 | @RestController 27 | @RequestMapping("/user") 28 | public class UserController { 29 | 30 | @Resource 31 | private IUserService userService; 32 | 33 | @Resource 34 | private IUserInfoService userInfoService; 35 | 36 | /** 37 | * 发送手机验证码 38 | */ 39 | @PostMapping("code") 40 | public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { 41 | // TODO 发送短信验证码并保存验证码 42 | // return Result.fail("功能未完成"); 43 | return userService.sendCode(phone, session); 44 | } 45 | 46 | /** 47 | * 登录功能 48 | * 49 | * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码 50 | */ 51 | @PostMapping("/login") 52 | public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) { 53 | // TODO 实现登录功能 54 | // return Result.fail("功能未完成"); 55 | return userService.login(loginForm, session); 56 | } 57 | 58 | /** 59 | * 登出功能 60 | * 61 | * @return 无 62 | */ 63 | @PostMapping("/logout") 64 | public Result logout() { 65 | // TODO 实现登出功能 66 | UserHolder.removeUser(); 67 | 68 | return Result.ok("退出成功!"); 69 | } 70 | 71 | @GetMapping("/me") 72 | public Result me() { 73 | // TODO 获取当前登录的用户并返回 74 | // return Result.fail("功能未完成"); 75 | UserDTO user = UserHolder.getUser(); 76 | return Result.ok(user); 77 | } 78 | 79 | @GetMapping("/info/{id}") 80 | public Result info(@PathVariable("id") Long userId) { 81 | // 查询详情 82 | UserInfo info = userInfoService.getById(userId); 83 | if (info == null) { 84 | // 没有详情,应该是第一次查看详情 85 | return Result.ok(); 86 | } 87 | info.setCreateTime(null); 88 | info.setUpdateTime(null); 89 | // 返回 90 | return Result.ok(info); 91 | } 92 | 93 | @GetMapping("/{id}") 94 | public Result queryUserById(@PathVariable("id") Long userId) { 95 | // 查询详情 96 | User user = userService.getById(userId); 97 | if (user == null) { 98 | return Result.ok(); 99 | } 100 | UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); 101 | // 返回 102 | return Result.ok(userDTO); 103 | } 104 | 105 | /** 106 | * 签到 107 | * 108 | * @return Result 109 | */ 110 | @PostMapping("/sign") 111 | public Result sign() { 112 | return userService.sign(); 113 | } 114 | 115 | /** 116 | * 签到统计 117 | * 118 | * @return 连续签到天数 119 | */ 120 | @GetMapping("/sign/count") 121 | public Result signCount() { 122 | return userService.signCount(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/VoucherController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | 4 | import com.xydp.dto.Result; 5 | import com.xydp.entity.Voucher; 6 | import com.xydp.service.IVoucherService; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import javax.annotation.Resource; 10 | 11 | /** 12 | * 优惠券管理 13 | * 14 | * @author 虎哥 15 | * @since 2021-12-22 16 | */ 17 | @RestController 18 | @RequestMapping("/voucher") 19 | public class VoucherController { 20 | 21 | @Resource 22 | private IVoucherService voucherService; 23 | 24 | /** 25 | * 新增普通券 26 | * 27 | * @param voucher 优惠券信息 28 | * @return 优惠券id 29 | */ 30 | @PostMapping 31 | public Result addVoucher(@RequestBody Voucher voucher) { 32 | voucherService.save(voucher); 33 | return Result.ok(voucher.getId()); 34 | } 35 | 36 | /** 37 | * 新增秒杀券 38 | * 39 | * @param voucher 优惠券信息,包含秒杀信息 40 | * @return 优惠券id 41 | */ 42 | @PostMapping("seckill") 43 | public Result addSeckillVoucher(@RequestBody Voucher voucher) { 44 | voucherService.addSeckillVoucher(voucher); 45 | return Result.ok(voucher.getId()); 46 | } 47 | 48 | /** 49 | * 查询店铺的优惠券列表 50 | * 51 | * @param shopId 店铺id 52 | * @return 优惠券列表 53 | */ 54 | @GetMapping("/list/{shopId}") 55 | public Result queryVoucherOfShop(@PathVariable("shopId") Long shopId) { 56 | return voucherService.queryVoucherOfShop(shopId); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/controller/VoucherOrderController.java: -------------------------------------------------------------------------------- 1 | package com.xydp.controller; 2 | 3 | import com.xydp.dto.Result; 4 | import com.xydp.prevent.Prevent; 5 | import com.xydp.service.IVoucherOrderService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | /** 13 | * 订单管理 14 | * 15 | * @author 虎哥 16 | * @since 2021-12-22 17 | */ 18 | @RestController 19 | @RequestMapping("/voucher-order") 20 | public class VoucherOrderController { 21 | 22 | @Autowired 23 | private IVoucherOrderService voucherOrderService; 24 | 25 | /** 26 | * 秒杀下单功能 27 | * 28 | * @param voucherId 优惠券id 29 | * @return 执行响应结果 30 | */ 31 | @PostMapping("seckill/{id}") 32 | @Prevent(value = 10, count = 5) 33 | public Result seckillVoucher(@PathVariable("id") Long voucherId) { 34 | return voucherOrderService.seckillVoucher(voucherId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/dto/LoginFormDTO.java: -------------------------------------------------------------------------------- 1 | package com.xydp.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class LoginFormDTO { 9 | private String phone; 10 | private String code; 11 | private String password; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/dto/Result.java: -------------------------------------------------------------------------------- 1 | package com.xydp.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.List; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class Result { 13 | private Boolean success; 14 | private String errorMsg; 15 | private Object data; 16 | private Long total; 17 | 18 | public static Result ok() { 19 | return new Result(true, null, null, null); 20 | } 21 | 22 | public static Result ok(Object data) { 23 | return new Result(true, null, data, null); 24 | } 25 | 26 | public static Result ok(List data, Long total) { 27 | return new Result(true, null, data, total); 28 | } 29 | 30 | public static Result fail(String errorMsg) { 31 | return new Result(false, errorMsg, null, null); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/dto/ScrollResult.java: -------------------------------------------------------------------------------- 1 | package com.xydp.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class ScrollResult { 9 | private List list; 10 | private Long minTime; // 最小时间 11 | private Integer offset; // 偏移量 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/dto/SeckillOrderDTO.java: -------------------------------------------------------------------------------- 1 | package com.xydp.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | /** 7 | * 秒杀订单基本信息实体类 8 | * 9 | * @author Wuxy 10 | * @version 1.0 11 | * @ClassName SeckillOrderDTO 12 | * @since 2023/1/14 10:41 13 | */ 14 | @Data 15 | @Builder 16 | public class SeckillOrderDTO { 17 | 18 | private Integer userId; 19 | 20 | private Integer voucherId; 21 | 22 | private Long orderId; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/dto/UserDTO.java: -------------------------------------------------------------------------------- 1 | package com.xydp.dto; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UserDTO { 7 | private Long id; 8 | private String nickName; 9 | private String icon; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/Blog.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.experimental.Accessors; 10 | 11 | import java.io.Serializable; 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | *

16 | * 博客实体类 17 | *

18 | * 19 | * @author 虎哥 20 | * @since 2021-12-22 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @Accessors(chain = true) 25 | @TableName("tb_blog") 26 | public class Blog implements Serializable { 27 | /** 28 | * 序列号ID 29 | */ 30 | private static final long serialVersionUID = 1L; 31 | 32 | /** 33 | * 主键 34 | */ 35 | @TableId(value = "id", type = IdType.AUTO) 36 | private Long id; 37 | 38 | /** 39 | * 商户id 40 | */ 41 | private Long shopId; 42 | 43 | /** 44 | * 用户id 45 | */ 46 | private Long userId; 47 | 48 | /** 49 | * 用户图标 50 | */ 51 | @TableField(exist = false) 52 | private String icon; 53 | 54 | /** 55 | * 用户姓名 56 | */ 57 | @TableField(exist = false) 58 | private String name; 59 | 60 | /** 61 | * 是否点赞过了 62 | */ 63 | @TableField(exist = false) 64 | private Boolean isLike; 65 | 66 | /** 67 | * 标题 68 | */ 69 | private String title; 70 | 71 | /** 72 | * 探店的照片,最多9张,多张以","隔开 73 | */ 74 | private String images; 75 | 76 | /** 77 | * 探店的文字描述 78 | */ 79 | private String content; 80 | 81 | /** 82 | * 点赞数量 83 | */ 84 | private Integer liked; 85 | 86 | /** 87 | * 评论数量 88 | */ 89 | private Integer comments; 90 | 91 | /** 92 | * 创建时间 93 | */ 94 | private LocalDateTime createTime; 95 | 96 | /** 97 | * 更新时间 98 | */ 99 | private LocalDateTime updateTime; 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/BlogComments.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | *

15 | * 16 | *

17 | * 18 | * @author 虎哥 19 | * @since 2021-12-22 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = false) 23 | @Accessors(chain = true) 24 | @TableName("tb_blog_comments") 25 | public class BlogComments implements Serializable { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * 主键 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 用户id 37 | */ 38 | private Long userId; 39 | 40 | /** 41 | * 探店id 42 | */ 43 | private Long blogId; 44 | 45 | /** 46 | * 关联的1级评论id,如果是一级评论,则值为0 47 | */ 48 | private Long parentId; 49 | 50 | /** 51 | * 回复的评论id 52 | */ 53 | private Long answerId; 54 | 55 | /** 56 | * 回复的内容 57 | */ 58 | private String content; 59 | 60 | /** 61 | * 点赞数 62 | */ 63 | private Integer liked; 64 | 65 | /** 66 | * 状态,0:正常,1:被举报,2:禁止查看 67 | */ 68 | private Boolean status; 69 | 70 | /** 71 | * 创建时间 72 | */ 73 | private LocalDateTime createTime; 74 | 75 | /** 76 | * 更新时间 77 | */ 78 | private LocalDateTime updateTime; 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/Follow.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | *

15 | * 16 | *

17 | * 18 | * @author 虎哥 19 | * @since 2021-12-22 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = false) 23 | @Accessors(chain = true) 24 | @TableName("tb_follow") 25 | public class Follow implements Serializable { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * 主键 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 用户id 37 | */ 38 | private Long userId; 39 | 40 | /** 41 | * 关联的用户id 42 | */ 43 | private Long followUserId; 44 | 45 | /** 46 | * 创建时间 47 | */ 48 | private LocalDateTime createTime; 49 | 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/SeckillVoucher.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | *

15 | * 秒杀优惠券表,与优惠券是一对一关系 16 | *

17 | * 18 | * @author 虎哥 19 | * @since 2022-01-04 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = false) 23 | @Accessors(chain = true) 24 | @TableName("tb_seckill_voucher") 25 | public class SeckillVoucher implements Serializable { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * 关联的优惠券的id 31 | */ 32 | @TableId(value = "voucher_id", type = IdType.INPUT) 33 | private Long voucherId; 34 | 35 | /** 36 | * 库存 37 | */ 38 | private Integer stock; 39 | 40 | /** 41 | * 创建时间 42 | */ 43 | private LocalDateTime createTime; 44 | 45 | /** 46 | * 生效时间 47 | */ 48 | private LocalDateTime beginTime; 49 | 50 | /** 51 | * 失效时间 52 | */ 53 | private LocalDateTime endTime; 54 | 55 | /** 56 | * 更新时间 57 | */ 58 | private LocalDateTime updateTime; 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/Shop.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.experimental.Accessors; 10 | 11 | import java.io.Serializable; 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | *

16 | * 店铺实体类 17 | *

18 | * 19 | * @author 虎哥 20 | * @since 2021-12-22 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @Accessors(chain = true) 25 | @TableName("tb_shop") 26 | public class Shop implements Serializable { 27 | 28 | private static final long serialVersionUID = 1L; 29 | 30 | /** 31 | * 主键 32 | */ 33 | @TableId(value = "id", type = IdType.AUTO) 34 | private Long id; 35 | 36 | /** 37 | * 商铺名称 38 | */ 39 | private String name; 40 | 41 | /** 42 | * 商铺类型的id 43 | */ 44 | private Long typeId; 45 | 46 | /** 47 | * 商铺图片,多个图片以','隔开 48 | */ 49 | private String images; 50 | 51 | /** 52 | * 商圈,例如陆家嘴 53 | */ 54 | private String area; 55 | 56 | /** 57 | * 地址 58 | */ 59 | private String address; 60 | 61 | /** 62 | * 经度 63 | */ 64 | private Double x; 65 | 66 | /** 67 | * 维度 68 | */ 69 | private Double y; 70 | 71 | /** 72 | * 均价,取整数 73 | */ 74 | private Long avgPrice; 75 | 76 | /** 77 | * 销量 78 | */ 79 | private Integer sold; 80 | 81 | /** 82 | * 评论数量 83 | */ 84 | private Integer comments; 85 | 86 | /** 87 | * 评分,1~5分,乘10保存,避免小数 88 | */ 89 | private Integer score; 90 | 91 | /** 92 | * 营业时间,例如 10:00-22:00 93 | */ 94 | private String openHours; 95 | 96 | /** 97 | * 创建时间 98 | */ 99 | private LocalDateTime createTime; 100 | 101 | /** 102 | * 更新时间 103 | */ 104 | private LocalDateTime updateTime; 105 | 106 | @TableField(exist = false) 107 | private Double distance; 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/ShopType.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.experimental.Accessors; 10 | 11 | import java.io.Serializable; 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | *

16 | * 17 | *

18 | * 19 | * @author 虎哥 20 | * @since 2021-12-22 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @Accessors(chain = true) 25 | @TableName("tb_shop_type") 26 | public class ShopType implements Serializable { 27 | 28 | private static final long serialVersionUID = 1L; 29 | 30 | /** 31 | * 主键 32 | */ 33 | @TableId(value = "id", type = IdType.AUTO) 34 | private Long id; 35 | 36 | /** 37 | * 类型名称 38 | */ 39 | private String name; 40 | 41 | /** 42 | * 图标 43 | */ 44 | private String icon; 45 | 46 | /** 47 | * 顺序 48 | */ 49 | private Integer sort; 50 | 51 | /** 52 | * 创建时间 53 | */ 54 | @JsonIgnore 55 | private LocalDateTime createTime; 56 | 57 | /** 58 | * 更新时间 59 | */ 60 | @JsonIgnore 61 | private LocalDateTime updateTime; 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | *

15 | * 16 | *

17 | * 18 | * @author 虎哥 19 | * @since 2021-12-22 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = false) 23 | @Accessors(chain = true) 24 | @TableName("tb_user") 25 | public class User implements Serializable { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * 主键 31 | */ 32 | @TableId(value = "id", type = IdType.AUTO) 33 | private Long id; 34 | 35 | /** 36 | * 手机号码 37 | */ 38 | private String phone; 39 | 40 | /** 41 | * 密码,加密存储 42 | */ 43 | private String password; 44 | 45 | /** 46 | * 昵称,默认是随机字符 47 | */ 48 | private String nickName; 49 | 50 | /** 51 | * 用户头像 52 | */ 53 | private String icon = ""; 54 | 55 | /** 56 | * 创建时间 57 | */ 58 | private LocalDateTime createTime; 59 | 60 | /** 61 | * 更新时间 62 | */ 63 | private LocalDateTime updateTime; 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDate; 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | *

16 | * 17 | *

18 | * 19 | * @author 虎哥 20 | * @since 2021-12-24 21 | */ 22 | @Data 23 | @EqualsAndHashCode(callSuper = false) 24 | @Accessors(chain = true) 25 | @TableName("tb_user_info") 26 | public class UserInfo implements Serializable { 27 | 28 | private static final long serialVersionUID = 1L; 29 | 30 | /** 31 | * 主键,用户id 32 | */ 33 | @TableId(value = "user_id", type = IdType.AUTO) 34 | private Long userId; 35 | 36 | /** 37 | * 城市名称 38 | */ 39 | private String city; 40 | 41 | /** 42 | * 个人介绍,不要超过128个字符 43 | */ 44 | private String introduce; 45 | 46 | /** 47 | * 粉丝数量 48 | */ 49 | private Integer fans; 50 | 51 | /** 52 | * 关注的人的数量 53 | */ 54 | private Integer followee; 55 | 56 | /** 57 | * 性别,0:男,1:女 58 | */ 59 | private Boolean gender; 60 | 61 | /** 62 | * 生日 63 | */ 64 | private LocalDate birthday; 65 | 66 | /** 67 | * 积分 68 | */ 69 | private Integer credits; 70 | 71 | /** 72 | * 会员级别,0~9级,0代表未开通会员 73 | */ 74 | private Boolean level; 75 | 76 | /** 77 | * 创建时间 78 | */ 79 | private LocalDateTime createTime; 80 | 81 | /** 82 | * 更新时间 83 | */ 84 | private LocalDateTime updateTime; 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/Voucher.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableField; 5 | import com.baomidou.mybatisplus.annotation.TableId; 6 | import com.baomidou.mybatisplus.annotation.TableName; 7 | import com.fasterxml.jackson.annotation.JsonFormat; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.experimental.Accessors; 11 | 12 | import java.io.Serializable; 13 | import java.time.LocalDateTime; 14 | 15 | /** 16 | *

17 | * 优惠券实体类 18 | *

19 | * 20 | * @author Wuxy 21 | * @since 2023-1-13 22 | */ 23 | @Data 24 | @EqualsAndHashCode(callSuper = false) 25 | @Accessors(chain = true) 26 | @TableName("tb_voucher") 27 | public class Voucher implements Serializable { 28 | 29 | private static final long serialVersionUID = 1L; 30 | 31 | /** 32 | * 主键 33 | */ 34 | @TableId(value = "id", type = IdType.AUTO) 35 | private Long id; 36 | 37 | /** 38 | * 商铺id 39 | */ 40 | private Long shopId; 41 | 42 | /** 43 | * 代金券标题 44 | */ 45 | private String title; 46 | 47 | /** 48 | * 副标题 49 | */ 50 | private String subTitle; 51 | 52 | /** 53 | * 使用规则 54 | */ 55 | private String rules; 56 | 57 | /** 58 | * 支付金额 59 | */ 60 | private Long payValue; 61 | 62 | /** 63 | * 抵扣金额 64 | */ 65 | private Long actualValue; 66 | 67 | /** 68 | * 优惠券类型 69 | */ 70 | private Integer type; 71 | 72 | /** 73 | * 优惠券类型 74 | */ 75 | private Integer status; 76 | /** 77 | * 库存 78 | */ 79 | @TableField(exist = false) 80 | private Integer stock; 81 | 82 | /** 83 | * 生效时间 84 | */ 85 | @TableField(exist = false) 86 | private LocalDateTime beginTime; 87 | 88 | /** 89 | * 失效时间 90 | */ 91 | @TableField(exist = false) 92 | @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") 93 | private LocalDateTime endTime; 94 | 95 | /** 96 | * 创建时间 97 | */ 98 | private LocalDateTime createTime; 99 | 100 | 101 | /** 102 | * 更新时间 103 | */ 104 | private LocalDateTime updateTime; 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/entity/VoucherOrder.java: -------------------------------------------------------------------------------- 1 | package com.xydp.entity; 2 | 3 | import com.baomidou.mybatisplus.annotation.IdType; 4 | import com.baomidou.mybatisplus.annotation.TableId; 5 | import com.baomidou.mybatisplus.annotation.TableName; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.io.Serializable; 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | *

15 | * 16 | *

17 | * 18 | * @author 虎哥 19 | * @since 2021-12-22 20 | */ 21 | @Data 22 | @EqualsAndHashCode(callSuper = false) 23 | @Accessors(chain = true) 24 | @TableName("tb_voucher_order") 25 | public class VoucherOrder implements Serializable { 26 | 27 | private static final long serialVersionUID = 1L; 28 | 29 | /** 30 | * 主键 31 | */ 32 | @TableId(value = "id", type = IdType.INPUT) 33 | private Long id; 34 | 35 | /** 36 | * 下单的用户id 37 | */ 38 | private Long userId; 39 | 40 | /** 41 | * 购买的代金券id 42 | */ 43 | private Long voucherId; 44 | 45 | /** 46 | * 支付方式 1:余额支付;2:支付宝;3:微信 47 | */ 48 | private Integer payType; 49 | 50 | /** 51 | * 订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款 52 | */ 53 | private Integer status; 54 | 55 | /** 56 | * 下单时间 57 | */ 58 | private LocalDateTime createTime; 59 | 60 | /** 61 | * 支付时间 62 | */ 63 | private LocalDateTime payTime; 64 | 65 | /** 66 | * 核销时间 67 | */ 68 | private LocalDateTime useTime; 69 | 70 | /** 71 | * 退款时间 72 | */ 73 | private LocalDateTime refundTime; 74 | 75 | /** 76 | * 更新时间 77 | */ 78 | private LocalDateTime updateTime; 79 | 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/exception/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.xydp.exception; 2 | 3 | /** 4 | * @author Wuxy 5 | * @version 1.0 6 | * @ClassName BusinessException 7 | * @since 2023/1/14 15:28 8 | */ 9 | public class BusinessException extends RuntimeException { 10 | 11 | private Throwable cause; 12 | 13 | private String message; 14 | 15 | public BusinessException(String message) { 16 | this.message = message; 17 | } 18 | 19 | public BusinessException(Throwable cause) { 20 | this.cause = cause; 21 | } 22 | 23 | public BusinessException(String message, Throwable cause) { 24 | super(message, cause); 25 | } 26 | 27 | @Override 28 | public String getMessage() { 29 | return message; 30 | } 31 | 32 | public void setMessage(String message) { 33 | this.message = message; 34 | } 35 | 36 | @Override 37 | public Throwable getCause() { 38 | return cause; 39 | } 40 | 41 | public void setCause(Throwable cause) { 42 | this.cause = cause; 43 | } 44 | 45 | /** 46 | * 不写入堆栈信息,提高性能 47 | */ 48 | @Override 49 | public Throwable fillInStackTrace() { 50 | return this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/interceptor/LoginInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xydp.interceptor; 2 | 3 | import com.xydp.utils.UserHolder; 4 | import org.springframework.web.servlet.HandlerInterceptor; 5 | import org.springframework.web.servlet.ModelAndView; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | 10 | public class LoginInterceptor implements HandlerInterceptor { 11 | @Override 12 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 13 | // 1.判断是否需要拦截(Thread Local中是否有用户) 14 | if (UserHolder.getUser() == null) { 15 | // 没有,需要拦截,设置状态码 16 | response.setStatus(401); 17 | // 拦截 18 | return false; 19 | } 20 | // 有用户则放行 21 | return true; 22 | } 23 | 24 | @Override 25 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 26 | HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); 27 | } 28 | 29 | @Override 30 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 31 | HandlerInterceptor.super.afterCompletion(request, response, handler, ex); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/interceptor/RefreshTokenInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xydp.interceptor; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.xydp.dto.UserDTO; 6 | import com.xydp.utils.RedisConstants; 7 | import com.xydp.utils.UserHolder; 8 | import org.springframework.data.redis.core.StringRedisTemplate; 9 | import org.springframework.web.servlet.HandlerInterceptor; 10 | import org.springframework.web.servlet.ModelAndView; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.util.Map; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | /** 18 | * 刷新 token 的拦截器,只做刷新token的功能,放行一切请求 19 | */ 20 | public class RefreshTokenInterceptor implements HandlerInterceptor { 21 | /** 22 | * 自己手动创建的对象,里面的实例需要自己手动注入, 23 | * spring创建的对象,可以使用自动注入方法。 24 | */ 25 | private final StringRedisTemplate stringRedisTemplate; 26 | 27 | public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) { 28 | this.stringRedisTemplate = stringRedisTemplate; 29 | } 30 | 31 | @Override 32 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 33 | // 1.获取请求头中的token 34 | String token = request.getHeader("authorization"); 35 | if (StrUtil.isBlank(token)) { 36 | return true; 37 | } 38 | 39 | // 2.基于token获取redis中的用户 40 | String key = RedisConstants.LOGIN_USER_KEY + token; 41 | Map userMap = stringRedisTemplate.opsForHash().entries(key); 42 | // 3.判断用户是否存在 43 | if (userMap.isEmpty()) { 44 | // 4.不存在,放行 45 | return true; 46 | } 47 | // 5.将查询到的Hash数据转为UserDTO对象 48 | UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); 49 | // 6. 存在,保存用户信息到 ThreadLocal 50 | UserHolder.saveUser(userDTO); 51 | // 7.刷新token有效期 52 | stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS); 53 | // 8.放行 54 | return true; 55 | } 56 | 57 | @Override 58 | public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { 59 | HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); 60 | } 61 | 62 | @Override 63 | public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 64 | HandlerInterceptor.super.afterCompletion(request, response, handler, ex); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/BlogCommentsMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.BlogComments; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface BlogCommentsMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/BlogMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.Blog; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface BlogMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/FollowMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.Follow; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface FollowMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/SeckillVoucherMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.SeckillVoucher; 5 | 6 | /** 7 | *

8 | * 秒杀优惠券表,与优惠券是一对一关系 Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2022-01-04 13 | */ 14 | public interface SeckillVoucherMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/ShopMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.Shop; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface ShopMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/ShopTypeMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.ShopType; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface ShopTypeMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/UserInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.UserInfo; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-24 13 | */ 14 | public interface UserInfoMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.User; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface UserMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/VoucherMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.Voucher; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | *

11 | * Mapper 接口 12 | *

13 | * 14 | * @author 虎哥 15 | * @since 2021-12-22 16 | */ 17 | public interface VoucherMapper extends BaseMapper { 18 | 19 | /** 20 | * 根据店铺 ID 查询所有的优惠券 21 | * 22 | * @param shopId 店铺 ID 23 | * @return 优惠券列表 {@link List}<{@link Voucher}> 24 | */ 25 | List queryVoucherOfShop(@Param("shopId") Long shopId); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mapper/VoucherOrderMapper.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mapper; 2 | 3 | import com.baomidou.mybatisplus.core.mapper.BaseMapper; 4 | import com.xydp.entity.VoucherOrder; 5 | 6 | /** 7 | *

8 | * Mapper 接口 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface VoucherOrderMapper extends BaseMapper { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mq/MqReceiver.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mq; 2 | 3 | import com.rabbitmq.client.Channel; 4 | import com.xydp.entity.VoucherOrder; 5 | import com.xydp.service.IVoucherOrderService; 6 | import com.xydp.utils.MQConstants; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.amqp.core.Message; 9 | import org.springframework.amqp.rabbit.annotation.RabbitHandler; 10 | import org.springframework.amqp.rabbit.annotation.RabbitListener; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.messaging.handler.annotation.Payload; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.io.IOException; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | /** 20 | * RabbitMQ 消息接收器类 21 | * 22 | * @author Wuxy 23 | * @version 1.0 24 | * @ClassName MqReceiver 25 | * @since 2023/1/14 10:21 26 | */ 27 | @Slf4j 28 | @RabbitListener(queues = MQConstants.SECKILL_QUEUE, ackMode = "MANUAL") 29 | @Component 30 | public class MqReceiver { 31 | 32 | @Autowired 33 | IVoucherOrderService voucherOrderService; 34 | 35 | private final ExecutorService threadPool = Executors.newFixedThreadPool(16); 36 | 37 | @RabbitHandler 38 | public void receiveSeckillOrder(@Payload VoucherOrder voucherOrder, Channel channel, Message message) { 39 | log.info("接收到的订单消息:" + voucherOrder); 40 | threadPool.submit(() -> { 41 | try { 42 | voucherOrderService.createVoucherOrder(voucherOrder); 43 | } catch (Exception e1) { 44 | log.warn("订单处理异常,重新尝试。"); 45 | try { 46 | voucherOrderService.createVoucherOrder(voucherOrder); 47 | } catch (Exception e2) { 48 | log.error("订单处理失败:", e2); 49 | // todo: 第二次处理失败,则更改 Redis 中的数据(也可以将消息放入异常订单数据库或队列中特殊处理)-如回滚库存等操作。。。 50 | } 51 | } 52 | // 手动确认消费完成 53 | try { 54 | channel.basicAck(message.getMessageProperties().getDeliveryTag(), true); 55 | } catch (IOException e) { 56 | throw new RuntimeException(e); 57 | } 58 | }); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/mq/MqSender.java: -------------------------------------------------------------------------------- 1 | package com.xydp.mq; 2 | 3 | import com.xydp.entity.VoucherOrder; 4 | import com.xydp.utils.MQConstants; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.amqp.rabbit.connection.CorrelationData; 7 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * RabbitMQ 消息发送器 13 | * 14 | * @author Wuxy 15 | * @version 1.0 16 | * @ClassName MqSender 17 | * @since 2023/1/14 10:21 18 | */ 19 | @Slf4j 20 | @Component 21 | public class MqSender { 22 | 23 | @Autowired 24 | RabbitTemplate rabbitTemplate; 25 | 26 | 27 | /** 28 | * 发送秒杀订单消息,这里需要保证可靠传递性,失败重传,消息发送到队列失败,进行消息回退 29 | * 30 | * @param voucherOrder 秒杀订单信息 31 | * @param reliable 是否保证可靠传输模式 32 | */ 33 | public void sendSeckillMessage(VoucherOrder voucherOrder, boolean reliable) { 34 | log.info("发送消息:" + voucherOrder); 35 | if (reliable) { 36 | // 定义确认回调机制,当 publisher 将消息发送到 exchange 失败,则重新发送一次 37 | rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> { 38 | if (!ack) { // 如果发送失败,则重新再次发送 39 | log.error("消息发送失败,错误原因:{},再次发送。", cause); 40 | // 重新发送消息 41 | rabbitTemplate.convertAndSend(MQConstants.SECKILL_EXCHANGE, MQConstants.SECKILL_ROUTING_KEY, 42 | voucherOrder); //, new CorrelationData(voucherOrder.getId().toString()) 43 | } 44 | }); 45 | // 设置交换机处理消息的模式 46 | rabbitTemplate.setMandatory(true); 47 | // 设置退回函数,当 exchange 将消息发送到队列失败时,会自动将消息退回给 publisher 48 | rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> 49 | log.error("交换机发送消息到队列失败,错误原因:{},执行将消息退回到 publisher 操作。", replyText)); 50 | } 51 | // 发送消息(默认为消息持久化) 52 | rabbitTemplate.convertAndSend(MQConstants.SECKILL_EXCHANGE, MQConstants.SECKILL_ROUTING_KEY, 53 | voucherOrder); // , new CorrelationData(voucherOrder.getId().toString()) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/prevent/Prevent.java: -------------------------------------------------------------------------------- 1 | package com.xydp.prevent; 2 | 3 | import org.aspectj.lang.JoinPoint; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * 接口防刷注解(控制相同 ip 和用户在规定时间内的最大访问次数。具体实现见{@link PreventAop#joinPoint(JoinPoint)}) 9 | * 10 | * @author Wuxy 11 | * @version 1.0 12 | * @ClassName Prevent 13 | * @see PreventAop 14 | * @since 2023/1/13 16:30 15 | */ 16 | @Target(ElementType.METHOD) 17 | @Retention(RetentionPolicy.RUNTIME) 18 | @Documented 19 | public @interface Prevent { 20 | 21 | /** 22 | * 限制的时间间隔(秒) 23 | */ 24 | long value() default 5; 25 | 26 | /** 27 | * 最大访问次数 28 | */ 29 | int count() default 5; 30 | 31 | /** 32 | * 提示信息 33 | */ 34 | String message() default ""; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/prevent/PreventAop.java: -------------------------------------------------------------------------------- 1 | package com.xydp.prevent; 2 | 3 | import cn.hutool.crypto.digest.MD5; 4 | import com.xydp.exception.BusinessException; 5 | import com.xydp.utils.IPUtils; 6 | import com.xydp.utils.RedisConstants; 7 | import com.xydp.utils.UserHolder; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.aspectj.lang.JoinPoint; 10 | import org.aspectj.lang.ProceedingJoinPoint; 11 | import org.aspectj.lang.annotation.Around; 12 | import org.aspectj.lang.annotation.Aspect; 13 | import org.aspectj.lang.annotation.Before; 14 | import org.aspectj.lang.annotation.Pointcut; 15 | import org.aspectj.lang.reflect.MethodSignature; 16 | import org.redisson.api.RLock; 17 | import org.redisson.api.RedissonClient; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.data.redis.core.StringRedisTemplate; 20 | import org.springframework.stereotype.Component; 21 | import org.springframework.util.DigestUtils; 22 | import org.springframework.util.StringUtils; 23 | import org.springframework.web.context.request.RequestContextHolder; 24 | import org.springframework.web.context.request.ServletRequestAttributes; 25 | 26 | import javax.servlet.http.HttpServletRequest; 27 | import javax.servlet.http.HttpServletResponse; 28 | import java.lang.reflect.Method; 29 | import java.util.Arrays; 30 | import java.util.Objects; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | import static com.xydp.utils.RedisConstants.ACCESS_LIMIT_IP_KEY; 34 | import static com.xydp.utils.RedisConstants.ACCESS_LIMIT_USER_KEY; 35 | 36 | /** 37 | * 接口防刷切面处理实现类 38 | * 39 | * @author Wuxy 40 | * @version 1.0 41 | * @ClassName PreventAop 42 | * @since 2023/1/13 16:38 43 | */ 44 | @Aspect // 声明当前类是一个切面 45 | @Component 46 | @Slf4j 47 | public class PreventAop { 48 | 49 | @Autowired 50 | private StringRedisTemplate stringRedisTemplate; 51 | 52 | @Autowired 53 | private RedissonClient redissonClient; 54 | 55 | /** 56 | * 定义防刷切入点 57 | */ 58 | @Pointcut("@annotation(com.xydp.prevent.Prevent)") 59 | public void pointcut() { 60 | 61 | } 62 | 63 | /** 64 | * 目标方法执行前的进行的逻辑 65 | */ 66 | @Before("pointcut()") 67 | public void joinPoint(JoinPoint joinPoint) throws NoSuchMethodException { 68 | // 这里主要限制每一个 IP 和 User 在规定时间内的最大访问次数 69 | // ip 锁 和 user 锁 70 | RLock ipLock = null, userLock = null; 71 | boolean isIp = false, isUser = false; 72 | try { 73 | // 得到方法签名 74 | MethodSignature signature = (MethodSignature) joinPoint.getSignature(); 75 | // 得到具体方法 76 | Method method = joinPoint.getTarget().getClass().getMethod(signature.getName(), signature.getParameterTypes()); 77 | 78 | // 得到 Prevent 注解 79 | Prevent prevent = method.getAnnotation(Prevent.class); 80 | 81 | // 获得过期时间 82 | long expire = prevent.value(); 83 | // 获得限制次数 84 | int count = prevent.count(); 85 | // 获得提示消息 86 | String message = prevent.message(); 87 | 88 | // 获得 http 请求 89 | ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()); 90 | assert attributes != null; 91 | HttpServletRequest request = attributes.getRequest(); 92 | 93 | HttpServletResponse response = attributes.getResponse(); 94 | assert response != null; 95 | response.setContentType("application/json;charset=UTF-8"); 96 | 97 | // todo:以下操作可以使用 lua 脚本进行实现,从而保证操作的原子性以及降低网络开销 98 | // 得到限制 ip 的 key 99 | String ipKey = ACCESS_LIMIT_IP_KEY + IPUtils.getIpAddr(request) + "-" + request.getRequestURI(); 100 | // 得到限制 user 的 key 101 | String userKey = ACCESS_LIMIT_USER_KEY + UserHolder.getUser().getId() + "-" + request.getRequestURI(); 102 | 103 | // 获取 ip 锁 104 | ipLock = redissonClient.getLock("lock:" + ipKey); 105 | isIp = ipLock.tryLock(); 106 | if (!isIp) { // User - 相同用户在 Redis 已经限制了 107 | throw new BusinessException("相同IP不能在同一时刻下单!"); 108 | } 109 | // 获取 User 锁 110 | userLock = redissonClient.getLock("lock:" + userKey); 111 | isUser = userLock.tryLock(); 112 | if (!isUser) { 113 | throw new BusinessException("相同用户不能在同一时刻下单!"); 114 | } 115 | // 取得在限定时间内的访问次数 116 | String ipVal = stringRedisTemplate.opsForValue().get(ipKey); 117 | int ipCount = ipVal == null ? 0 : Integer.parseInt(ipVal); 118 | String userVal = stringRedisTemplate.opsForValue().get(userKey); 119 | int userCount = userVal == null ? 0 : Integer.parseInt(userVal); 120 | if (ipCount < count && userCount < count) { 121 | if (ipCount == 0) { 122 | stringRedisTemplate.opsForValue().set(ipKey, Integer.toString(1), expire, TimeUnit.SECONDS); 123 | } else { 124 | stringRedisTemplate.opsForValue().increment(ipKey, 1); 125 | } 126 | if (userCount == 0) { 127 | stringRedisTemplate.opsForValue().set(userKey, Integer.toString(1), expire, TimeUnit.SECONDS); 128 | } else { 129 | stringRedisTemplate.opsForValue().increment(userKey, 1); 130 | } 131 | } else { 132 | message = "".equals(message) ? "相同IP或用户在" + expire + "秒内达到了最大访问次数:" + count : message; 133 | throw new BusinessException(message); 134 | } 135 | } finally { 136 | // 去释放已经拿到的锁 137 | if (isIp) { 138 | ipLock.unlock(); 139 | } 140 | if (isUser) { 141 | userLock.unlock(); 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * 定义防止重复提交切入点 148 | */ 149 | @Pointcut("@annotation(com.xydp.prevent.RepeatSubmit)") 150 | public void repeatSubmitPointCut() { 151 | 152 | } 153 | 154 | /** 155 | * 回绕通知 156 | */ 157 | @Around("repeatSubmitPointCut()") 158 | public Object around(ProceedingJoinPoint pjp) throws Throwable { 159 | // 获取当前 http request 对象 160 | HttpServletRequest request = ((ServletRequestAttributes) 161 | Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); 162 | // 获取当前登录用户id 163 | Long userId = UserHolder.getUser().getId(); 164 | // 用于记录最终结果 165 | boolean result; 166 | // 得到方法签名 167 | MethodSignature signature = (MethodSignature) pjp.getSignature(); 168 | // 得到具体方法 169 | Method method = pjp.getTarget().getClass().getMethod(signature.getName(), signature.getParameterTypes()); 170 | 171 | // 得到 @RepeatSubmit 注解 172 | RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class); 173 | 174 | // 防重类型判断 175 | String type = repeatSubmit.limitType().name(); 176 | if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) { 177 | // 方式一,参数形式防重提交 178 | long lockTime = repeatSubmit.lockTime(); 179 | String ipAddr = IPUtils.getIpAddr(request); 180 | MethodSignature methodSignature = (MethodSignature) pjp.getSignature(); 181 | method = methodSignature.getMethod(); 182 | String className = method.getDeclaringClass().getName(); 183 | String key = "order-server:repeat_submit:" + Arrays.toString(DigestUtils.md5Digest(String.format("%s-%s-%s-%s", ipAddr, className, method, userId).getBytes())); 184 | // 加锁 185 | 186 | // 这种也可以 setnx 的 187 | // result = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS); 188 | 189 | // 使用redisson 190 | RLock lock = redissonClient.getLock(key); 191 | // 尝试加锁,最多等待0秒,上锁以后5秒自动解锁 [lockTime默认为5s, 可以自定义] 192 | result = lock.tryLock(0, lockTime, TimeUnit.SECONDS); 193 | } else { 194 | // 方式二,令牌形式防重提交 195 | String requestToken = request.getHeader("request-token"); 196 | if (StringUtils.isEmpty(requestToken)) { 197 | throw new BusinessException("token is blank!"); 198 | } 199 | String key = String.format(RedisConstants.SUBMIT_ORDER_TOKEN_KEY, userId, requestToken); 200 | 201 | // 直接删除,删除成功则表示完成 202 | result = Boolean.TRUE.equals(stringRedisTemplate.delete(key)); 203 | } 204 | 205 | if (!result) { 206 | log.error("请求重复提交"); 207 | log.info("环绕通知中"); 208 | return null; 209 | } 210 | 211 | // 执行方法 212 | return pjp.proceed(); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/prevent/RepeatSubmit.java: -------------------------------------------------------------------------------- 1 | package com.xydp.prevent; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 自定义防重提交注解 7 | * 8 | * @author Wuxy 9 | * @version 1.0 10 | * @ClassName RepeatSubmit 11 | * @since 2023/6/3 17:37 12 | */ 13 | @Documented 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.METHOD) 16 | public @interface RepeatSubmit { 17 | 18 | /** 19 | * 防重提交,支持两种,一个是方法参数,一个是令牌 20 | */ 21 | enum Type {PARAM, TOKEN} 22 | 23 | /** 24 | * 默认防重提交策略,方法参数 25 | * 26 | * @return 防重提交策略 27 | */ 28 | Type limitType() default Type.PARAM; 29 | 30 | /** 31 | * 加锁过期时间,默认是 5 秒 32 | * 33 | * @return 过期时间 34 | */ 35 | long lockTime() default 5; 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IBlogCommentsService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.entity.BlogComments; 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | 6 | /** 7 | *

8 | * 服务类 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-22 13 | */ 14 | public interface IBlogCommentsService extends IService { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IBlogService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.dto.Result; 4 | import com.xydp.entity.Blog; 5 | import com.baomidou.mybatisplus.extension.service.IService; 6 | 7 | /** 8 | *

9 | * 服务类 10 | *

11 | * 12 | * @author 虎哥 13 | * @since 2021-12-22 14 | */ 15 | public interface IBlogService extends IService { 16 | 17 | Result queryHotBlog(Integer current); 18 | 19 | Result queryBlogById(Long id); 20 | 21 | Result likeBlog(Long id); 22 | 23 | Result queryBlogLikes(Long id); 24 | 25 | Result saveBlog(Blog blog); 26 | 27 | Result queryBlogOfFollow(Long max, Integer offset); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IFollowService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.dto.Result; 4 | import com.xydp.entity.Follow; 5 | import com.baomidou.mybatisplus.extension.service.IService; 6 | 7 | /** 8 | *

9 | * 服务类 10 | *

11 | * 12 | * @author 虎哥 13 | * @since 2021-12-22 14 | */ 15 | public interface IFollowService extends IService { 16 | 17 | Result follow(Long followUserId, Boolean isFollow); 18 | 19 | Result isFollow(Long followUserId); 20 | 21 | Result followCommons(Long id); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/ISeckillVoucherService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.entity.SeckillVoucher; 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | 6 | /** 7 | *

8 | * 秒杀优惠券表,与优惠券是一对一关系 服务类 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2022-01-04 13 | */ 14 | public interface ISeckillVoucherService extends IService { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IShopService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.dto.Result; 4 | import com.xydp.entity.Shop; 5 | import com.baomidou.mybatisplus.extension.service.IService; 6 | 7 | /** 8 | *

9 | * 服务类 10 | *

11 | * 12 | * @author 虎哥 13 | * @since 2021-12-22 14 | */ 15 | public interface IShopService extends IService { 16 | 17 | Result queryById(Long id); 18 | 19 | Result update(Shop shop); 20 | 21 | Result queryShopByType(Integer typeId, Integer current, Double x, Double y); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IShopTypeService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.dto.Result; 4 | import com.xydp.entity.ShopType; 5 | import com.baomidou.mybatisplus.extension.service.IService; 6 | 7 | /** 8 | *

9 | * 服务类 10 | *

11 | * 12 | * @author 虎哥 13 | * @since 2021-12-22 14 | */ 15 | public interface IShopTypeService extends IService { 16 | 17 | Result queryTypeList(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/ITokenService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | /** 4 | * Token 服务类接口 5 | * 6 | * @author Wuxy 7 | * @version 1.0 8 | * @ClassName TokenService 9 | * @since 2023/6/3 17:32 10 | */ 11 | public interface ITokenService { 12 | 13 | /** 14 | * 下单前获取令牌用户防重提交 15 | * 16 | * @return 返回生成的 token 17 | */ 18 | String getOrderToken(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IUserInfoService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.entity.UserInfo; 4 | import com.baomidou.mybatisplus.extension.service.IService; 5 | 6 | /** 7 | *

8 | * 服务类 9 | *

10 | * 11 | * @author 虎哥 12 | * @since 2021-12-24 13 | */ 14 | public interface IUserInfoService extends IService { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IUserService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.xydp.dto.LoginFormDTO; 5 | import com.xydp.dto.Result; 6 | import com.xydp.entity.User; 7 | 8 | import javax.servlet.http.HttpSession; 9 | 10 | /** 11 | *

12 | * 用户服务类 13 | *

14 | * 15 | * @author 虎哥 16 | * @since 2021-12-22 17 | */ 18 | public interface IUserService extends IService { 19 | 20 | Result sendCode(String phone, HttpSession session); 21 | 22 | Result login(LoginFormDTO loginForm, HttpSession session); 23 | 24 | Result sign(); 25 | 26 | Result signCount(); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IVoucherOrderService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.baomidou.mybatisplus.extension.service.IService; 4 | import com.xydp.dto.Result; 5 | import com.xydp.entity.VoucherOrder; 6 | 7 | /** 8 | *

9 | * 优惠券订单服务类接口 10 | *

11 | * 12 | * @author Wuxy 13 | * @since 2021-12-22 14 | */ 15 | public interface IVoucherOrderService extends IService { 16 | 17 | Result seckillVoucher(Long voucherId); 18 | 19 | void createVoucherOrder(VoucherOrder voucherOrder); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/IVoucherService.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service; 2 | 3 | import com.xydp.dto.Result; 4 | import com.xydp.entity.Voucher; 5 | import com.baomidou.mybatisplus.extension.service.IService; 6 | 7 | /** 8 | *

9 | * 服务类 10 | *

11 | * 12 | * @author 虎哥 13 | * @since 2021-12-22 14 | */ 15 | public interface IVoucherService extends IService { 16 | 17 | Result queryVoucherOfShop(Long shopId); 18 | 19 | void addSeckillVoucher(Voucher voucher); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/BlogCommentsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import com.xydp.entity.BlogComments; 4 | import com.xydp.mapper.BlogCommentsMapper; 5 | import com.xydp.service.IBlogCommentsService; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | *

11 | * 服务实现类 12 | *

13 | * 14 | * @author 虎哥 15 | * @since 2021-12-22 16 | */ 17 | @Service 18 | public class BlogCommentsServiceImpl extends ServiceImpl implements IBlogCommentsService { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/BlogServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import com.xydp.dto.Result; 8 | import com.xydp.dto.ScrollResult; 9 | import com.xydp.dto.UserDTO; 10 | import com.xydp.entity.Blog; 11 | import com.xydp.entity.Follow; 12 | import com.xydp.entity.User; 13 | import com.xydp.mapper.BlogMapper; 14 | import com.xydp.service.IBlogService; 15 | import com.xydp.service.IFollowService; 16 | import com.xydp.service.IUserService; 17 | import com.xydp.utils.SystemConstants; 18 | import com.xydp.utils.UserHolder; 19 | import org.springframework.data.redis.core.StringRedisTemplate; 20 | import org.springframework.data.redis.core.ZSetOperations; 21 | import org.springframework.stereotype.Service; 22 | 23 | import javax.annotation.Resource; 24 | import java.util.*; 25 | import java.util.stream.Collectors; 26 | 27 | import static com.xydp.utils.RedisConstants.BLOG_LIKED_KEY; 28 | import static com.xydp.utils.RedisConstants.FEED_KEY; 29 | 30 | /** 31 | *

32 | * 服务实现类 33 | *

34 | * 35 | * @author 虎哥 36 | * @since 2021-12-22 37 | */ 38 | @Service 39 | public class BlogServiceImpl extends ServiceImpl implements IBlogService { 40 | 41 | @Resource 42 | private IUserService userService; 43 | @Resource 44 | private IFollowService followService; 45 | @Resource 46 | private StringRedisTemplate stringRedisTemplate; 47 | 48 | @Override 49 | public Result queryHotBlog(Integer current) { 50 | // 根据用户查询 51 | Page page = query() 52 | .orderByDesc("liked") 53 | .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); 54 | // 获取当前页数据 55 | List records = page.getRecords(); 56 | // 查询用户 57 | records.forEach(blog -> { 58 | this.queryBlogUser(blog); 59 | this.isBlogLiked(blog); 60 | }); 61 | return Result.ok(records); 62 | } 63 | 64 | @Override 65 | public Result queryBlogById(Long id) { 66 | // 1.查询blog 67 | Blog blog = getById(id); 68 | if (blog == null) { 69 | return Result.fail("笔记不存在!"); 70 | } 71 | // 2.查询blog有关的用户 72 | queryBlogUser(blog); 73 | // 3.查询blog是否被点赞 74 | isBlogLiked(blog); 75 | return Result.ok(blog); 76 | } 77 | 78 | private void isBlogLiked(Blog blog) { 79 | // 1.获取登录用户 80 | UserDTO user = UserHolder.getUser(); 81 | if (user == null) { 82 | // 用户未登录,无需查询是否点赞 83 | return; 84 | } 85 | // 2.判断当前登录用户是否已经点赞 86 | String key = BLOG_LIKED_KEY + blog.getId(); 87 | Double score = stringRedisTemplate.opsForZSet().score(key, user.getId().toString()); 88 | blog.setIsLike(score != null); 89 | } 90 | 91 | @Override 92 | public Result likeBlog(Long id) { 93 | // 1.获取登录用户 94 | Long userId = UserHolder.getUser().getId(); 95 | // 2.判断当前登录用户是否已经点赞 96 | String key = BLOG_LIKED_KEY + id; 97 | Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString()); 98 | if (score == null) { 99 | // 3.如果未点赞,可以点赞 100 | // 3.1 数据库点赞数+1 101 | boolean success = update().setSql("liked = liked + 1").eq("id", id).update(); 102 | // 3.2 保存用户到Redis的set集合 103 | if (success) { 104 | stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis()); 105 | } 106 | } else { 107 | // 4.如果已经点赞,取消点赞 108 | // 4.1 数据库点赞数-1 109 | boolean success = update().setSql("liked = liked - 1").eq("id", id).update(); 110 | // 4.2 把用户从Redis的set集合中移除 111 | if (success) { 112 | stringRedisTemplate.opsForZSet().remove(key, userId.toString()); 113 | } 114 | } 115 | return Result.ok(); 116 | } 117 | 118 | /** 119 | * 查询点赞时间顺序前五的用户 120 | * 121 | * @param id 日志id 122 | * @return Result 123 | */ 124 | @Override 125 | public Result queryBlogLikes(Long id) { 126 | String key = BLOG_LIKED_KEY + id; 127 | // 1.查询top5的点赞用户 zrange key 0 4 128 | Set top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); 129 | if (top5 == null || top5.isEmpty()) { 130 | return Result.ok(Collections.emptyList()); 131 | } 132 | // 2.解析出其中的用户id 133 | List ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); 134 | // 3.根据用户id查询用户 WHERE id IN (5, 1) ORDER BY FIELD(id, 5, 1) 135 | String idsStr = StrUtil.join(",", ids); 136 | List userDTOS = userService 137 | .query() 138 | .in("id", ids) 139 | .last("ORDER BY FIELD(id," + idsStr + ")") 140 | .list() 141 | .stream() 142 | .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) 143 | .collect(Collectors.toList()); 144 | // 4.返回 145 | return Result.ok(userDTOS); 146 | } 147 | 148 | @Override 149 | public Result saveBlog(Blog blog) { 150 | // 1.获取登录用户 151 | UserDTO user = UserHolder.getUser(); 152 | blog.setUserId(user.getId()); 153 | // 2.保存探店博文 154 | boolean success = save(blog); 155 | if (!success) return Result.fail("发布笔记失败!"); 156 | // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?; 157 | List follows = followService.query().eq("follow_user_id", user.getId()).list(); 158 | // 4.推送笔记id给所有粉丝 159 | for (Follow follow : follows) { 160 | // 4.1 获取粉丝id 161 | Long userId = follow.getUserId(); 162 | // 4.2 推送 163 | String key = FEED_KEY + userId; 164 | stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); 165 | } 166 | // 5.返回id 167 | return Result.ok(blog.getId()); 168 | } 169 | 170 | /** 171 | * 查询推送的 Blog 消息 172 | * 173 | * @param max 当前时间戳(上一次查询的最小时间戳,作为这次查询的最大时间) 174 | * @param offset 在上一次结果中,与最小是指一样的元素个数 175 | * @return 推送 Blog 176 | */ 177 | @Override 178 | public Result queryBlogOfFollow(Long max, Integer offset) { 179 | // 1.获取当前用户 180 | Long userId = UserHolder.getUser().getId(); 181 | // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count 182 | String key = FEED_KEY + userId; 183 | Set> typedTuples = stringRedisTemplate 184 | .opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 3); 185 | // 3.非空判断 186 | if (typedTuples == null || typedTuples.size() == 0) { 187 | return Result.ok(); 188 | } 189 | // 4.解析数据:blogId、score(时间戳)、offset 190 | List ids = new ArrayList<>(typedTuples.size()); 191 | long minTime = 0; 192 | int os = 1; // 统计和最小时间相同的个数(下一次的偏移量) 193 | for (ZSetOperations.TypedTuple typedTuple : typedTuples) { 194 | // 4.1 获取id 195 | ids.add(Long.valueOf(typedTuple.getValue())); 196 | // 4.2 获取分数(时间戳) 197 | long time = typedTuple.getScore().longValue(); 198 | if (time == minTime) os++; 199 | else { 200 | minTime = time; 201 | os = 1; // 重置为1 202 | } 203 | } 204 | // 5.根据id查询blog 205 | String idStr = StrUtil.join(",", ids); 206 | List blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); 207 | for (Blog blog : blogs) { 208 | // 5.1 查询blog有关的用户 209 | queryBlogUser(blog); 210 | // 5.2 查询blog是否被点赞 211 | isBlogLiked(blog); 212 | } 213 | 214 | // 6.封装并返回 215 | ScrollResult r = new ScrollResult(); 216 | r.setList(blogs); 217 | r.setOffset(os); 218 | r.setMinTime(minTime); 219 | return Result.ok(r); 220 | } 221 | 222 | private void queryBlogUser(Blog blog) { 223 | Long userId = blog.getUserId(); 224 | User user = userService.getById(userId); 225 | blog.setName(user.getNickName()); 226 | blog.setIcon(user.getIcon()); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/FollowServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 5 | import com.xydp.dto.Result; 6 | import com.xydp.dto.UserDTO; 7 | import com.xydp.entity.Follow; 8 | import com.xydp.mapper.FollowMapper; 9 | import com.xydp.service.IFollowService; 10 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 11 | import com.xydp.service.IUserService; 12 | import com.xydp.utils.UserHolder; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.data.redis.core.StringRedisTemplate; 15 | import org.springframework.stereotype.Service; 16 | 17 | import javax.annotation.Resource; 18 | import java.util.Collections; 19 | import java.util.List; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | 23 | /** 24 | *

25 | * 服务实现类 26 | *

27 | * 28 | * @author 虎哥 29 | * @since 2021-12-22 30 | */ 31 | @Service 32 | public class FollowServiceImpl extends ServiceImpl implements IFollowService { 33 | 34 | @Resource 35 | private StringRedisTemplate stringRedisTemplate; 36 | @Autowired 37 | private IUserService userService; 38 | 39 | @Override 40 | public Result follow(Long followUserId, Boolean isFollow) { 41 | // 获取用户 42 | Long userId = UserHolder.getUser().getId(); 43 | String key = "follows:" + userId; 44 | // 1.判断是关注还是取关 45 | if (isFollow) { 46 | // 2. 关注,新增数据 47 | Follow follow = new Follow(); 48 | follow.setUserId(userId); 49 | follow.setFollowUserId(followUserId); 50 | boolean success = save(follow); 51 | if (success) { 52 | // 把关注用户的id放入redis的set集合 sadd userId followerUserId 53 | stringRedisTemplate.opsForSet().add(key, followUserId.toString()); 54 | } 55 | } else { 56 | // 3.取关,删除 57 | boolean success = remove(new QueryWrapper() 58 | .eq("user_id", userId) 59 | .eq("follow_user_id", followUserId)); 60 | // 把关注的用户id从Redis集合中移除 61 | if (success) 62 | stringRedisTemplate.opsForSet().remove(key); 63 | } 64 | return Result.ok(); 65 | } 66 | 67 | @Override 68 | public Result isFollow(Long followUserId) { 69 | // 1.获取登录用户 70 | Long userId = UserHolder.getUser().getId(); 71 | // 2.查询是否关注 select * from tb_follow where user_id = ? and follow_user_id = ? 72 | Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count(); 73 | // 3.判断 74 | return Result.ok(count > 0); 75 | } 76 | 77 | @Override 78 | public Result followCommons(Long id) { 79 | // 1.获取当前用户 80 | Long userId = UserHolder.getUser().getId(); 81 | String key = "follows:" + userId; 82 | // 2.求交集 83 | String key2 = "follows:" + id; 84 | Set intersect = stringRedisTemplate.opsForSet().intersect(key, key2); 85 | if (intersect == null || intersect.size() == 0) { 86 | // 无交集 87 | return Result.ok(Collections.emptyList()); 88 | } 89 | // 3.解析出id集合 90 | List ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); 91 | // 4.查询用户 92 | List users = userService 93 | .listByIds(ids) 94 | .stream() 95 | .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) 96 | .collect(Collectors.toList()); 97 | return Result.ok(users); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/ITokenServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import com.xydp.service.ITokenService; 4 | import com.xydp.utils.CommonUtils; 5 | import com.xydp.utils.RedisConstants; 6 | import com.xydp.utils.UserHolder; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.data.redis.core.StringRedisTemplate; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * @author Wuxy 15 | * @version 1.0 16 | * @ClassName TokenServiceImpl 17 | * @since 2023/6/3 17:34 18 | */ 19 | @Service 20 | public class ITokenServiceImpl implements ITokenService { 21 | @Autowired 22 | private StringRedisTemplate redisTemplate; 23 | 24 | 25 | @Override 26 | public String getOrderToken() { 27 | // 获取登录用户账号 28 | Long userId = UserHolder.getUser().getId(); 29 | // 随机获取32位的 数字+字母 作为token 30 | String token = CommonUtils.getStringNumRandom(32); 31 | // key的组成 32 | String key = String.format(RedisConstants.SUBMIT_ORDER_TOKEN_KEY, userId, token); 33 | // 设置防重令牌的有效时间是 30 分钟 34 | redisTemplate.opsForValue().set(key, String.valueOf(Thread.currentThread().getId()), 30, TimeUnit.MINUTES); 35 | 36 | return token; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/SeckillVoucherServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import com.xydp.entity.SeckillVoucher; 4 | import com.xydp.mapper.SeckillVoucherMapper; 5 | import com.xydp.service.ISeckillVoucherService; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | *

11 | * 秒杀优惠券表,与优惠券是一对一关系 服务实现类 12 | *

13 | * 14 | * @author 虎哥 15 | * @since 2022-01-04 16 | */ 17 | @Service 18 | public class SeckillVoucherServiceImpl extends ServiceImpl implements ISeckillVoucherService { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/ShopServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import cn.hutool.core.util.BooleanUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import cn.hutool.json.JSONObject; 6 | import cn.hutool.json.JSONUtil; 7 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 8 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 9 | import com.xydp.dto.Result; 10 | import com.xydp.entity.Shop; 11 | import com.xydp.mapper.ShopMapper; 12 | import com.xydp.service.IShopService; 13 | import com.xydp.utils.CacheClient; 14 | import com.xydp.utils.RedisData; 15 | import com.xydp.utils.SystemConstants; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.data.geo.Distance; 18 | import org.springframework.data.geo.GeoResult; 19 | import org.springframework.data.geo.GeoResults; 20 | import org.springframework.data.redis.connection.RedisGeoCommands; 21 | import org.springframework.data.redis.core.StringRedisTemplate; 22 | import org.springframework.data.redis.domain.geo.GeoReference; 23 | import org.springframework.stereotype.Service; 24 | import org.springframework.transaction.annotation.Transactional; 25 | 26 | import java.time.LocalDateTime; 27 | import java.util.ArrayList; 28 | import java.util.HashMap; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | import java.util.concurrent.TimeUnit; 34 | 35 | import static com.xydp.utils.RedisConstants.*; 36 | 37 | /** 38 | *

39 | * 服务实现类 40 | *

41 | * 42 | * @author 虎哥 43 | * @since 2021-12-22 44 | */ 45 | @Service 46 | @SuppressWarnings("Duplicates") 47 | public class ShopServiceImpl extends ServiceImpl implements IShopService { 48 | 49 | @Autowired 50 | private StringRedisTemplate stringRedisTemplate; 51 | @Autowired 52 | private CacheClient cacheClient; 53 | 54 | @Override 55 | public Result queryById(Long id) { 56 | // 缓存穿透 57 | // Shop shop = queryWithPassThrough(id); 58 | // Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES); 59 | 60 | // 互斥锁解决缓存击穿 61 | Shop shop = queryWithMutex(id); 62 | 63 | // 逻辑过期解决缓存冲击 64 | // Shop shop = queryWithLogicalExpire(id); 65 | // Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS); 66 | if (shop == null) { 67 | return Result.fail("店铺不存在!"); 68 | } 69 | // 7.返回 70 | return Result.ok(shop); 71 | } 72 | 73 | private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); 74 | 75 | /** 76 | * 通过id查询店铺信息,使用【逻辑过期】策略 解决【缓存击穿】 77 | * 78 | * @param id 店铺id 79 | * @return 返回查询到的店铺,没有返回null 80 | */ 81 | private Shop queryWithLogicalExpire(Long id) { 82 | // 1.从redis查询商铺缓存 83 | String key = CACHE_SHOP_KEY + id; 84 | String shopJson = stringRedisTemplate.opsForValue().get(key); 85 | // 2.判断是否存在 86 | if (StrUtil.isBlank(shopJson)) { 87 | // 3.不存在,直接放回null 88 | return null; 89 | } 90 | // 4.命中,需要先把json反序列化为对象 91 | RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class); 92 | JSONObject data = (JSONObject) redisData.getData(); 93 | Shop shop = JSONUtil.toBean(data, Shop.class); 94 | LocalDateTime expireTime = redisData.getExpireTime(); 95 | // 5.判断是否过期 96 | if (expireTime.isAfter(LocalDateTime.now())) { 97 | // 5.1 未过期,直接返回店铺信息 98 | return shop; 99 | } 100 | // 6.缓存重建 101 | // 6.1 获取互斥锁 102 | String lockKey = LOCK_SHOP_KEY + id; 103 | boolean isLock = tryLock(lockKey); 104 | // 6.2 判断是否获取锁成功 105 | if (isLock) { 106 | // 6.3 成功,开启独立线程,实现缓存重建 107 | CACHE_REBUILD_EXECUTOR.submit(() -> { 108 | // 重建缓存 109 | try { 110 | this.saveShop2Redis(id, 20L); 111 | } catch (Exception e) { 112 | throw new RuntimeException(e); 113 | } finally { 114 | // 释放锁 115 | this.unlock(lockKey); 116 | } 117 | }); 118 | } 119 | // 6.4 返回过期的商城信息 120 | return shop; 121 | } 122 | 123 | /** 124 | * 通过id查询店铺信息,使用【互斥锁】策略 解决【缓存击穿】 125 | * 126 | * @param id 店铺id 127 | * @return 返回查询到的店铺,没有返回null 128 | */ 129 | private Shop queryWithMutex(Long id) { 130 | // 1.从redis查询商铺缓存 131 | String key = CACHE_SHOP_KEY + id; 132 | String shopJson = stringRedisTemplate.opsForValue().get(key); 133 | // 2.判断是否存在 134 | if (!StrUtil.isBlank(shopJson)) { 135 | // 3.存在,直接放回 136 | return JSONUtil.toBean(shopJson, Shop.class); 137 | } 138 | // 【缓存穿透】判断命中的是否是空值 139 | if (shopJson != null) { // shopJson = "" - 缓存的空值 140 | // 返回一个错误信息 141 | // "店铺信息不存在!" 142 | return null; 143 | } 144 | // 4.实现缓存重建 145 | // 4.1 获取互斥锁 146 | String lockKey = "lock:shop:" + id; 147 | Shop shop; 148 | try { 149 | boolean isLock = tryLock(lockKey); 150 | // 4.2 判断是否获取成功 151 | if (!isLock) { 152 | // 4.3 失败,则休眠并重试 153 | Thread.sleep(50); 154 | return queryWithMutex(id); 155 | } 156 | // 4.4 成功,根据id查询数据库 157 | // 4.不存在 158 | shop = getById(id); 159 | // 5.不存在,返回错误 160 | if (shop == null) { 161 | // 【缓存穿透】将空值写入redis 162 | stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); 163 | // 返回错误信息 164 | // "店铺不存在!" 165 | return null; 166 | } 167 | // 6.存在,写入redis 168 | stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); 169 | } catch (InterruptedException e) { 170 | throw new RuntimeException(e); 171 | } 172 | // 7.释放互斥锁 173 | unlock(lockKey); 174 | // 8.返回 175 | return shop; 176 | } 177 | 178 | /** 179 | * 通过 id 查询店铺,同时解决 【缓存穿透】 问题 180 | * 181 | * @param id 店铺id 182 | * @return 返回查询到的店铺信息,若店铺不存在,返回 null 183 | */ 184 | private Shop queryWithPassThrough(Long id) { 185 | // 1.从redis查询商铺缓存 186 | String key = CACHE_SHOP_KEY + id; 187 | String shopJson = stringRedisTemplate.opsForValue().get(key); 188 | // 2.判断是否存在 189 | if (!StrUtil.isBlank(shopJson)) { 190 | // 3.存在,直接放回 191 | return JSONUtil.toBean(shopJson, Shop.class); 192 | } 193 | // 【缓存穿透】判断命中的是否是空值 194 | if (shopJson != null) { 195 | // 返回一个错误信息 196 | //"店铺信息不存在!" 197 | return null; 198 | } 199 | // 4.不存在,根据id查询数据库 200 | Shop shop = getById(id); 201 | // 5.不存在,返回错误 202 | if (shop == null) { 203 | // 【缓存穿透】将空置写入redis 204 | stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES); 205 | // 返回错误信息 206 | // "店铺不存在!" 207 | return null; 208 | } 209 | // 6.存在,写入redis 210 | stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); 211 | // 7.返回 212 | return shop; 213 | } 214 | 215 | /** 216 | * 获取锁(setnx命令) 217 | * 218 | * @param key redis的key值 219 | * @return 获取成功返回 true,否则返回 false 220 | */ 221 | private boolean tryLock(String key) { 222 | // setIfAbsent == setnx 223 | Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS); 224 | return BooleanUtil.isTrue(flag); 225 | } 226 | 227 | /** 228 | * 释放redis锁(删除对应key的缓存) 229 | * 230 | * @param key 锁的key值 231 | */ 232 | private void unlock(String key) { 233 | stringRedisTemplate.delete(key); 234 | } 235 | 236 | /** 237 | * 热点店铺 238 | *

239 | * 根据id从数据库中查询出商店信息,添加过期时间,封装成 {@link RedisData} 类存入Redis中 240 | * 241 | * @param id 店铺id 242 | * @param expireSeconds 过期时间 243 | */ 244 | public void saveShop2Redis(Long id, Long expireSeconds) { 245 | // 1.查询店铺数据 246 | Shop shop = getById(id); 247 | // 2.封装逻辑过期时间 248 | RedisData redisData = new RedisData(); 249 | redisData.setData(shop); 250 | redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); 251 | // 3.写入Redis 252 | stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData)); 253 | } 254 | 255 | /** 256 | * 更新店铺操作 257 | * 258 | * @param shop 要更新的店铺 259 | * @return 返回更新的结果 260 | */ 261 | @Override 262 | @Transactional // 添加事务注解控制原子性操作 -- 方便出现异常时数据库进行回滚 263 | public Result update(Shop shop) { 264 | Long id = shop.getId(); 265 | if (id == null) { 266 | return Result.fail("店铺id不能为空!"); 267 | } 268 | // 1.更新数据库 269 | updateById(shop); 270 | // 2.删除缓存(当删除失败可能会导致缓存中为旧数据) 271 | stringRedisTemplate.delete(CACHE_SHOP_KEY + id); 272 | return Result.ok(); 273 | } 274 | 275 | @Override 276 | public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) { 277 | // 1.判断是否需要根据坐标查询 278 | if (x == null || y == null) { 279 | // 不需要坐标查询,按照数据库查询 280 | // 根据类型分页查询 281 | Page page = query() 282 | .eq("type_id", typeId) 283 | .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE)); 284 | // 返回数据 285 | return Result.ok(page.getRecords()); 286 | } 287 | // 2.计算分页参数 288 | int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE; 289 | int end = current * SystemConstants.DEFAULT_PAGE_SIZE; 290 | 291 | // 3.查询redis,按照距离排序、分页,结果:shopId、distance 292 | String key = SHOP_GEO_KEY + typeId; 293 | GeoResults> results = stringRedisTemplate 294 | .opsForGeo() // GEOSEARCH key BYLONLAT x y BYRADIUS 10 WITHDISTANCE 295 | .search( 296 | key, 297 | GeoReference.fromCoordinate(x, y), 298 | new Distance(5000.), 299 | RedisGeoCommands 300 | .GeoSearchCommandArgs 301 | .newGeoSearchArgs() 302 | .includeDistance() 303 | .limit(end) 304 | ); 305 | // 4.解析出id 306 | if (results == null) { 307 | return Result.ok(); 308 | } 309 | List>> list = results.getContent(); 310 | if (list.size() <= from) { 311 | // 没有一下页了 312 | return Result.ok(); 313 | } 314 | // 4.1 截取 from ~ end 的部分 315 | List ids = new ArrayList<>(list.size()); 316 | Map distanceMap = new HashMap<>(list.size()); 317 | list.stream().skip(from).forEach(result -> { 318 | // 4.2 获取店铺id 319 | String shopIdStr = result.getContent().getName(); 320 | ids.add(Long.valueOf(shopIdStr)); 321 | // 4.3 获取距离 322 | Distance distance = result.getDistance(); 323 | distanceMap.put(shopIdStr, distance); 324 | }); 325 | // 5.根据id查询shop 326 | String idStr = StrUtil.join(",", ids); 327 | List shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); 328 | for (Shop shop : shops) { 329 | shop.setDistance(distanceMap.get(shop.getId().toString()).getValue()); 330 | } 331 | // 6.返回 332 | return Result.ok(shops); 333 | } 334 | 335 | public Result queryById1(Long id) { 336 | String key = CACHE_SHOP_KEY + id; 337 | // 1.从Redis中查询商铺缓存 338 | String shopJson = stringRedisTemplate.opsForValue().get(key); 339 | // 2.判断是否存在 340 | if (StrUtil.isNotBlank(shopJson)) { 341 | // 3.存在,直接返回 342 | Shop shop = JSONUtil.toBean(shopJson, Shop.class); 343 | return Result.ok(shop); 344 | } 345 | // 4.不存在,根据id查询数据库 346 | Shop shop = getById(id); 347 | // 5.不存在,直接返回 348 | if (shop == null) { 349 | return Result.fail("商铺不存在!"); 350 | } 351 | // 6.存在,写入Redis 352 | stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); 353 | // 7.返回 354 | return Result.ok(shop); 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/ShopTypeServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 5 | import com.xydp.dto.Result; 6 | import com.xydp.entity.ShopType; 7 | import com.xydp.mapper.ShopTypeMapper; 8 | import com.xydp.service.IShopTypeService; 9 | import com.xydp.utils.RedisConstants; 10 | import org.springframework.data.redis.core.StringRedisTemplate; 11 | import org.springframework.stereotype.Service; 12 | 13 | import javax.annotation.Resource; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | *

19 | * 服务实现类 20 | *

21 | * 22 | * @author 虎哥 23 | * @since 2021-12-22 24 | */ 25 | @Service 26 | public class ShopTypeServiceImpl extends ServiceImpl implements IShopTypeService { 27 | @Resource 28 | private StringRedisTemplate stringRedisTemplate; 29 | 30 | @Override 31 | public Result queryTypeList() { 32 | String key = RedisConstants.CACHE_SHOP_TYPE_KEY; 33 | List typesString = stringRedisTemplate.opsForList().range(key, 0, -1); // 以 -1 表示列表的最后一个元素 34 | List types = new ArrayList<>(); 35 | // 判断是否存在 36 | if (typesString != null && typesString.size() > 0) { // 存在 37 | for (String str : typesString) { 38 | types.add(JSONUtil.toBean(str, ShopType.class)); 39 | } 40 | return Result.ok(types); 41 | } 42 | // 不存在,查询数据库 43 | types = query().orderByAsc("sort").list(); 44 | 45 | typesString = new ArrayList<>(); 46 | for (ShopType type : types) { 47 | typesString.add(JSONUtil.toJsonStr(type)); 48 | } 49 | // 写入redis 50 | stringRedisTemplate.opsForList().rightPushAll(key, typesString); 51 | 52 | return Result.ok(types); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/UserInfoServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import com.xydp.entity.UserInfo; 4 | import com.xydp.mapper.UserInfoMapper; 5 | import com.xydp.service.IUserInfoService; 6 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 7 | import org.springframework.stereotype.Service; 8 | 9 | /** 10 | *

11 | * 服务实现类 12 | *

13 | * 14 | * @author 虎哥 15 | * @since 2021-12-24 16 | */ 17 | @Service 18 | public class UserInfoServiceImpl extends ServiceImpl implements IUserInfoService { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import cn.hutool.core.bean.BeanUtil; 4 | import cn.hutool.core.bean.copier.CopyOptions; 5 | import cn.hutool.core.lang.UUID; 6 | import cn.hutool.core.util.RandomUtil; 7 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 8 | import com.xydp.dto.LoginFormDTO; 9 | import com.xydp.dto.Result; 10 | import com.xydp.dto.UserDTO; 11 | import com.xydp.entity.User; 12 | import com.xydp.mapper.UserMapper; 13 | import com.xydp.service.IUserService; 14 | import com.xydp.utils.RegexUtils; 15 | import com.xydp.utils.UserHolder; 16 | import lombok.extern.slf4j.Slf4j; 17 | import org.springframework.data.redis.connection.BitFieldSubCommands; 18 | import org.springframework.data.redis.core.StringRedisTemplate; 19 | import org.springframework.stereotype.Service; 20 | 21 | import javax.annotation.Resource; 22 | import javax.servlet.http.HttpSession; 23 | 24 | import java.time.LocalDateTime; 25 | import java.time.format.DateTimeFormatter; 26 | import java.util.HashMap; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.concurrent.TimeUnit; 30 | 31 | import static com.xydp.utils.RedisConstants.*; 32 | import static com.xydp.utils.SystemConstants.USER_NICK_NAME_PREFIX; 33 | 34 | /** 35 | *

36 | * 服务实现类 37 | *

38 | * 39 | * @author 虎哥 40 | * @since 2021-12-22 41 | */ 42 | @Slf4j 43 | @Service 44 | public class UserServiceImpl extends ServiceImpl implements IUserService { 45 | 46 | @Resource 47 | private StringRedisTemplate stringRedisTemplate; 48 | 49 | @Override 50 | public Result sendCode(String phone, HttpSession session) { 51 | // 1.检验手机号 52 | if (RegexUtils.isPhoneInvalid(phone)) { 53 | // 2.如果不符合,返回错误信息 54 | return Result.fail("手机号格式错误!"); 55 | } 56 | // 3.符合,生成验证码 57 | String code = RandomUtil.randomNumbers(6); 58 | // 4.保存验证码到session 59 | // session.setAttribute("code", code); 60 | // 4.保存验证码到redis 61 | stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); 62 | 63 | // 5.发送验证码 64 | log.debug("发送短信验证码成功,验证码:{}", code); 65 | // 返回ok 66 | return Result.ok(code); 67 | } 68 | 69 | @Override 70 | public Result login(LoginFormDTO loginForm, HttpSession session) { 71 | // 1.校验手机号 72 | String phone = loginForm.getPhone(); 73 | if (RegexUtils.isPhoneInvalid(phone)) { 74 | // 2.如果不符合,返回错误信息 75 | return Result.fail("手机号格式错误!"); 76 | } 77 | // 2.校验验证码 78 | // Object cacheCode = session.getAttribute("code"); 79 | 80 | // 2.从redis获取验证码并校验 81 | String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); 82 | String code = loginForm.getCode(); 83 | if (cacheCode == null || !cacheCode.equals(code)) { 84 | // 3.不一致,报错 85 | return Result.fail("验证码不一致!"); 86 | } 87 | // 4.一致,根据手机号查询用户 88 | User user = query().eq("phone", phone).one(); 89 | // 5.判断用户是否存在 90 | if (user == null) { 91 | // 6.不存在 92 | user = createUserWithPhone(phone); 93 | } 94 | // 7.保存用户信息到session中 95 | // session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class)); 96 | 97 | // 7.保存用户信息到redis中 98 | // 7.1 随机生成token,作为登录令牌 99 | String token = UUID.randomUUID().toString(true); 100 | // 7.2 将User对象转为Hash存储 101 | UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); 102 | Map userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), 103 | CopyOptions 104 | .create() 105 | .setIgnoreNullValue(true) 106 | .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())); 107 | 108 | String tokenKey = LOGIN_USER_KEY + token; 109 | // 7.3 存储 110 | stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); 111 | // 7.4 设置token有效期 112 | stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.SECONDS); 113 | 114 | // 8.返回token 115 | return Result.ok(token); 116 | } 117 | 118 | @Override 119 | public Result sign() { 120 | // 1.获取当前登录的用户 121 | Long userId = UserHolder.getUser().getId(); 122 | // 2.获取日期 123 | LocalDateTime now = LocalDateTime.now(); 124 | // 3.拼接key 125 | String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); 126 | String key = USER_SIGN_KEY + userId + keySuffix; 127 | // 4.获取今天是本月的第几天 128 | int dayOfMonth = now.getDayOfMonth(); 129 | // 5.写入Redis SETBIT key offset 1 130 | stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true); 131 | return Result.ok(); 132 | } 133 | 134 | // 获取连续签到天数 135 | @Override 136 | public Result signCount() { 137 | // 1.获取当前登录用户 138 | Long userId = UserHolder.getUser().getId(); 139 | // 2.获取日期 140 | LocalDateTime now = LocalDateTime.now(); 141 | // 3.拼接key 142 | String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); 143 | String key = USER_SIGN_KEY + userId + keySuffix; 144 | // 4. 获取今天是本月第几天 145 | int dayOfMonth = now.getDayOfMonth(); 146 | // 5.获取本月截止到今天为止的所有的签到记录 147 | List result = stringRedisTemplate.opsForValue().bitField( 148 | key, 149 | BitFieldSubCommands.create() 150 | .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0) 151 | ); 152 | if (result == null || result.isEmpty()) { 153 | return Result.ok(0); 154 | } 155 | Long num = result.get(0); 156 | if (num == null || num == 0) { 157 | return Result.ok(0); 158 | } 159 | int count = 0; 160 | // 6.循环遍历 161 | while (true) { 162 | // 7.位运算遍历 163 | if ((num & 1) == 0) { // 未签到 164 | break; 165 | } else { 166 | count++; 167 | } 168 | num >>>= 1; 169 | } 170 | return Result.ok(count); 171 | } 172 | 173 | private User createUserWithPhone(String phone) { 174 | // 创建用户 175 | User user = new User(); 176 | user.setPhone(phone); 177 | user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); 178 | save(user); 179 | return user; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/VoucherOrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import com.xydp.dto.Result; 5 | import com.xydp.entity.SeckillVoucher; 6 | import com.xydp.entity.VoucherOrder; 7 | import com.xydp.mapper.VoucherOrderMapper; 8 | import com.xydp.mq.MqSender; 9 | import com.xydp.service.ISeckillVoucherService; 10 | import com.xydp.service.IVoucherOrderService; 11 | import com.xydp.utils.RedisIdWorker; 12 | import com.xydp.utils.UserHolder; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.redisson.api.RLock; 15 | import org.redisson.api.RedissonClient; 16 | import org.springframework.aop.framework.AopContext; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.core.io.ClassPathResource; 19 | import org.springframework.data.redis.core.StringRedisTemplate; 20 | import org.springframework.data.redis.core.script.DefaultRedisScript; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.transaction.annotation.Transactional; 23 | 24 | import javax.annotation.PostConstruct; 25 | import javax.annotation.Resource; 26 | import java.time.LocalDateTime; 27 | import java.time.ZoneId; 28 | import java.util.Collections; 29 | import java.util.concurrent.ArrayBlockingQueue; 30 | import java.util.concurrent.BlockingQueue; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | 34 | /** 35 | *

36 | * 优惠券订单服务实现类 37 | *

38 | * 39 | * @author Wuxy 40 | * @since 2023/1/12 41 | */ 42 | @Slf4j 43 | @Service 44 | public class VoucherOrderServiceImpl extends ServiceImpl implements IVoucherOrderService { 45 | @Resource 46 | private ISeckillVoucherService seckillVoucherService; 47 | 48 | @Resource 49 | private RedisIdWorker redisIdWorker; 50 | 51 | @Resource 52 | private RedissonClient redissonClient; 53 | 54 | @Resource 55 | private StringRedisTemplate stringRedisTemplate; 56 | 57 | @Autowired 58 | private MqSender mqSender; 59 | 60 | private IVoucherOrderService proxy; 61 | 62 | private static final DefaultRedisScript SECKILL_SCRIPT; 63 | 64 | static { 65 | SECKILL_SCRIPT = new DefaultRedisScript<>(); 66 | SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua")); 67 | SECKILL_SCRIPT.setResultType(Long.class); 68 | } 69 | 70 | @Deprecated 71 | private final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor(); 72 | 73 | @PostConstruct 74 | private void init() { 75 | // SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler()); 76 | } 77 | 78 | // =========================== lua脚本 + 阻塞队列(改造成消息队列) ================================= 79 | 80 | @Deprecated 81 | private final BlockingQueue orderTasks = new ArrayBlockingQueue<>(1024 * 1024); 82 | 83 | 84 | /** 85 | * 实现秒杀优惠券下单(配合阻塞队列进行异步订单创建),需要提前将优惠券信息导入 Redis 中 86 | *

87 | * 具体过程:使用lua脚本实现 -- 通过阻塞队列实现 异步下单,提高并发效率 88 | *

    89 | *
  1. 先利用Redis完成库存余量、一人一单判断,完成抢单业务
  2. 90 | *
  3. 再将下单业务放入阻塞队列,利用独立线程异步下单
  4. 91 | *
92 | *

93 | */ 94 | @Override 95 | public Result seckillVoucher(Long voucherId) { 96 | Long userId = UserHolder.getUser().getId(); 97 | // 1.执行lua脚本 98 | Long result = stringRedisTemplate.execute( 99 | SECKILL_SCRIPT, 100 | Collections.emptyList(), 101 | voucherId.toString(), 102 | userId.toString(), 103 | Long.toString(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()) 104 | ); 105 | int r = result == null ? 0 : result.intValue(); 106 | // 2.判断结果是否为0 107 | if (r != 0) { 108 | // 2.1 不为0,代表没有购买资格 109 | String errorMsg = r == 1 ? "秒杀尚未开始!" : r == 2 ? "秒杀已经结束!" : r == 3 ? "库存不足!" : "不能重复下单!"; 110 | log.warn(errorMsg); 111 | return Result.fail(errorMsg); 112 | } 113 | // 2.2 为0,有购买资格,把下单信息保存到阻塞队列中 114 | // 2.3 订单id 115 | long orderId = redisIdWorker.nextId("order"); 116 | VoucherOrder voucherOrder = new VoucherOrder(); 117 | voucherOrder.setId(orderId); 118 | // 2.4 用户id 119 | voucherOrder.setUserId(userId); 120 | // 2.5 代金券id 121 | voucherOrder.setVoucherId(voucherId); 122 | 123 | //// // 保存阻塞队列 -- 通过线程池 异步执行 数据库读写任务 124 | // orderTasks.add(voucherOrder); 125 | //// // 获取当前类代理对象并赋值 126 | // proxy = (IVoucherOrderService) AopContext.currentProxy(); 127 | 128 | // 发送到消息队列 129 | mqSender.sendSeckillMessage(voucherOrder, false); 130 | 131 | // 3.返回订单id 132 | return Result.ok(orderId); 133 | } 134 | 135 | @Deprecated 136 | private class VoucherOrderHandler implements Runnable { 137 | 138 | @Override 139 | public void run() { 140 | while (true) { 141 | try { 142 | // 1.获取队列中的订单信息 143 | VoucherOrder voucherOrder = orderTasks.take(); 144 | // 2.创建订单 145 | handleVoucherOrder(voucherOrder); 146 | } catch (Exception e) { 147 | log.error("处理订单异常", e); 148 | } 149 | } 150 | } 151 | } 152 | 153 | @Deprecated 154 | private void handleVoucherOrder(VoucherOrder voucherOrder) { 155 | // 1.获取用户 156 | Long userId = voucherOrder.getUserId(); 157 | // 2.创建锁对象 158 | RLock redisLock = redissonClient.getLock("lock:order:" + userId); 159 | // 3.获取锁 160 | boolean isLock = redisLock.tryLock(); 161 | // 4.判断获取锁是否成功 162 | if (!isLock) { 163 | // 获取锁失败,直接放回失败或者重试 164 | log.error("不允许重复下单!"); 165 | return; 166 | } 167 | try { 168 | proxy.createVoucherOrder(voucherOrder); 169 | } finally { 170 | // 释放锁 171 | redisLock.unlock(); 172 | } 173 | } 174 | 175 | @Transactional 176 | @Override 177 | public void createVoucherOrder(VoucherOrder voucherOrder) { 178 | Long userId = voucherOrder.getUserId(); 179 | Long voucherId = voucherOrder.getVoucherId(); 180 | // 创建锁对象 181 | RLock redisLock = redissonClient.getLock("order" + userId); // name 不应该和其他键同名 182 | boolean isLock = redisLock.tryLock(); 183 | if (!isLock) { 184 | // 获取锁失败,直接放回失败或者重试 185 | log.error("不允许重复下单!"); 186 | return; 187 | } 188 | try { 189 | // 5.1 查询订单 190 | Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); 191 | // 5.2 判断是否存在 192 | if (count > 0) { 193 | // 用户已经购买过了 194 | log.error("该用户已经购买过一次!"); 195 | return; 196 | } 197 | // 6.扣减库存 198 | boolean success = seckillVoucherService.update() 199 | .setSql("stock = stock - 1") 200 | .eq("voucher_id", voucherId) 201 | // .eq("stock", voucher.getStock()) // CAS法实现乐观锁,解决并发问题(失败率高) 202 | .gt("stock", 0) // CAS优化,只要库存大于0就可以秒杀成功 203 | .update(); 204 | 205 | if (!success) { 206 | // 扣减失败 207 | log.error("库存不足!"); 208 | return; 209 | } 210 | // 保存订单 211 | save(voucherOrder); 212 | } finally { 213 | // 释放锁 214 | redisLock.unlock(); 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/service/impl/VoucherServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xydp.service.impl; 2 | 3 | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 4 | import com.xydp.dto.Result; 5 | import com.xydp.entity.Voucher; 6 | import com.xydp.mapper.VoucherMapper; 7 | import com.xydp.entity.SeckillVoucher; 8 | import com.xydp.service.ISeckillVoucherService; 9 | import com.xydp.service.IVoucherService; 10 | import com.xydp.utils.RedisConstants; 11 | import org.springframework.data.redis.core.StringRedisTemplate; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import javax.annotation.Resource; 16 | import java.time.LocalDateTime; 17 | import java.time.ZoneId; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | /** 23 | *

24 | * 服务实现类 25 | *

26 | * 27 | * @author 虎哥 28 | * @since 2021-12-22 29 | */ 30 | @Service 31 | public class VoucherServiceImpl extends ServiceImpl implements IVoucherService { 32 | 33 | @Resource 34 | private ISeckillVoucherService seckillVoucherService; 35 | @Resource 36 | private StringRedisTemplate stringRedisTemplate; 37 | 38 | @Override 39 | public Result queryVoucherOfShop(Long shopId) { 40 | // 查询优惠券信息 41 | List vouchers = getBaseMapper().queryVoucherOfShop(shopId); 42 | // 返回结果 43 | return Result.ok(vouchers); 44 | } 45 | 46 | @Override 47 | @Transactional 48 | public void addSeckillVoucher(Voucher voucher) { 49 | // 保存优惠券 50 | save(voucher); 51 | // 保存秒杀信息 52 | SeckillVoucher seckillVoucher = new SeckillVoucher(); 53 | seckillVoucher.setVoucherId(voucher.getId()); 54 | seckillVoucher.setStock(voucher.getStock()); 55 | seckillVoucher.setBeginTime(voucher.getBeginTime()); 56 | seckillVoucher.setEndTime(voucher.getEndTime()); 57 | seckillVoucherService.save(seckillVoucher); 58 | // 保存库存到redis中 59 | // stringRedisTemplate.opsForValue().set(RedisConstants.SECKILL_STOCK_KEY + voucher.getId(), String.valueOf(voucher.getStock())); 60 | Map map = new HashMap<>(); 61 | if (seckillVoucher.getBeginTime() == null) { 62 | seckillVoucher.setBeginTime(LocalDateTime.now()); 63 | } 64 | if (seckillVoucher.getEndTime() == null) { 65 | seckillVoucher.setEndTime(LocalDateTime.now()); 66 | } 67 | map.put("stock", Integer.toString(seckillVoucher.getStock())); 68 | map.put("begin", Long.toString(seckillVoucher.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())); 69 | map.put("end", Long.toString(seckillVoucher.getEndTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli())); 70 | stringRedisTemplate.opsForHash().putAll(RedisConstants.SECKILL + voucher.getId(), map); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/CacheClient.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import cn.hutool.core.util.BooleanUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import cn.hutool.json.JSONObject; 6 | import cn.hutool.json.JSONUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.data.redis.core.StringRedisTemplate; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.time.LocalDateTime; 12 | import java.util.concurrent.ExecutorService; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.function.Function; 16 | 17 | import static com.xydp.utils.RedisConstants.*; 18 | 19 | /** 20 | * 缓存工具封装类 21 | */ 22 | @Slf4j 23 | @Component 24 | @SuppressWarnings("Duplicates") 25 | public class CacheClient { 26 | private final StringRedisTemplate stringRedisTemplate; 27 | 28 | public CacheClient(StringRedisTemplate stringRedisTemplate) { 29 | this.stringRedisTemplate = stringRedisTemplate; 30 | } 31 | 32 | public void set(String key, Object value, Long timeout, TimeUnit unit) { 33 | stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit); 34 | } 35 | 36 | public void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) { 37 | // 设置逻辑过期 38 | RedisData redisData = new RedisData(); 39 | redisData.setData(value); 40 | redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(timeout))); 41 | // 写入Redis缓存 42 | stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData)); 43 | } 44 | 45 | /** 46 | * 通过 id 查询实体,使用 【返回空对象】 解决 【缓存穿透】 问题 47 | * 48 | * @param id 实体Bean id 49 | * @return 返回查询到的 实体Bean 信息,若实体不存在,返回 null 50 | */ 51 | public R queryWithPassThrough( 52 | String keyPrefix, ID id, Class type, Function dbFallback, Long timeout, TimeUnit unit) { 53 | // 1.从redis查询商铺缓存 54 | String key = keyPrefix + id; 55 | String json = stringRedisTemplate.opsForValue().get(key); 56 | // 2.判断是否存在 57 | if (!StrUtil.isBlank(json)) { 58 | // 3.存在,直接放回 59 | return JSONUtil.toBean(json, type); 60 | } 61 | // 【缓存穿透】判断命中的是否是空值 62 | if (json != null) { 63 | // 返回一个错误信息 64 | return null; 65 | } 66 | // 4.不存在,根据id查询数据库 67 | R r = dbFallback.apply(id); 68 | // 5.不存在,返回错误 69 | if (r == null) { 70 | // 【缓存穿透】将空置写入redis 71 | stringRedisTemplate.opsForValue().set(key, "", timeout, unit); 72 | // 返回错误信息 73 | // "店铺不存在!" 74 | return null; 75 | } 76 | // 6.存在,写入redis 77 | this.set(key, r, timeout, unit); 78 | // 7.返回 79 | return r; 80 | } 81 | 82 | private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); 83 | 84 | /** 85 | * 通过id查询实体 Bean 信息,使用【逻辑过期】策略 解决【缓存击穿】 86 | *

87 | * 使用逻辑过期策略一般需要提前将热点 Key 导入数据库中 88 | *

89 | * @param id 实体Bean id 90 | * @param 返回的 实体Bean 类型 91 | * @param 实体Bean ID 类型 92 | * @return 返回查询到的 实体Bean,没有返回null 93 | */ 94 | public R queryWithLogicalExpire( 95 | String keyPrefix, ID id, Class type, Function dbFallback, Long timeout, TimeUnit unit) { 96 | // 1.从redis查询商铺缓存 97 | String key = keyPrefix + id; 98 | String json = stringRedisTemplate.opsForValue().get(key); 99 | // 2.判断是否存在 100 | if (StrUtil.isBlank(json)) { 101 | // 3.不存在,直接放回null 102 | return null; 103 | } 104 | // 4.命中,需要先把json反序列化为对象 105 | RedisData redisData = JSONUtil.toBean(json, RedisData.class); 106 | JSONObject data = (JSONObject) redisData.getData(); 107 | R r = JSONUtil.toBean(data, type); 108 | LocalDateTime expireTime = redisData.getExpireTime(); 109 | // 5.判断是否过期 110 | if (expireTime.isAfter(LocalDateTime.now())) { 111 | // 5.1 未过期,直接放回店铺信息 112 | return r; 113 | } 114 | // 6.缓存重建 115 | // 6.1 获取互斥锁 116 | String lockKey = LOCK_SHOP_KEY + id; 117 | boolean isLock = tryLock(lockKey); 118 | // 6.2 判断是否获取锁成功 119 | if (isLock) { 120 | // 6.3 成功,开启独立线程,实现缓存重建 121 | CACHE_REBUILD_EXECUTOR.submit(() -> { 122 | try { 123 | // 重建缓存 124 | R r1 = dbFallback.apply(id); 125 | // 写入Redis 126 | this.setWithLogicalExpire(key, r1, timeout, unit); 127 | } catch (Exception e) { 128 | throw new RuntimeException(e); 129 | } finally { 130 | // 释放锁 131 | this.unlock(lockKey); 132 | } 133 | }); 134 | } 135 | // 6.4 返回过期的商城信息 136 | return r; 137 | } 138 | 139 | private boolean tryLock(String key) { 140 | // setIfAbsent == setnx 141 | Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS); 142 | return BooleanUtil.isTrue(flag); 143 | } 144 | 145 | private void unlock(String key) { 146 | stringRedisTemplate.delete(key); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/CommonUtils.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import java.util.Random; 4 | 5 | /** 6 | * 通用工具类 7 | * 8 | * @author Wuxy 9 | * @version 1.0 10 | * @ClassName CommonUtils 11 | * @since 2023/6/3 17:27 12 | */ 13 | public class CommonUtils { 14 | private static final String ALL_CHAR_NUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 15 | 16 | 17 | /** 18 | * 获取指定长度的随机串 19 | * 20 | * @param length 指定长度 21 | * @return 返回指定长度的随机字符串 22 | */ 23 | public static String getStringNumRandom(int length) { 24 | // 生成随机数字和字母 25 | Random random = new Random(); 26 | StringBuilder sb = new StringBuilder(length); 27 | for (int i = 0; i < length; i++) { 28 | sb.append(ALL_CHAR_NUM.charAt(random.nextInt(ALL_CHAR_NUM.length()))); 29 | } 30 | return sb.toString(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/ILock.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | public interface ILock { 4 | 5 | /** 6 | * 尝试获取锁 7 | * 8 | * @param timeoutSec 锁持有的超时时间,过期后自动释放 9 | * @return 获取锁成功返回true,否则返回false 10 | */ 11 | boolean tryLock(long timeoutSec); 12 | 13 | /** 14 | * 释放锁 15 | */ 16 | void unlock(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/IPUtils.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import java.net.InetAddress; 5 | import java.net.UnknownHostException; 6 | 7 | /** 8 | * IP 工具类 9 | * 10 | * @author Wuxy 11 | * @version 1.0 12 | * @ClassName IPUtils 13 | * @since 2023/1/13 17:17 14 | **/ 15 | public class IPUtils { 16 | 17 | /** 18 | * 获取 HttpRequest 的请求 IP 19 | * 20 | * @param request http request 21 | * @return 返回真实 IP 22 | */ 23 | public static String getIpAddr(HttpServletRequest request) { 24 | String ipAddress; 25 | try { 26 | ipAddress = request.getHeader("x-forwarded-for"); 27 | if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { 28 | ipAddress = request.getHeader("Proxy-Client-IP"); 29 | } 30 | if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { 31 | ipAddress = request.getHeader("WL-Proxy-Client-IP"); 32 | } 33 | if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { 34 | ipAddress = request.getRemoteAddr(); // localhost会被解析为 ipv6:0:0:0:0:0:0:0:1 35 | if (ipAddress.equals("127.0.0.1")) { 36 | // 根据网卡取本机配置的IP 37 | InetAddress inet = null; 38 | try { 39 | inet = InetAddress.getLocalHost(); 40 | } catch (UnknownHostException e) { 41 | e.printStackTrace(); 42 | } 43 | assert inet != null; 44 | ipAddress = inet.getHostAddress(); 45 | } 46 | } 47 | // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割 48 | if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length() 49 | // = 15 50 | if (ipAddress.indexOf(",") > 0) { 51 | ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); 52 | } 53 | } 54 | } catch (Exception e) { 55 | ipAddress = request.getRemoteAddr(); 56 | } 57 | return ipAddress; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/MQConstants.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | /** 4 | * MQ 常量类 5 | * 6 | * @author Wuxy 7 | * @version 1.0 8 | * @ClassName MQConstants 9 | * @since 2023/1/13 23:42 10 | */ 11 | public class MQConstants { 12 | 13 | public static final String QUEUE = "queue"; 14 | 15 | public static final String SECKILL_QUEUE = "seckill.queue"; 16 | 17 | public static final String SECKILL_EXCHANGE = "seckill_exchange"; 18 | 19 | public static final String SECKILL_ROUTING_KEY = "seckill_routing_key"; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/PasswordEncoder.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import cn.hutool.core.util.RandomUtil; 4 | import org.springframework.util.DigestUtils; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | public class PasswordEncoder { 9 | 10 | public static String encode(String password) { 11 | // 生成盐 12 | String salt = RandomUtil.randomString(20); 13 | // 加密 14 | return encode(password,salt); 15 | } 16 | private static String encode(String password, String salt) { 17 | // 加密 18 | return salt + "@" + DigestUtils.md5DigestAsHex((password + salt).getBytes(StandardCharsets.UTF_8)); 19 | } 20 | public static Boolean matches(String encodedPassword, String rawPassword) { 21 | if (encodedPassword == null || rawPassword == null) { 22 | return false; 23 | } 24 | if(!encodedPassword.contains("@")){ 25 | throw new RuntimeException("密码格式不正确!"); 26 | } 27 | String[] arr = encodedPassword.split("@"); 28 | // 获取盐 29 | String salt = arr[0]; 30 | // 比较 31 | return encodedPassword.equals(encode(rawPassword, salt)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/RedisConstants.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | public class RedisConstants { 4 | public static final String LOGIN_CODE_KEY = "login:code:"; 5 | public static final Long LOGIN_CODE_TTL = 2L; 6 | public static final String LOGIN_USER_KEY = "login:token:"; 7 | public static final Long LOGIN_USER_TTL = 36000L; // 36000L 8 | public static final Long CACHE_NULL_TTL = 2L; 9 | public static final Long CACHE_SHOP_TTL = 30L; 10 | public static final String CACHE_SHOP_KEY = "cache:shop:"; 11 | public static final String CACHE_SHOP_TYPE_KEY = "cache:shoptype"; 12 | public static final String LOCK_SHOP_KEY = "lock:shop:"; 13 | public static final Long LOCK_SHOP_TTL = 10L; 14 | public static final String SECKILL_STOCK_KEY = "seckill:stock:"; 15 | public static final String SECKILL = "seckill:"; 16 | public static final String BLOG_LIKED_KEY = "blog:liked:"; 17 | public static final String FEED_KEY = "feed:"; 18 | public static final String SHOP_GEO_KEY = "shop:geo:"; 19 | public static final String USER_SIGN_KEY = "sign:"; 20 | public static final String ACCESS_LIMIT_IP_KEY = "access:limit:ip:"; 21 | public static final String ACCESS_LIMIT_USER_KEY = "access:limit:user:"; 22 | 23 | /** 24 | * 提交订单令牌的缓存 key 25 | */ 26 | public static final String SUBMIT_ORDER_TOKEN_KEY = "order:submit:%s:%s"; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/RedisData.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.LocalDateTime; 6 | 7 | @Data 8 | public class RedisData { 9 | /** 10 | * 过期时间 11 | */ 12 | private LocalDateTime expireTime; 13 | /** 14 | * Shop 对象数据 15 | */ 16 | private Object data; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/RedisIdWorker.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.redis.core.StringRedisTemplate; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.time.LocalDateTime; 8 | import java.time.ZoneOffset; 9 | import java.time.format.DateTimeFormatter; 10 | 11 | /** 12 | * 基于Redis的订单id生成工具类 13 | */ 14 | @Component 15 | public class RedisIdWorker { 16 | 17 | /** 18 | * 开始时间戳 19 | */ 20 | private static final long START_TIMESTAMP = 1640995200L; 21 | 22 | /** 23 | * 序列号位数 24 | */ 25 | private static final int COUNT_BITS = 32; 26 | 27 | @Autowired 28 | private StringRedisTemplate stringRedisTemplate; 29 | 30 | public long nextId(String keyPrefix) { 31 | // 1.生成时间戳 32 | LocalDateTime now = LocalDateTime.now(); 33 | long nowSecond = now.toEpochSecond(ZoneOffset.UTC); 34 | long timestamp = nowSecond - START_TIMESTAMP; 35 | // 2.生成序列号 36 | // 2.1 获取当前日期,精确到天 37 | String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd")); 38 | // 2.2 自增长 39 | long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date); 40 | // 3.拼接并返回 41 | return timestamp << COUNT_BITS | count; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/RegexPatterns.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | /** 4 | * @author 虎哥 5 | */ 6 | public abstract class RegexPatterns { 7 | /** 8 | * 手机号正则 9 | */ 10 | public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$"; 11 | /** 12 | * 邮箱正则 13 | */ 14 | public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$"; 15 | /** 16 | * 密码正则。4~32位的字母、数字、下划线 17 | */ 18 | public static final String PASSWORD_REGEX = "^\\w{4,32}$"; 19 | /** 20 | * 验证码正则, 6位数字或字母 21 | */ 22 | public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$"; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/RegexUtils.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | /** 6 | * @author 虎哥 7 | */ 8 | public class RegexUtils { 9 | /** 10 | * 是否是无效手机格式 11 | * 12 | * @param phone 要校验的手机号 13 | * @return true:符合,false:不符合 14 | */ 15 | public static boolean isPhoneInvalid(String phone) { 16 | return mismatch(phone, RegexPatterns.PHONE_REGEX); 17 | } 18 | 19 | /** 20 | * 是否是无效邮箱格式 21 | * 22 | * @param email 要校验的邮箱 23 | * @return true:符合,false:不符合 24 | */ 25 | public static boolean isEmailInvalid(String email) { 26 | return mismatch(email, RegexPatterns.EMAIL_REGEX); 27 | } 28 | 29 | /** 30 | * 是否是无效验证码格式 31 | * 32 | * @param code 要校验的验证码 33 | * @return true:符合,false:不符合 34 | */ 35 | public static boolean isCodeInvalid(String code) { 36 | return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX); 37 | } 38 | 39 | // 校验是否不符合正则格式 40 | private static boolean mismatch(String str, String regex) { 41 | if (StrUtil.isBlank(str)) { 42 | return true; 43 | } 44 | return !str.matches(regex); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/SimpleRedisLock.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import cn.hutool.core.lang.UUID; 4 | import org.springframework.core.io.ClassPathResource; 5 | import org.springframework.data.redis.core.StringRedisTemplate; 6 | import org.springframework.data.redis.core.script.DefaultRedisScript; 7 | 8 | import java.util.Collections; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * 基于Redis的分布式锁 13 | */ 14 | public class SimpleRedisLock implements ILock { 15 | private final String name; 16 | 17 | private final StringRedisTemplate stringRedisTemplate; 18 | 19 | private static final String KEY_PREFIX = "lock:"; 20 | 21 | /** 22 | * 线程id前缀,确保不同的JVM的线程一定存在不一样的线程id 23 | */ 24 | private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-"; 25 | 26 | private static final DefaultRedisScript UNLOCK_SCRIPT; 27 | 28 | static { 29 | UNLOCK_SCRIPT = new DefaultRedisScript<>(); 30 | UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua")); 31 | UNLOCK_SCRIPT.setResultType(Long.class); 32 | } 33 | 34 | 35 | public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) { 36 | this.name = name; 37 | this.stringRedisTemplate = stringRedisTemplate; 38 | } 39 | 40 | @Override 41 | public boolean tryLock(long timeoutSec) { 42 | // 获取线程标识 43 | String threadId = ID_PREFIX + Thread.currentThread().getId(); 44 | // 获取锁:set lock thread1 nx ex t -- (nx是互斥、ex是超时时间) 45 | Boolean success = stringRedisTemplate.opsForValue() 46 | .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); 47 | return Boolean.TRUE.equals(success); 48 | } 49 | 50 | /** 51 | * 释放锁,并且确保当前id只能释放自己获取的锁,从而避免并发情况下,由于阻塞等异常情况,导致错误释放其它线程的锁 52 | */ 53 | @Override 54 | public void unlock() { 55 | // 调用lua脚本,使得释放锁的两个操作(1、获取线程标识;2、释放锁)成为原子操作,避免高并发异常情况 56 | stringRedisTemplate.execute( 57 | UNLOCK_SCRIPT, 58 | Collections.singletonList(KEY_PREFIX + name), 59 | ID_PREFIX + Thread.currentThread().getId()); 60 | } 61 | 62 | /*@Override 63 | public void unlock() { 64 | // 获取线程标识 65 | String threadId = ID_PREFIX + Thread.currentThread().getId(); 66 | // 获取锁中的标识 67 | String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name); 68 | // 判断标识是否一致 69 | if (threadId.equals(id)) { 70 | // 释放锁 71 | stringRedisTemplate.delete(KEY_PREFIX + name); 72 | } 73 | }*/ 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/SystemConstants.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | public class SystemConstants { 4 | public static final String IMAGE_UPLOAD_DIR = "D:\\lessons\\Redis\\primary\\nginx-1.18.0\\html\\hmdp\\imgs\\"; 5 | public static final String USER_NICK_NAME_PREFIX = "user_"; 6 | public static final int DEFAULT_PAGE_SIZE = 5; 7 | public static final int MAX_PAGE_SIZE = 10; 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/xydp/utils/UserHolder.java: -------------------------------------------------------------------------------- 1 | package com.xydp.utils; 2 | 3 | import com.xydp.dto.UserDTO; 4 | 5 | public class UserHolder { 6 | private static final ThreadLocal tl = new ThreadLocal<>(); 7 | 8 | public static void saveUser(UserDTO user){ 9 | tl.set(user); 10 | } 11 | 12 | public static UserDTO getUser(){ 13 | return tl.get(); 14 | } 15 | 16 | public static void removeUser(){ 17 | tl.remove(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | spring: 4 | application: 5 | name: hmdp 6 | datasource: 7 | driver-class-name: com.mysql.jdbc.Driver 8 | url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true 9 | username: root 10 | password: 123456 11 | redis: 12 | host: 127.0.0.1 13 | port: 6379 14 | password: # redis没有设置密码时,此参数不需要设置 15 | lettuce: 16 | pool: 17 | max-active: 10 18 | max-idle: 10 19 | min-idle: 1 20 | time-between-eviction-runs: 10s 21 | jackson: 22 | default-property-inclusion: non_null # JSON处理时忽略非空字段 23 | # RabbitMQ配置 24 | rabbitmq: 25 | host: 127.0.0.1 26 | port: 5672 27 | username: admin 28 | password: admin 29 | virtual-host: / 30 | publisher-confirm-type: correlated # 确认消息已发送到交换机(交互类型) 31 | publisher-returns: true 32 | template: 33 | mandatory: true #设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除 34 | # 消费者配置 35 | listener: 36 | direct: 37 | auto-startup: true 38 | retry: 39 | enabled: true 40 | max-attempts: 3 41 | simple: 42 | acknowledge-mode: manual # 设置消费者为手动确认模式 43 | mybatis-plus: 44 | type-aliases-package: com.xydp.entity # 别名扫描包 45 | logging: 46 | level: 47 | com.xydp: debug -------------------------------------------------------------------------------- /src/main/resources/mapper/VoucherMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/reentrantTryLock.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by Wuxy. 4 | --- DateTime: 2022/5/12 21:18 5 | --- 获取锁的Lua脚本 6 | local key = KEYS[1]; -- 锁的key 7 | local threadId = ARGV[1]; -- 线程唯一标识 8 | local releaseTime = ARGV[2]; -- 锁的自动释放时间 9 | -- 判断是否存在 10 | if (redis.call('exists', key) == 0) then 11 | -- 不存在,获取锁 12 | redis.call('hset', key, threadId, '1'); 13 | -- 设置有效期 14 | redis.call('expire', key, releaseTime); 15 | return 1; -- 返回结果 16 | end; 17 | -- 锁已经存在,判断threadId是否是自己 18 | if (redis.call('hexists', key, threadId) == 1) then 19 | -- 不存在,获取锁,重入次数+1 20 | redis.call('hincrby', key, threadId, '1'); 21 | -- 设置有效期 22 | redis.call('expire', key, releaseTime); 23 | return 1; -- 返回结果 24 | end; 25 | return 0; --- 代码走到这里,说明获取的锁不是自己,获取锁失败 26 | -------------------------------------------------------------------------------- /src/main/resources/reentrantUnlock.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by Wuxy. 4 | --- DateTime: 2022/5/12 21:25 5 | --- 6 | local key = KEYS[1]; -- 锁的key 7 | local threadId = ARGV[1]; -- 线程唯一标识 8 | local releaseTime = ARGV[2]; -- 锁的自动释放时间 9 | -- 判断当前锁是否还是被自己持有 10 | if (redis.call('HEXISTS', key, threadId) == 0) then 11 | return nil; -- 如果已经不是自己,则直接放回 12 | end; 13 | -- 是自己的锁,则重入次数-1 14 | local count = redis.call('HINCBY', key, threadId, -1); 15 | -- 判断是否冲入次数是否已经为0 16 | if (count > 0) then 17 | -- 大于0说明不能释放锁,重置有效期然后放回 18 | redis.call('EXPIRE', key, releaseTime); 19 | return nil; 20 | else -- 等于0说明可以释放锁,直接删除 21 | redis.call('DEL', key); 22 | return nil; 23 | end; -------------------------------------------------------------------------------- /src/main/resources/seckill.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by Wuxy. 4 | --- DateTime: 2022/5/12 23:59 5 | --- 6 | -- 1.参数列表 7 | -- 1.1 优惠券id 8 | local voucherId = ARGV[1] 9 | -- 1.2 用户id 10 | local userId = ARGV[2] 11 | -- 1.3 当前时间戳 12 | local timestamp = ARGV[3] 13 | 14 | -- 2.数据key 15 | -- 2.1 库存key 16 | local stockKey = 'seckill:' .. voucherId 17 | -- 2.2 订单key 18 | local orderKey = 'seckill:order:' .. voucherId 19 | 20 | -- 3.脚本业务 21 | -- 3.1 判断当前时间戳是否大于开始时间 22 | if (redis.call('hget', 'seckill:' .. voucherId, 'begin') > timestamp) then 23 | -- 3.2 秒杀还未开始 24 | return 1; 25 | end; 26 | if (redis.call('hget', 'seckill:' .. voucherId, 'end') < timestamp) then 27 | -- 3.3 秒杀已经结束 28 | return 2; 29 | end; 30 | -- 3.4 判断库存是否充足 hget stockKey stock 31 | if (tonumber(redis.call('hget', stockKey, 'stock')) <= 0) then 32 | -- 3.5 库存不足,放回1 33 | return 3; 34 | end; 35 | -- 3.6 判断用户是否下单 SISMEMBER orderKey userId 36 | if (redis.call('sismember', orderKey, userId) == 1) then 37 | -- 3.7 存在,说明是重复下单,返回2 38 | return 4; 39 | end; 40 | -- 3.4 扣库存 incrby stockKey -1 41 | redis.call('hincrby', 'seckill:' .. voucherId, 'stock', -1); 42 | -- 3.5 下单(保存用户)sadd orderKey userId 43 | redis.call('sadd', orderKey, userId); 44 | -- todo: 3.6 发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ... 45 | --redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId); 46 | return 0; -- 成功,返回0 47 | -------------------------------------------------------------------------------- /src/main/resources/seckillBackup.lua: -------------------------------------------------------------------------------- 1 | --- 2 | --- Generated by EmmyLua(https://github.com/EmmyLua) 3 | --- Created by Wuxy. 4 | --- DateTime: 2022/5/12 23:59 5 | --- 6 | -- 1.参数列表 7 | -- 1.1 优惠券id 8 | local voucherId = ARGV[1] 9 | -- 1.2 用户id 10 | local userId = ARGV[2] 11 | -- 1.3 订单id 12 | local orderId = ARGV[4] 13 | 14 | -- 2.数据key 15 | -- 2.1 库存key 16 | local stockKey = 'seckill:stock:' .. voucherId 17 | -- 2.2 订单key 18 | local orderKey = 'seckill:order:' .. voucherId 19 | 20 | -- 3.脚本业务 21 | -- 3.1 判断库存是否充足 get stockKey 22 | if (tonumber(redis.call('get', stockKey)) <= 0) then 23 | -- 3.2 库存不足,放回1 24 | return 1; 25 | end; 26 | -- 3.2 判断用户是否下单 SISMEMBER orderKey userId 27 | if (redis.call('sismember', orderKey, userId) == 1) then 28 | -- 3.3 存在,说明是重复下单,返回2 29 | return 2; 30 | end; 31 | -- 3.4 扣库存 incrby stockKey -1 32 | redis.call('incrby', stockKey, -1); 33 | -- 3.5 下单(保存用户)sadd orderKey userId 34 | redis.call('sadd', orderKey, userId); 35 | -- todo: 3.6 发送消息到队列中, XADD stream.orders * k1 v1 k2 v2 ... 36 | --redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId); 37 | return 0; -- 成功,返回0 -------------------------------------------------------------------------------- /src/main/resources/unlock.lua: -------------------------------------------------------------------------------- 1 | -- 获取锁中的线程标识 get key 2 | local id = redis.call('get', KEYS[1]) 3 | -- 比较线程标识与锁中的标识是否一致 4 | if (id == ARGV[1]) then 5 | -- 释放锁 del key 6 | return redis.call('del', KEYS[1]) 7 | end 8 | return 0 -------------------------------------------------------------------------------- /src/test/java/com/xydp/HmDianPingApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.xydp; 2 | 3 | import com.xydp.entity.Shop; 4 | import com.xydp.service.impl.ShopServiceImpl; 5 | import com.xydp.utils.CacheClient; 6 | import com.xydp.utils.RedisIdWorker; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.data.geo.Point; 11 | import org.springframework.data.redis.connection.RedisGeoCommands; 12 | import org.springframework.data.redis.core.StringRedisTemplate; 13 | 14 | import javax.annotation.Resource; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.Executors; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.stream.Collectors; 23 | 24 | import static com.xydp.utils.RedisConstants.CACHE_SHOP_KEY; 25 | 26 | @SpringBootTest 27 | class HmDianPingApplicationTests { 28 | 29 | @Autowired 30 | private CacheClient cacheClient; 31 | 32 | @Resource 33 | private ShopServiceImpl shopService; 34 | 35 | @Resource 36 | private RedisIdWorker redisIdWorker; 37 | 38 | @Resource 39 | private StringRedisTemplate stringRedisTemplate; 40 | 41 | private final ExecutorService es = Executors.newFixedThreadPool(500); 42 | 43 | @Test 44 | void testSaveShop() { 45 | shopService.saveShop2Redis(1L, 10L); 46 | } 47 | 48 | @Test 49 | void testSaveLogicalExpire() { 50 | Shop shop = shopService.getById(1L); 51 | cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY + 1L, shop, 10L, TimeUnit.SECONDS); 52 | } 53 | 54 | @Test 55 | void testIdWorker() throws InterruptedException { 56 | CountDownLatch latch = new CountDownLatch(300); 57 | Runnable task = () -> { 58 | for (int i = 0; i < 100; i++) { 59 | long id = redisIdWorker.nextId("order"); 60 | System.out.println("id: " + id); 61 | } 62 | latch.countDown(); 63 | }; 64 | long begin = System.currentTimeMillis(); 65 | for (int i = 0; i < 300; i++) { 66 | es.submit(task); 67 | } 68 | latch.await(); 69 | long end = System.currentTimeMillis(); 70 | System.out.println("time = " + (end - begin)); 71 | } 72 | 73 | @Test 74 | void loadShopData() { 75 | // 1.查询店铺信息 76 | List list = shopService.list(); 77 | // 2.把店铺分组,按照typeId分组,id一致的放到一个集合 78 | Map> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId)); 79 | // 3.分批完成写入Redis 80 | for (Map.Entry> entry : map.entrySet()) { 81 | // 3.1 获取类型id 82 | Long typeId = entry.getKey(); 83 | String key = "shop:geo:" + typeId; 84 | // 3.2 获取同类型的店铺的集合 85 | List value = entry.getValue(); 86 | List> locations = new ArrayList<>(value.size()); 87 | // 3.3 写入Redis GEOADD key 经度 纬度 member 88 | for (Shop shop : value) { 89 | // stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString()); 90 | locations.add(new RedisGeoCommands.GeoLocation<>( 91 | shop.getId().toString(), 92 | new Point(shop.getX(), shop.getY()))); 93 | } 94 | // 批量写入,提高效率 95 | stringRedisTemplate.opsForGeo().add(key, locations); 96 | } 97 | } 98 | 99 | @Test 100 | void testHyperLogLog() { 101 | String[] values = new String[1000]; 102 | for (int i = 0; i < 1000000; i++) { 103 | values[i % 1000] = "user_" + i; 104 | if (i % 1000 == 999) { 105 | // 发送到Redis 106 | stringRedisTemplate.opsForHyperLogLog().add("hl2", values); 107 | } 108 | } 109 | // 统计数量 110 | Long count = stringRedisTemplate.opsForHyperLogLog().size("hl2"); 111 | System.out.println("count: " + count); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/com/xydp/RedissonTest.java: -------------------------------------------------------------------------------- 1 | package com.xydp; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.redisson.RedissonMultiLock; 7 | import org.redisson.api.RLock; 8 | import org.redisson.api.RedissonClient; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | 11 | import javax.annotation.Resource; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | @Slf4j 15 | @SpringBootTest 16 | public class RedissonTest { 17 | 18 | @Resource 19 | private RedissonClient redissonClient; 20 | @Resource 21 | private RedissonClient redissonClient2; // 测试Redis集群(主从分离) 22 | @Resource 23 | private RedissonClient redissonClient3; 24 | 25 | private RLock lock; 26 | 27 | @BeforeEach 28 | void setUp() { 29 | RLock lock1 = redissonClient.getLock("order"); 30 | RLock lock2 = redissonClient2.getLock("order"); 31 | RLock lock3 = redissonClient3.getLock("order"); 32 | 33 | // 创建联锁 multiLock 34 | lock = new RedissonMultiLock(lock1, lock2, lock3); 35 | } 36 | 37 | @Test 38 | void method1() throws InterruptedException { 39 | // 尝试获取锁 40 | boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS); 41 | if (!isLock) { 42 | log.error("获取锁失败 ....1"); 43 | return; 44 | } 45 | try { 46 | log.info("获取锁成功 ....1"); 47 | method2(); 48 | log.info("开始执行业务 ....1"); 49 | } finally { 50 | log.warn("准备释放锁 ....1"); 51 | lock.unlock(); 52 | } 53 | } 54 | 55 | @Test 56 | void method2() { 57 | // 尝试获取锁 58 | boolean isLock = lock.tryLock(); 59 | if (!isLock) { 60 | log.error("获取锁失败 ....2"); 61 | return; 62 | } 63 | try { 64 | log.info("获取锁成功 ....2"); 65 | log.info("开始执行业务 ....2"); 66 | } finally { 67 | log.warn("准备释放锁 ....2"); 68 | lock.unlock(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/xydp/VoucherOrderControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.xydp; 2 | 3 | import cn.hutool.core.lang.Assert; 4 | import cn.hutool.core.thread.ThreadUtil; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.xydp.dto.LoginFormDTO; 7 | import com.xydp.dto.Result; 8 | import com.xydp.entity.User; 9 | import com.xydp.service.IUserService; 10 | import lombok.SneakyThrows; 11 | import org.junit.jupiter.api.DisplayName; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 18 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 19 | 20 | import javax.annotation.Resource; 21 | import java.io.BufferedWriter; 22 | import java.io.File; 23 | import java.io.OutputStreamWriter; 24 | import java.nio.charset.StandardCharsets; 25 | import java.nio.file.Files; 26 | import java.util.List; 27 | import java.util.concurrent.CopyOnWriteArrayList; 28 | import java.util.concurrent.CountDownLatch; 29 | import java.util.concurrent.ExecutorService; 30 | import java.util.stream.Collectors; 31 | 32 | /** 33 | * @author Wuxy 34 | * @version 1.0 35 | * @ClassName VoucherOrderControllerTest 36 | * @since 2023/1/13 11:45 37 | */ 38 | @SpringBootTest 39 | @AutoConfigureMockMvc 40 | class VoucherOrderControllerTest { 41 | 42 | @Resource 43 | private MockMvc mockMvc; 44 | 45 | @Resource 46 | private IUserService userService; 47 | 48 | @Resource 49 | private ObjectMapper mapper; 50 | 51 | @Test 52 | @SneakyThrows 53 | @DisplayName("登录1000个用户,并输出到文件中") 54 | void login() { 55 | List phoneList = userService.lambdaQuery().select(User::getPhone).last("limit 1000").list().stream().map(User::getPhone).collect(Collectors.toList()); 56 | ExecutorService executorService = ThreadUtil.newExecutor(phoneList.size()); 57 | List tokenList = new CopyOnWriteArrayList<>(); 58 | CountDownLatch countDownLatch = new CountDownLatch(phoneList.size()); 59 | phoneList.forEach(phone -> executorService.execute(() -> { 60 | try { 61 | // 验证码 62 | String codeJson = mockMvc.perform(MockMvcRequestBuilders.post("/user/code").queryParam("phone", phone)).andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse().getContentAsString(); 63 | Result result = mapper.readerFor(Result.class).readValue(codeJson); 64 | Assert.isTrue(result.getSuccess(), String.format("获取“%s”手机号的验证码失败", phone)); 65 | String code = result.getData().toString(); 66 | LoginFormDTO formDTO = LoginFormDTO.builder().code(code).phone(phone).build(); 67 | String json = mapper.writeValueAsString(formDTO); 68 | // token 69 | String tokenJson = mockMvc.perform(MockMvcRequestBuilders.post("/user/login").content(json).contentType(MediaType.APPLICATION_JSON)).andExpect(MockMvcResultMatchers.status().isOk()).andReturn().getResponse().getContentAsString(); 70 | result = mapper.readerFor(Result.class).readValue(tokenJson); 71 | Assert.isTrue(result.getSuccess(), String.format("获取“%s”手机号的token失败,json为“%s”", phone, json)); 72 | String token = result.getData().toString(); 73 | tokenList.add(token); 74 | countDownLatch.countDown(); 75 | } catch (Exception e) { 76 | e.printStackTrace(); 77 | } 78 | })); 79 | countDownLatch.await(); 80 | executorService.shutdown(); 81 | Assert.isTrue(tokenList.size() == phoneList.size()); 82 | writeToTxt(tokenList, "\\tokens.txt"); 83 | System.out.println("写入完成!"); 84 | } 85 | 86 | private static void writeToTxt(List list, String suffixPath) throws Exception { 87 | // 1. 创建文件 88 | File file = new File(System.getProperty("user.dir") + "\\src\\main\\resources" + suffixPath); 89 | if (!file.exists()) { 90 | System.out.println("The state of file create is " + file.createNewFile()); 91 | } 92 | // 2. 输出 93 | BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(file.toPath()), StandardCharsets.UTF_8)); 94 | for (String content : list) { 95 | bw.write(content); 96 | bw.newLine(); 97 | } 98 | bw.close(); 99 | } 100 | } 101 | --------------------------------------------------------------------------------