From 6327874166c54ee039cf0d38eb09b14109065753 Mon Sep 17 00:00:00 2001 From: Doghole Date: Mon, 27 Oct 2025 23:30:05 +0800 Subject: [PATCH] Commit --- pom.xml | 6 + .../TushareDataServiceApplication.java | 6 - .../mid/tushare/annotation/UpdateMethod.java | 24 + .../api/common/StockCalendarController.java | 8 +- .../component/OkHttpClientProvider.java | 150 +++ .../component/PlatformInterceptor.java | 100 -- .../component/StaticAttributeInterceptor.java | 155 +++ .../component/UpdateMethodInterceptor.java | 107 ++ .../component/UpdatePlanSyncAspect.java | 96 ++ .../at17/mid/tushare/config/QuartzConfig.java | 6 +- .../at17/mid/tushare/config/VerichConfig.java | 18 +- .../at17/mid/tushare/dao/StockAdjustDao.java | 2 +- .../mid/tushare/dao/StockCalendarDao.java | 99 +- .../mid/tushare/dao/StockDailyBasicDao.java | 6 +- .../at17/mid/tushare/dao/StockDailyDao.java | 34 +- .../at17/mid/tushare/dao/StockHolderDao.java | 5 +- .../at17/mid/tushare/dao/StockInfoDao.java | 4 +- .../at17/mid/tushare/dao/StockLimitDao.java | 6 +- .../at17/mid/tushare/dao/StockMinuteDao.java | 9 +- ...StockThsDailyDao.java => ThsDailyDao.java} | 4 +- .../{StockThsListDao.java => ThsListDao.java} | 6 +- ...ockThsMemberDao.java => ThsMemberDao.java} | 10 +- .../at17/mid/tushare/dao/UpdatePlanDao.java | 12 + .../mid/tushare/data/crawler/QueryWay.java | 2 + .../data/crawler/tushare/TushareClient.java | 27 +- .../data/crawler/tushare/TushareCrawler.java | 549 +------- .../mid/tushare/data/models/StockInfo.java | 2 + .../mid/tushare/data/models/UpdateLog.java | 8 + .../tushare/data/models/UpdateMethodInfo.java | 48 + .../mid/tushare/data/models/UpdatePlan.java | 72 + .../data/service/StockAdjustService.java | 96 ++ .../data/service/StockCalendarService.java | 350 +++++ .../data/service/StockDailyBasicService.java | 114 ++ .../data/service/StockDailyService.java | 177 +++ .../data/service/StockHolderService.java | 138 ++ .../data/service/StockInfoService.java | 105 ++ .../data/service/StockLimitService.java | 115 ++ .../data/service/StockMinuteService.java | 200 +++ .../tushare/data/service/ThsDailyService.java | 100 ++ .../tushare/data/service/ThsListService.java | 51 + .../data/service/ThsMemberService.java | 96 ++ .../data/typehandler/JsonListTypeHandler.java | 67 + .../UpdateMethodInfoListTypeHandler.java | 40 + .../mid/tushare/data/util/CryptoUtil.java | 21 - .../tushare/data/validator/AllowedEnum.java | 19 + .../data/validator/AllowedEnumValidator.java | 23 + .../tushare/data/validator/CronValidator.java | 18 + .../tushare/data/validator/IValidator.java | 17 + .../validator/UpdateMethodInfoValidator.java | 63 + .../mid/tushare/data/validator/ValidCron.java | 17 + .../data/validator/ValidUpdateMethodInfo.java | 20 + .../link/at17/mid/tushare/dto/JsonViews.java | 6 + .../link/at17/mid/tushare/dto/LayPageReq.java | 43 + .../at17/mid/tushare/enums/ListStatus.java | 9 + .../at17/mid/tushare/enums/UpdateLogType.java | 9 + .../mid/tushare/service/BaseServiceImpl.java | 88 ++ .../at17/mid/tushare/service/StatService.java | 11 + .../tushare/service/UpdateMethodService.java | 194 +++ .../tushare/service/UpdatePlanService.java | 162 +++ .../system/util/SpringBeanDetector.java | 44 + .../system/util/SpringContextHolder.java | 26 +- ...ry.java => AutowireCapableJobFactory.java} | 14 +- .../tushare/task/job/DailyUpdateDataJob.java | 93 -- .../mid/tushare/task/job/UpdatePlanJob.java | 61 + .../CacheDailyEvictionScheduler.java | 4 +- .../scheduler/DailyUpdateDataScheduler.java | 31 - .../web/controller/ManageController.java | 8 + .../controller/UpdateMethodController.java | 52 + .../web/controller/UpdatePlanController.java | 105 ++ src/main/resources/application.yml | 8 +- src/main/resources/conf/system/system.json | 8 +- src/main/resources/mappers/StockAdjust.xml | 2 +- src/main/resources/mappers/StockCalendar.xml | 2 +- src/main/resources/mappers/StockDaily.xml | 12 +- src/main/resources/mappers/StockHolder.xml | 2 +- src/main/resources/mappers/StockInfo.xml | 40 +- src/main/resources/mappers/StockLimit.xml | 2 +- src/main/resources/mappers/StockMinute.xml | 6 +- .../{StockThsDaily.xml => ThsDaily.xml} | 2 +- .../mappers/{StockThsList.xml => ThsList.xml} | 2 +- .../{StockThsMember.xml => ThsMember.xml} | 2 +- .../postgresql-verich-public-structure.sql | 51 +- .../admin/res/adminui/dist/css/res/logo.png | Bin 0 -> 3562 bytes .../static/admin/res/modules/cron.js | 1198 +++++++++++++++++ .../static/admin/res/modules/cron/cron.css | 195 +++ src/main/resources/static/favicon.ico | Bin 0 -> 5430 bytes .../admin/drawLine/render.html | 6 +- .../admin/login/login.html | 0 .../admin/manage/reviews/plans/plan-list.html | 444 ++++++ .../manage/reviews/set/system/system.html | 0 .../manage/views/app/content/comment.html | 0 .../admin/manage/views/app/content/list.html | 0 .../admin/manage/views/app/content/tags.html | 0 .../admin/manage/views/app/forum/list.html | 0 .../admin/manage/views/app/forum/replys.html | 0 .../admin/manage/views/app/message/index.html | 0 .../manage/views/app/workorder/list.html | 0 .../manage/views/component/anim/index.html | 0 .../views/component/auxiliar/index.html | 0 .../manage/views/component/badge/index.html | 0 .../manage/views/component/button/index.html | 0 .../views/component/carousel/index.html | 0 .../manage/views/component/code/index.html | 2 +- .../views/component/colorpicker/index.html | 0 .../views/component/dropdown/index.html | 46 +- .../manage/views/component/flow/index.html | 0 .../manage/views/component/form/element.html | 0 .../manage/views/component/form/group.html | 0 .../manage/views/component/grid/all.html | 0 .../manage/views/component/grid/list.html | 0 .../views/component/grid/mobile-pc.html | 0 .../manage/views/component/grid/mobile.html | 0 .../views/component/grid/speed-dial.html | 0 .../manage/views/component/grid/stack.html | 0 .../manage/views/component/laydate/demo1.html | 0 .../manage/views/component/laydate/demo2.html | 0 .../views/component/laydate/special-demo.html | 0 .../manage/views/component/laydate/theme.html | 0 .../manage/views/component/layer/list.html | 2 +- .../views/component/layer/special-demo.html | 0 .../manage/views/component/layer/theme.html | 0 .../manage/views/component/laypage/demo1.html | 0 .../manage/views/component/laypage/demo2.html | 0 .../manage/views/component/nav/index.html | 0 .../manage/views/component/panel/index.html | 0 .../views/component/progress/index.html | 0 .../manage/views/component/rate/index.html | 0 .../manage/views/component/slider/index.html | 0 .../manage/views/component/table/auto.html | 2 +- .../views/component/table/cellEdit.html | 2 +- .../views/component/table/cellEvent.html | 2 +- .../manage/views/component/table/data.html | 4 +- .../manage/views/component/table/fixed.html | 2 +- .../manage/views/component/table/form.html | 2 +- .../manage/views/component/table/index.html | 8 +- .../views/component/table/initSort.html | 2 +- .../manage/views/component/table/onrow.html | 4 +- .../views/component/table/parseData.html | 4 +- .../manage/views/component/table/radio.html | 4 +- .../views/component/table/resetPage.html | 2 +- .../manage/views/component/table/search.html | 2 +- .../manage/views/component/table/static.html | 0 .../manage/views/component/table/style.html | 2 +- .../manage/views/component/table/thead.html | 0 .../views/component/table/tostatic.html | 0 .../manage/views/component/tabs/index.html | 0 .../views/component/timeline/index.html | 0 .../views/component/transfer/index.html | 0 .../manage/views/component/tree/index.html | 58 +- .../views/component/treeTable/index.html | 2 +- .../manage/views/component/upload/demo1.html | 0 .../manage/views/component/upload/demo2.html | 0 .../manage/views/component/util/index.html | 0 .../admin/manage/views/demo-index.html} | 1 + .../admin/manage/views/home/console.html | 0 .../admin/manage/views/home/homepage1.html | 0 .../admin/manage/views/home/homepage2.html | 0 .../templates/admin/manage/views/index.html | 226 ++++ .../manage/views/senior/echarts/bar.html | 0 .../manage/views/senior/echarts/line.html | 0 .../manage/views/senior/echarts/map.html | 0 .../admin/manage/views/senior/im/index.html | 2 +- .../admin/manage/views/set/system/email.html | 0 .../manage/views/set/system/website.html | 0 .../admin/manage/views/set/user/info.html | 0 .../admin/manage/views/set/user/password.html | 0 .../admin/manage/views/system/about.html | 0 .../admin/manage/views/system/theme.html | 0 .../manage/views/template/addresslist.html | 0 .../admin/manage/views/template/caller.html | 0 .../manage/views/template/goodslist.html | 0 .../admin/manage/views/template/msgboard.html | 0 .../manage/views/template/personalpage.html | 0 .../admin/manage/views/template/search.html | 0 .../admin/manage/views/template/tips/404.html | 0 .../manage/views/template/tips/error.html | 0 .../views/user/administrators/list.html | 0 .../views/user/administrators/role.html | 0 .../admin/manage/views/user/forget.html | 0 .../admin/manage/views/user/login.html | 0 .../admin/manage/views/user/reg.html | 0 .../admin/manage/views/user/user/list.html | 0 .../{webpage => templates}/docs/docs.html | 0 .../mid/tushare/FireDragonOutSeaTest.java | 118 -- .../at17/mid/tushare/LonBullishCrossTest.java | 115 -- .../mid/tushare/MaBullishArrangeTest.java | 141 -- .../mid/tushare/MinuteCostDistribution.java | 12 +- .../link/at17/mid/tushare/VolIncreaseBk.java | 20 +- .../at17/mid/tushare/data/UpdateData.java | 85 +- .../tushare/data/UpdateDataCrossCheck.java | 62 +- .../mid/tushare/data/UpdateMinuteTest.java | 20 +- .../tushare/data/UpdateStockAdjustTest.java | 10 +- .../tushare/data/UpdateStockLimitTest.java | 10 +- .../playground/StockInfoServiceTest.java | 39 + .../tushare/test/CostDistributionTest.java | 6 +- .../at17/mid/tushare/test/IndicatorTest.java | 7 +- .../mid/tushare/test/StockCalendarTest.java | 50 +- 197 files changed, 5866 insertions(+), 1580 deletions(-) create mode 100644 src/main/java/link/at17/mid/tushare/annotation/UpdateMethod.java create mode 100644 src/main/java/link/at17/mid/tushare/component/OkHttpClientProvider.java create mode 100644 src/main/java/link/at17/mid/tushare/component/StaticAttributeInterceptor.java create mode 100644 src/main/java/link/at17/mid/tushare/component/UpdateMethodInterceptor.java create mode 100644 src/main/java/link/at17/mid/tushare/component/UpdatePlanSyncAspect.java rename src/main/java/link/at17/mid/tushare/dao/{StockThsDailyDao.java => ThsDailyDao.java} (87%) rename src/main/java/link/at17/mid/tushare/dao/{StockThsListDao.java => ThsListDao.java} (77%) rename src/main/java/link/at17/mid/tushare/dao/{StockThsMemberDao.java => ThsMemberDao.java} (69%) create mode 100644 src/main/java/link/at17/mid/tushare/dao/UpdatePlanDao.java create mode 100644 src/main/java/link/at17/mid/tushare/data/models/UpdateLog.java create mode 100644 src/main/java/link/at17/mid/tushare/data/models/UpdateMethodInfo.java create mode 100644 src/main/java/link/at17/mid/tushare/data/models/UpdatePlan.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockAdjustService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockCalendarService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockDailyBasicService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockDailyService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockHolderService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockInfoService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockLimitService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/StockMinuteService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/ThsDailyService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/ThsListService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/service/ThsMemberService.java create mode 100644 src/main/java/link/at17/mid/tushare/data/typehandler/JsonListTypeHandler.java create mode 100644 src/main/java/link/at17/mid/tushare/data/typehandler/UpdateMethodInfoListTypeHandler.java delete mode 100644 src/main/java/link/at17/mid/tushare/data/util/CryptoUtil.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/AllowedEnum.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/AllowedEnumValidator.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/CronValidator.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/IValidator.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/UpdateMethodInfoValidator.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/ValidCron.java create mode 100644 src/main/java/link/at17/mid/tushare/data/validator/ValidUpdateMethodInfo.java create mode 100644 src/main/java/link/at17/mid/tushare/dto/JsonViews.java create mode 100644 src/main/java/link/at17/mid/tushare/dto/LayPageReq.java create mode 100644 src/main/java/link/at17/mid/tushare/enums/UpdateLogType.java create mode 100644 src/main/java/link/at17/mid/tushare/service/BaseServiceImpl.java create mode 100644 src/main/java/link/at17/mid/tushare/service/StatService.java create mode 100644 src/main/java/link/at17/mid/tushare/service/UpdateMethodService.java create mode 100644 src/main/java/link/at17/mid/tushare/service/UpdatePlanService.java create mode 100644 src/main/java/link/at17/mid/tushare/system/util/SpringBeanDetector.java rename src/main/java/link/at17/mid/tushare/task/{TaskSchedulerFactory.java => AutowireCapableJobFactory.java} (55%) delete mode 100644 src/main/java/link/at17/mid/tushare/task/job/DailyUpdateDataJob.java create mode 100644 src/main/java/link/at17/mid/tushare/task/job/UpdatePlanJob.java delete mode 100644 src/main/java/link/at17/mid/tushare/task/scheduler/DailyUpdateDataScheduler.java create mode 100644 src/main/java/link/at17/mid/tushare/web/controller/UpdateMethodController.java create mode 100644 src/main/java/link/at17/mid/tushare/web/controller/UpdatePlanController.java rename src/main/resources/mappers/{StockThsDaily.xml => ThsDaily.xml} (95%) rename src/main/resources/mappers/{StockThsList.xml => ThsList.xml} (90%) rename src/main/resources/mappers/{StockThsMember.xml => ThsMember.xml} (93%) create mode 100644 src/main/resources/static/admin/res/adminui/dist/css/res/logo.png create mode 100644 src/main/resources/static/admin/res/modules/cron.js create mode 100644 src/main/resources/static/admin/res/modules/cron/cron.css create mode 100644 src/main/resources/static/favicon.ico rename src/main/resources/{webpage => templates}/admin/drawLine/render.html (97%) rename src/main/resources/{webpage => templates}/admin/login/login.html (100%) create mode 100644 src/main/resources/templates/admin/manage/reviews/plans/plan-list.html rename src/main/resources/{webpage => templates}/admin/manage/reviews/set/system/system.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/content/comment.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/content/list.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/content/tags.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/forum/list.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/forum/replys.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/message/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/app/workorder/list.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/anim/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/auxiliar/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/badge/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/button/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/carousel/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/code/index.html (95%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/colorpicker/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/dropdown/index.html (93%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/flow/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/form/element.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/form/group.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/grid/all.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/grid/list.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/grid/mobile-pc.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/grid/mobile.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/grid/speed-dial.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/grid/stack.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/laydate/demo1.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/laydate/demo2.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/laydate/special-demo.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/laydate/theme.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/layer/list.html (97%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/layer/special-demo.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/layer/theme.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/laypage/demo1.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/laypage/demo2.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/nav/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/panel/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/progress/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/rate/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/slider/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/auto.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/cellEdit.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/cellEvent.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/data.html (95%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/fixed.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/form.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/index.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/initSort.html (95%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/onrow.html (94%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/parseData.html (94%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/radio.html (94%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/resetPage.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/search.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/static.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/style.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/thead.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/table/tostatic.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/tabs/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/timeline/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/transfer/index.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/tree/index.html (90%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/treeTable/index.html (97%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/upload/demo1.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/upload/demo2.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/component/util/index.html (100%) rename src/main/resources/{webpage/admin/manage/views/index.html => templates/admin/manage/views/demo-index.html} (97%) rename src/main/resources/{webpage => templates}/admin/manage/views/home/console.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/home/homepage1.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/home/homepage2.html (100%) create mode 100644 src/main/resources/templates/admin/manage/views/index.html rename src/main/resources/{webpage => templates}/admin/manage/views/senior/echarts/bar.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/senior/echarts/line.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/senior/echarts/map.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/senior/im/index.html (96%) rename src/main/resources/{webpage => templates}/admin/manage/views/set/system/email.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/set/system/website.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/set/user/info.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/set/user/password.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/system/about.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/system/theme.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/addresslist.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/caller.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/goodslist.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/msgboard.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/personalpage.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/search.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/tips/404.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/template/tips/error.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/user/administrators/list.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/user/administrators/role.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/user/forget.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/user/login.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/user/reg.html (100%) rename src/main/resources/{webpage => templates}/admin/manage/views/user/user/list.html (100%) rename src/main/resources/{webpage => templates}/docs/docs.html (100%) delete mode 100644 src/test/java/link/at17/mid/tushare/FireDragonOutSeaTest.java delete mode 100644 src/test/java/link/at17/mid/tushare/LonBullishCrossTest.java delete mode 100644 src/test/java/link/at17/mid/tushare/MaBullishArrangeTest.java create mode 100644 src/test/java/link/at17/mid/tushare/playground/StockInfoServiceTest.java diff --git a/pom.xml b/pom.xml index 5cef539..59c67cf 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,7 @@ org.springframework.boot spring-boot-devtools + runtime true @@ -255,6 +256,11 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + com.github.yulichang + mybatis-plus-join-boot-starter + 1.4.11 + diff --git a/src/main/java/link/at17/mid/tushare/TushareDataServiceApplication.java b/src/main/java/link/at17/mid/tushare/TushareDataServiceApplication.java index a93eee5..bb34ae7 100644 --- a/src/main/java/link/at17/mid/tushare/TushareDataServiceApplication.java +++ b/src/main/java/link/at17/mid/tushare/TushareDataServiceApplication.java @@ -1,24 +1,18 @@ package link.at17.mid.tushare; import org.mybatis.spring.annotation.MapperScan; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; -import link.at17.mid.tushare.task.scheduler.DailyUpdateDataScheduler; - @EnableAsync @EnableScheduling @SpringBootApplication @MapperScan(basePackages = {"link.at17.mid.tushare.dao", "link.at17.mid.tushare.web.mapper"}) @EnableCaching(proxyTargetClass=true) public class TushareDataServiceApplication { - - @Autowired - DailyUpdateDataScheduler dailyUpdateDataScheduler; public static void main(String[] args) { SpringApplication.run(TushareDataServiceApplication.class, args); diff --git a/src/main/java/link/at17/mid/tushare/annotation/UpdateMethod.java b/src/main/java/link/at17/mid/tushare/annotation/UpdateMethod.java new file mode 100644 index 0000000..d84e8c0 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/annotation/UpdateMethod.java @@ -0,0 +1,24 @@ +package link.at17.mid.tushare.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * 升级参数 + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface UpdateMethod { + + /** + * 升级方法名称 + * @return + */ + String name(); + + /** + * 加载到内存中的排序 + * @return + */ + int order() default -1; + +} diff --git a/src/main/java/link/at17/mid/tushare/api/common/StockCalendarController.java b/src/main/java/link/at17/mid/tushare/api/common/StockCalendarController.java index 273f6ea..b0c78e1 100644 --- a/src/main/java/link/at17/mid/tushare/api/common/StockCalendarController.java +++ b/src/main/java/link/at17/mid/tushare/api/common/StockCalendarController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import link.at17.mid.tushare.dao.StockCalendarDao; +import link.at17.mid.tushare.data.service.StockCalendarService; import link.at17.mid.tushare.enums.StockMarket; import link.at17.mid.tushare.system.util.LocalDateTimeUtils; import lombok.extern.slf4j.Slf4j; @@ -18,18 +18,18 @@ import lombok.extern.slf4j.Slf4j; public class StockCalendarController { @Autowired - StockCalendarDao stockCalendarDao; + StockCalendarService stockCalendarService; @GetMapping("todayIsOpen") private String todayIsOpen(@Param("stockMarket") StockMarket stockMarket) { - return String.valueOf(stockCalendarDao.isOpen(LocalDateTime.now(), stockMarket)); + return String.valueOf(stockCalendarService.isOpen(LocalDateTime.now(), stockMarket)); } @GetMapping("isOpen") private String isOpen(@Param("stockMarket") StockMarket stockMarket, @Param("date") String date) { LocalDateTime dateTime = LocalDateTimeUtils.parseDate(date); - return String.valueOf(stockCalendarDao.isOpen(dateTime, stockMarket)); + return String.valueOf(stockCalendarService.isOpen(dateTime, stockMarket)); } } diff --git a/src/main/java/link/at17/mid/tushare/component/OkHttpClientProvider.java b/src/main/java/link/at17/mid/tushare/component/OkHttpClientProvider.java new file mode 100644 index 0000000..4720435 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/component/OkHttpClientProvider.java @@ -0,0 +1,150 @@ +package link.at17.mid.tushare.component; + +import java.net.Proxy; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.function.Consumer; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import link.at17.mid.tushare.system.config.SystemConfig; +import link.at17.mid.tushare.system.util.SpringContextHolder; +import okhttp3.OkHttpClient; + +/** + * OkHttpClient 提供器 + * @see link.at17.mid.tushare.system.config.SystemConfig + * @see okhttp3.internal.http.BridgeInterceptor + */ +public class OkHttpClientProvider { + + private static volatile SystemConfig systemConfig; + + private static SystemConfig getSystemConfig() { + if (systemConfig == null) { + synchronized (OkHttpClientProvider.class) { + if (systemConfig == null) { + systemConfig = SpringContextHolder.getBean(SystemConfig.class); + } + } + } + return systemConfig; + } + + /** + * 根据 SystemConfig 获取一个 OkHttpClient 实例 + * @return + */ + public static OkHttpClient getInstance() { + return getInstance(null); + } + + /** + * 根据 SystemConfig 获取一个 OkHttpClient 实例 + * @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据 systemConfig 情况覆盖 + * @return + */ + public static OkHttpClient getInstance(Consumer builderConsumer) { + SystemConfig systemConfig = getSystemConfig(); + return getInstance( + systemConfig.getProxy(), + systemConfig.getIgnoreHttpsVerification(), + builderConsumer); + } + + /** + * 根据指定代理和是否忽略 https 证书获取一个 OkHttpClient 实例 + * @param proxy 指定代理 + * @param ignoreHttpsVerification 是否忽略 https 证书 + * @return + */ + public static OkHttpClient getInstance(Proxy proxy, boolean ignoreHttpsVerification) { + return getInstance(proxy, ignoreHttpsVerification, null); + } + + /** + * 根据指定代理、是否忽略 https 证书和额外 builder 设置获取一个 OkHttpClient 实例 + * @param proxy 指定代理 + * @param ignoreHttpsVerification 是否忽略 https 证书 + * @param builderConsumer 可根据该 consumer 自定义 builder 其他参数,注意 proxy、https 校验等最终仍会根据其他参数覆盖 + * @return + */ + public static OkHttpClient getInstance( + Proxy proxy, + boolean ignoreHttpsVerification, + Consumer builderConsumer) { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (builderConsumer != null) { + builderConsumer.accept(builder); + } + builder.proxy(proxy); + if (ignoreHttpsVerification) { + builder + .sslSocketFactory(getSSLSocketFactory(), getX509TrustManager()) + .hostnameVerifier(getHostnameVerifier()); + } + return builder.build(); + } + + private static HostnameVerifier getHostnameVerifier() { + HostnameVerifier hostnameVerifier = new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + return hostnameVerifier; + } + + private static SSLSocketFactory getSSLSocketFactory() { + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, getTrustManager(), new SecureRandom()); + return sslContext.getSocketFactory(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static X509TrustManager getX509TrustManager() { + X509TrustManager trustManager = null; + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); + } + trustManager = (X509TrustManager) trustManagers[0]; + } catch (Exception e) { + e.printStackTrace(); + } + + return trustManager; + } + + private static TrustManager[] getTrustManager() { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + } + }; + return trustAllCerts; + } +} diff --git a/src/main/java/link/at17/mid/tushare/component/PlatformInterceptor.java b/src/main/java/link/at17/mid/tushare/component/PlatformInterceptor.java index a254044..ccc26ea 100644 --- a/src/main/java/link/at17/mid/tushare/component/PlatformInterceptor.java +++ b/src/main/java/link/at17/mid/tushare/component/PlatformInterceptor.java @@ -3,7 +3,6 @@ package link.at17.mid.tushare.component; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.reflections.Reflections; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -13,15 +12,6 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import link.at17.mid.tushare.annotation.StaticAttribute; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; -import java.util.Map.Entry; /** @@ -34,17 +24,6 @@ public class PlatformInterceptor implements HandlerInterceptor { @Autowired Reflections reflections; - /** - * 静态注入的类缓存 - */ - Map> staticAttributeClassCache; - - /** - * 静态注入的枚举字段缓存 - */ - Map>> options = null; - Map optionNameMap = new HashMap<>(); - @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { @@ -75,85 +54,6 @@ public class PlatformInterceptor implements HandlerInterceptor { } request.setAttribute("request", request); // 把 request 本身放到 attribute 里去,供前端部分位置用 - - // 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用 - if (staticAttributeClassCache == null) { - log.debug("init static attribute classes."); - staticAttributeClassCache = new HashMap<>(); - - - Set> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class); - for (Class staticClass : staticClasses) { - StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class); - String name = staticClass.getSimpleName(); - if (StringUtils.isNotBlank(sa.value())) { - name = sa.value(); - } - - if (staticAttributeClassCache.containsKey(name)) { - // 缓存中已经存在了这个名字,直接忽略 - log.warn("StaticAttribute annotation name {} for class {} has been taken, ignore.", - name, staticClass.getName()); - } - staticAttributeClassCache.put(name, staticClass); - log.debug("{} injected as name {}", staticClass, name); - } - } - else if (staticAttributeClassCache.isEmpty()) { - // 跑完一次后,缓存仍然为空,可能不正常,提示一下 - log.warn("StaticAttributes' staticAttributeClassCache is empty, it's unusual, if you didn't exclude it manually, please check."); - } - Iterator>> iterator = staticAttributeClassCache.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry> entry = iterator.next(); - request.setAttribute(entry.getKey(), entry.getValue()); - } - - if (options == null) { - options = new HashMap<>(); - Set enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class); - for (Field f : enumOptionFields) { - if (!f.isAnnotationPresent(StaticAttribute.class)) continue; - if (!Enum.class.isAssignableFrom(f.getType())) continue; - - // 拿到注解和名前缀 - StaticAttribute anno = f.getAnnotation(StaticAttribute.class); - String prefix = anno.value().isBlank() ? - f.getName() + "Enum" : - anno.value(); - - prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1); - - if (optionNameMap.containsKey(prefix)) { - log.warn("EnumOption name {}:{} has already been taken by {}, please check", - prefix, f.getType().getName(), optionNameMap.get(prefix)); - continue; - } - - optionNameMap.put(prefix, f.getType().getName()); - - // enum 值列表 - @SuppressWarnings("unchecked") - Class> enumType = (Class>) f.getType(); - Enum[] constants = enumType.getEnumConstants(); - - // 构造 Map - Map> optionsMap = new LinkedHashMap<>(); - for (Enum c : constants) { - optionsMap.put(c.name(), c); - } - - // 放到 Model 里 - options.put(prefix, optionsMap); - } - - } - for (Entry>> entry : options.entrySet()) { - request.setAttribute(entry.getKey(), entry.getValue()); - log.debug("Inject enums {}: {} to request {}", - entry.getKey(), optionNameMap.get(entry.getKey()), - request.getRequestURI()); - } } } diff --git a/src/main/java/link/at17/mid/tushare/component/StaticAttributeInterceptor.java b/src/main/java/link/at17/mid/tushare/component/StaticAttributeInterceptor.java new file mode 100644 index 0000000..c55d9c3 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/component/StaticAttributeInterceptor.java @@ -0,0 +1,155 @@ +package link.at17.mid.tushare.component; + + +import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.StringUtils; +import org.reflections.Reflections; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import link.at17.mid.tushare.annotation.StaticAttribute; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + + +/** + * 注入拦截器 + */ +@Component +@Slf4j +public class StaticAttributeInterceptor implements HandlerInterceptor { + + @Autowired + Reflections reflections; + + /** + * 静态注入的类缓存 + */ + Map> staticAttributeClassCache; + + /** + * 静态注入的枚举字段缓存 + */ + Map>> options = null; + Map optionNameMap = new HashMap<>(); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + injectNecessary(request, handler); + return true; + } + + /** + * 为前端模板注入变量 + * @param request + */ + public void injectNecessary(HttpServletRequest request, Object handler) { + + // 只在 Controller(HandlerMethod)里才做注入 + if (!(handler instanceof HandlerMethod)) { + // 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理, + // 这里一律跳过 + return; + } + + // 排除 @ResponseBody/json 接口 + HandlerMethod hm = (HandlerMethod) handler; + if (hm.hasMethodAnnotation(ResponseBody.class) + || hm.getBeanType().isAnnotationPresent(RestController.class)) { + return; + } + + // 查找添加了 @StaticAttribute 注解的类,将其中的枚举注入前端供使用 + if (staticAttributeClassCache == null) { + log.debug("init static attribute classes."); + staticAttributeClassCache = new HashMap<>(); + + + Set> staticClasses = reflections.getTypesAnnotatedWith(StaticAttribute.class); + for (Class staticClass : staticClasses) { + StaticAttribute sa = staticClass.getAnnotation(StaticAttribute.class); + String name = staticClass.getSimpleName(); + if (StringUtils.isNotBlank(sa.value())) { + name = sa.value(); + } + + if (staticAttributeClassCache.containsKey(name)) { + // 缓存中已经存在了这个名字,直接忽略 + log.warn("StaticAttribute 注解的类 {} 指定的名称 {} 已经存在, 旧值会被覆盖", + staticClass.getName(), + name); + } + staticAttributeClassCache.put(name, staticClass); + log.debug("{} 类以 {} 为名称注入", staticClass, name); + } + } + else if (staticAttributeClassCache.isEmpty()) { + // 跑完一次后,缓存仍然为空,可能不正常,提示一下 + log.warn("初始化后,StaticAttribute 缓存为空,这仅在无任何类被 @StaticAttribute 注解的情况下出现。如果这与您的期望不符,请检查"); + } + Iterator>> iterator = staticAttributeClassCache.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + request.setAttribute(entry.getKey(), entry.getValue()); + } + + if (options == null) { + options = new HashMap<>(); + Set enumOptionFields = reflections.getFieldsAnnotatedWith(StaticAttribute.class); + for (Field f : enumOptionFields) { + if (!f.isAnnotationPresent(StaticAttribute.class)) continue; + if (!Enum.class.isAssignableFrom(f.getType())) continue; + + // 拿到注解和名前缀 + StaticAttribute anno = f.getAnnotation(StaticAttribute.class); + String prefix = anno.value().isBlank() ? + f.getName() + "Enum" : + anno.value(); + + prefix = prefix.toUpperCase().charAt(0) + prefix.substring(1); + + if (optionNameMap.containsKey(prefix)) { + log.warn("EnumOption name {}:{} has already been taken by {}, please check", + prefix, f.getType().getName(), optionNameMap.get(prefix)); + continue; + } + + optionNameMap.put(prefix, f.getType().getName()); + + // enum 值列表 + @SuppressWarnings("unchecked") + Class> enumType = (Class>) f.getType(); + Enum[] constants = enumType.getEnumConstants(); + + // 构造 Map + Map> optionsMap = new LinkedHashMap<>(); + for (Enum c : constants) { + optionsMap.put(c.name(), c); + } + + // 放到 Model 里 + options.put(prefix, optionsMap); + } + + } + for (Entry>> entry : options.entrySet()) { + request.setAttribute(entry.getKey(), entry.getValue()); + log.debug("Inject enums {}: {} to request {}", + entry.getKey(), optionNameMap.get(entry.getKey()), + request.getRequestURI()); + } + } + +} diff --git a/src/main/java/link/at17/mid/tushare/component/UpdateMethodInterceptor.java b/src/main/java/link/at17/mid/tushare/component/UpdateMethodInterceptor.java new file mode 100644 index 0000000..375d5cc --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/component/UpdateMethodInterceptor.java @@ -0,0 +1,107 @@ +package link.at17.mid.tushare.component; + + +import lombok.extern.slf4j.Slf4j; + +import org.reflections.Reflections; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import link.at17.mid.tushare.annotation.StaticAttribute; +import link.at17.mid.tushare.data.models.UpdateMethodInfo; +import link.at17.mid.tushare.service.UpdateMethodService; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + + +/** + * 更新注入拦截器 + */ +@Component +@Slf4j +public class UpdateMethodInterceptor implements HandlerInterceptor { + + @Autowired + Reflections reflections; + + Map> optionArgCache; + + @Autowired + UpdateMethodService updateDataService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + injectNecessary(request, handler); + return true; + } + + /** + * 为前端模板注入变量 + * @param request + */ + public void injectNecessary(HttpServletRequest request, Object handler) { + + // 只在 Controller(HandlerMethod)里才做注入 + if (!(handler instanceof HandlerMethod)) { + // 静态资源、图片、css、js 都会被 ResourceHttpRequestHandler 处理, + // 这里一律跳过 + return; + } + + // 排除 @ResponseBody/json 接口 + HandlerMethod hm = (HandlerMethod) handler; + if (hm.hasMethodAnnotation(ResponseBody.class) + || hm.getBeanType().isAnnotationPresent(RestController.class)) { + return; + } + + + if (optionArgCache == null) { + + optionArgCache = new HashMap<>(); + + List potentialUpdateMethods = updateDataService.getPotentialUpdateMethodInfos(); + for (UpdateMethodInfo info : potentialUpdateMethods) { + for (UpdateMethodInfo.UpdateParamInfo paramInfo : info.getParams()) { + Class typeClass = paramInfo.getTypeClass(); + if (typeClass == null) continue; + if (typeClass.isEnum()) { + StaticAttribute sa = typeClass.getAnnotation(StaticAttribute.class); + String simpleNmae = typeClass.getSimpleName(); + if (sa != null) { + log.info("UpdateMethod {} 的参数 {}, 类型 {} 已被 @StaticAttribute 注解,不再重复注入", + info.getMethodName(), paramInfo.getName(), simpleNmae); + continue; + } + if (optionArgCache.containsKey(simpleNmae)) { + log.warn("UpdateMethod {} 参数 {}, 类型 {} 指定的名称 {} 已经存在, 旧值会被覆盖", + typeClass, + simpleNmae); + } + optionArgCache.put(simpleNmae, typeClass); + log.debug("{} 类以 {} 为名称注入", typeClass, simpleNmae); + } + } + } + } + else if (optionArgCache.isEmpty()) { + // 跑完一次后,缓存仍然为空,可能不正常,提示一下 + log.warn("初始化后,Option(s)Arg(s)' 缓存为空,这仅在无任何方法被 UpdateMethod 注解,或 UpdateMethod 不存在枚举类参数的情况下出现。如果这与您的期望不符,请检查"); + } + Iterator>> iterator = optionArgCache.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + request.setAttribute(entry.getKey(), entry.getValue()); + } + } + +} diff --git a/src/main/java/link/at17/mid/tushare/component/UpdatePlanSyncAspect.java b/src/main/java/link/at17/mid/tushare/component/UpdatePlanSyncAspect.java new file mode 100644 index 0000000..ae9afd8 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/component/UpdatePlanSyncAspect.java @@ -0,0 +1,96 @@ +package link.at17.mid.tushare.component; + +import java.util.Collection; +import java.util.List; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; + +import link.at17.mid.tushare.data.models.UpdatePlan; +import link.at17.mid.tushare.service.UpdatePlanService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 更新计划同步切片器

+ * 主要用于拦截通过 updatePlanService 下的所有 save/update/remove 方法 + *

注意: + *

    + *
  1. 仅限第一个参数是 {@code UpdatePlan}, {@code Collection} 或 {@code Wrapper} 的方法 + *
  2. 由于切片是对 UpdatePlanService 切片, 但切片器内部又调用了 UpdatePlanService, + * 如果调用的方法恰好是被切片的方法, 则会进入调用递归导致栈溢出, 需要额外注意. + */ +@Aspect +@Component +@Slf4j +@Validated +@RequiredArgsConstructor +public class UpdatePlanSyncAspect { + + private final UpdatePlanService updatePlanService; + + // 拦截所有 UpdatePlanService 的 save/update/remove 方法 + @Pointcut("(" + + "execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.save*(..)) || " + + "execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.update*(..)) || " + + "execution(* com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.remove*(..)) || " + + "execution(* com.baomidou.mybatisplus.extension.service.IService.save*(..)) || " + + "execution(* com.baomidou.mybatisplus.extension.service.IService.update*(..)) || " + + "execution(* com.baomidou.mybatisplus.extension.service.IService.remove*(..))" + + ") && " + + "target(link.at17.mid.tushare.service.UpdatePlanService)") + public void taskOps() {} + + @AfterReturning(pointcut = "taskOps()", returning = "result") + public void afterTaskOp(JoinPoint jp, Object result) { + + if (!(result instanceof Boolean bool) || !bool) { + // 未成功执行,不进行切片 + return; + } + + Object arg = jp.getArgs()[0]; + if (arg instanceof UpdatePlan task) { + handle(task, jp.getSignature().getName()); + } + else if (arg instanceof Collection coll) { + coll.stream() + .filter(UpdatePlan.class::isInstance) + .map(UpdatePlan.class::cast) + .forEach(t -> handle(t, jp.getSignature().getName())); + } + else if (arg instanceof Wrapper wrapper) { + @SuppressWarnings("unchecked") + List plans = updatePlanService.list((Wrapper)wrapper); + plans.stream() + .filter(UpdatePlan.class::isInstance) + .map(UpdatePlan.class::cast) + .forEach(t -> handle(t, jp.getSignature().getName())); + } + } + + private void handle(UpdatePlan task, String methodName) { + + // 但凡存在的都应该 reschedule + + try { + if (methodName.startsWith("remove")) { + updatePlanService.deleteTask(task.getId()); + log.info("已同步删除 Quartz 任务: [{}]{}", task.getId(), task.getName()); + } + else { + updatePlanService.rescheduleTask(task); + log.info("已同步更新 Quartz 任务: [{}]{}", task.getId(), task.getName()); + } + } + catch (Exception e) { + log.error("同步 Quartz 调度失败: [{}]{}", task.getId(), task.getName(), e); + } + } +} diff --git a/src/main/java/link/at17/mid/tushare/config/QuartzConfig.java b/src/main/java/link/at17/mid/tushare/config/QuartzConfig.java index 20a1037..cef75a5 100644 --- a/src/main/java/link/at17/mid/tushare/config/QuartzConfig.java +++ b/src/main/java/link/at17/mid/tushare/config/QuartzConfig.java @@ -8,17 +8,17 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.quartz.SchedulerFactoryBean; -import link.at17.mid.tushare.task.TaskSchedulerFactory; +import link.at17.mid.tushare.task.AutowireCapableJobFactory; @Configuration public class QuartzConfig { @Autowired - private TaskSchedulerFactory taskSchedulerFactory; + private AutowireCapableJobFactory autowireCapableJobFactory; @Bean public SchedulerFactoryBean schedulerFactoryBean() { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); - schedulerFactoryBean.setJobFactory(taskSchedulerFactory); + schedulerFactoryBean.setJobFactory(autowireCapableJobFactory); return schedulerFactoryBean; } diff --git a/src/main/java/link/at17/mid/tushare/config/VerichConfig.java b/src/main/java/link/at17/mid/tushare/config/VerichConfig.java index e63c680..0362506 100644 --- a/src/main/java/link/at17/mid/tushare/config/VerichConfig.java +++ b/src/main/java/link/at17/mid/tushare/config/VerichConfig.java @@ -7,18 +7,28 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import link.at17.mid.tushare.component.ConfigAutoRegistrar; -import link.at17.mid.tushare.component.PlatformInterceptor; +import link.at17.mid.tushare.component.StaticAttributeInterceptor; +import link.at17.mid.tushare.component.UpdateMethodInterceptor; @Configuration @Import(ConfigAutoRegistrar.class) public class VerichConfig implements WebMvcConfigurer { + + @Autowired + StaticAttributeInterceptor platformInterceptor; @Autowired - PlatformInterceptor platformInterceptor; + UpdateMethodInterceptor updateMethodInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(platformInterceptor) - .addPathPatterns("/**"); + registry + .addInterceptor(platformInterceptor) + .addPathPatterns("/**"); + + registry + .addInterceptor(updateMethodInterceptor) + .addPathPatterns("/**"); + } } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockAdjustDao.java b/src/main/java/link/at17/mid/tushare/dao/StockAdjustDao.java index 93742e7..6783bb6 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockAdjustDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockAdjustDao.java @@ -15,5 +15,5 @@ import java.util.List; @Mapper public interface StockAdjustDao extends ITsTradeDate { @BatchInsert - int insertOrUpdateListTushare(@BatchList List list); + int insertOrUpdateList(@BatchList List list); } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockCalendarDao.java b/src/main/java/link/at17/mid/tushare/dao/StockCalendarDao.java index d73f9be..034e0e5 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockCalendarDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockCalendarDao.java @@ -1,7 +1,7 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; @@ -22,13 +22,13 @@ import java.util.List; @Component @Mapper -public interface StockCalendarDao extends BaseMapper { +public interface StockCalendarDao extends MPJBaseMapper { /** * 更新股票日历 * @param list */ @BatchInsert - void insertOrUpdateList(@BatchList List list); + int insertOrUpdateList(@BatchList List list); /** * 获取数据库内指定证交所的最近一个交易日历 @@ -36,36 +36,6 @@ public interface StockCalendarDao extends BaseMapper { * @return */ StockCalendar getLatest(@Nullable StockMarket stockMarket); - - /** - * 获取数据库内指定证交所的最早一个交易日历的日期 - *

    仅为日期,并未指定是否是开市日 - * @see #getGreatest - * @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定) - * @return {@code LocalDate} 或 {@code null} - */ - default LocalDate getLatestLocalDate(@Nullable StockMarket stockMarket) { - StockCalendar stockCalendar = getLatest(stockMarket); - if (stockCalendar != null) { - return stockCalendar.getDate().atStartOfDay().toLocalDate(); - } - return null; - } - - /** - * 获取数据库内指定证交所的最早一个交易日历的日期时间 - *

    仅为日期时间,并未指定是否是开市日 - * @see #getGreatest - * @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定) - * @return {@code LocalDateTime} 或 {@code null} - */ - default LocalDateTime getLatestLocalDateTime(@Nullable StockMarket stockMarket) { - StockCalendar stockCalendar = getLatest(stockMarket); - if (stockCalendar != null) { - return stockCalendar.getDate().atStartOfDay(); - } - return null; - } /** * 获取数据库内指定证交所的最早一个交易日历 @@ -75,52 +45,24 @@ public interface StockCalendarDao extends BaseMapper { */ StockCalendar getGreatest(@Nullable StockMarket stockMarket); - /** - * 获取数据库内指定证交所的最早一个交易日历的日期 - *

    仅为日期,并未指定是否是开市日 - * @see #getGreatest - * @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定) - * @return {@code LocalDate} 或 {@code null} - */ - default LocalDate getGreatestLocalDate(@Nullable StockMarket stockMarket) { - StockCalendar stockCalendar = getGreatest(stockMarket); - if (stockCalendar != null) { - return stockCalendar.getDate().atStartOfDay().toLocalDate(); - } - return null; - } /** - * 获取数据库内指定证交所的最早一个交易日历的日期时间 - *

    仅为日期,并未指定是否是开市日 - * @see #getGreatest - * @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定) - * @return {@code LocalDateTime} 或 {@code null} - */ - default LocalDateTime getGreatestLocalDateTime(@Nullable StockMarket stockMarket) { - StockCalendar stockCalendar = getGreatest(stockMarket); - if (stockCalendar != null) { - return stockCalendar.getDate().atStartOfDay(); - } - return null; - } - - /** - * 获取指定证交所两个日期(含)之间的交易日个数
    > + * 获取指定证交所两个日期(含)之间的交易日个数
    + *

    * 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate}
    * 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒 *

    例: *

    • - * startDate : LocalDateTime 2021-08-16 01:20:30
      - * endDate : LocalDateTime 2021-08-20 19:20:30
      + * startDate : LocalDateTime 2021-08-16 01:20:30
      + * endDate   : LocalDateTime 2021-08-20 19:20:30
      * 返回 5 *
    • countOpenDaysBetween(StockMarket.SH, startDate, endDate) 其中
      - * startDate : LocalDateTime 2021-08-16 01:20:30
      - * endDate : LocalDateTime 2021-08-16 12:34:56
      + * startDate : LocalDateTime 2021-08-16 01:20:30
      + * endDate   : LocalDateTime 2021-08-16 12:34:56
      * 返回 1 *
    • countOpenDaysBetween(StockMarket.SH, startDate, endDate) 其中
      - * startDate : LocalDateTime 2021-08-15 01:20:30
      - * endDate : LocalDateTime 2021-08-16 12:34:56
      + * startDate : LocalDateTime 2021-08-15 01:20:30
      + * endDate   : LocalDateTime 2021-08-16 12:34:56
      * 返回 1 *
    *

    @@ -133,15 +75,24 @@ public interface StockCalendarDao extends BaseMapper { long countOpenDaysBetween(@Param("exchange") StockMarket exchange, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate); + /** * 获取指定证交所两个日期(含)之间的所有交易日
    * 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate}
    * 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒 *

    例: *

    • countOpenDaysBetween(StockMarket.SH, startDate, endDate) 其中
      - * startDate : LocalDateTime 2021-08-16 01:20:30
      - * endDate : LocalDateTime 2021-08-20 19:20:30
      - * 返回:2021-08-16 2021-08-17 2021-08-18 2021-08-19 2021-08-20 + * startDate : LocalDateTime 2021-08-16 01:20:30
      + * endDate  : LocalDateTime 2021-08-20 19:20:30
      + *

      + *

      + * 返回: + *

        + *
      • 2021-08-16 + *
      • 2021-08-17 + *
      • 2021-08-18 + *
      • 2021-08-19 + *
      • 2021-08-20 *
      *

      * @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重获得结果 @@ -164,10 +115,10 @@ public interface StockCalendarDao extends BaseMapper { /** * 获取指定证交所指定日期(含)以后的所有交易日 * @param exchange - * @param after 留空则查询所有 + * @param sence 留空则查询所有 * @return */ - List getAllOpenDate(@Param("exchange") @Nullable StockMarket exchange, @Param("after") @Nullable Temporal after); + List getAllOpenDateSence(@Param("exchange") @Nullable StockMarket exchange, @Param("after") @Nullable Temporal sence); /** * 判断指定日是否为指定交易所的交易日 diff --git a/src/main/java/link/at17/mid/tushare/dao/StockDailyBasicDao.java b/src/main/java/link/at17/mid/tushare/dao/StockDailyBasicDao.java index 79d9844..449b650 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockDailyBasicDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockDailyBasicDao.java @@ -1,9 +1,11 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; +import link.at17.mid.tushare.data.models.StockValueEx; import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; import org.apache.ibatis.annotations.Mapper; @@ -13,11 +15,11 @@ import java.util.List; @Component @Mapper -public interface StockDailyBasicDao extends ITsTradeDate { +public interface StockDailyBasicDao extends MPJBaseMapper, ITsTradeDate { /** * 批量插入/更新日基本数据 * @param list */ @BatchInsert - void insertOrUpdateList(@BatchList List list); + int insertOrUpdateList(@BatchList List list); } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockDailyDao.java b/src/main/java/link/at17/mid/tushare/dao/StockDailyDao.java index 8c8b7e3..1734d94 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockDailyDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockDailyDao.java @@ -1,11 +1,11 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; import link.at17.mid.tushare.data.models.StockValue; -import link.at17.mid.tushare.data.models.StockValueEx; import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; import org.apache.ibatis.annotations.Mapper; @@ -13,65 +13,65 @@ import org.apache.ibatis.annotations.Param; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; +import java.time.temporal.Temporal; import java.util.List; @Component @Mapper -public interface StockDailyDao extends ITsTradeDate { +public interface StockDailyDao extends MPJBaseMapper, ITsTradeDate { /** * 批量插入/更新日线数据 * @param list */ @BatchInsert - void insertOrUpdateList(@BatchList List list); + int insertOrUpdateList(@BatchList List list); /** * 获取除权日线数据 + 基本行情数据 - * @param stockCode 股票代码,不允许为空 + * @param tsCode Tushare 股票代码,不允许为空 * @param endDate 结束日期(包含),留空则为最新一个交易日 - * @param before 多少个交易日以前,留空则查询上市以来所有数据 + * @param before 多少个交易日以前,留空则查询上市至 endDate 以来所有数据 * @return */ - List getExDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before); + List getExDailyBefore(@Param("stockCode") @NotNull String tsCode, @Param("endDate") Temporal endDate, @Param("before") Long before); /** * 获取前复权日线数据 - * @param stockCode 股票代码,不允许为空 + * @param tsCode Tushare 股票代码,不允许为空 * @param startDate 开始日期,留空则为股票上市日起 * @param endDate 结束日期,null 则为最新一个交易日 * @return */ - List getQfqDailyTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); + List getQfqDaily(@Param("stockCode") @NotNull String tsCode, @Param("startDate") Temporal startDate, @Param("endDate") Temporal endDate); /** * 获取前复权日线数据 - * @param stockCode 股票代码,不允许为空 + * @param tsCode Tushare 股票代码,不允许为空 * @param endDate 结束日期,留空则为最新一个交易日 * @param before 多少个交易日以前,null 则查询上市以来所有数据 * @return */ - List getQfqDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before); + List getQfqDailyBefore(@Param("stockCode") @NotNull String tsCode, @Param("endDate") Temporal endDate, @Param("before") Long before); /** * 获取前复权日线数据 + 基本行情数据 - * @param stockCode 股票代码,不允许为空 + * @param tsCode Tushare 股票代码,不允许为空 * @param startDate 开始日期(包含),留空则为上市第一日 * @param after 多少个交易日以前,null 则查询 startDate 以来所有数据 * @return */ - List getExQfqDailyAfterTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("after") Long after); + List getExQfqDailyAfter(@Param("stockCode") @NotNull String tsCode, @Param("startDate") Temporal startDate, @Param("after") Long after); /** * 获取前复权日线数据 + 基本行情数据 - * @param stockCode 股票代码,不允许为空 + * @param tsCode Tushare 股票代码,不允许为空 * @param endDate 结束日期,留空则为最新一个交易日 * @param before 多少个交易日以前,null 则查询上市以来所有数据 * @return */ - List getExQfqDailyBeforeTushare(@Param("stockCode") @NotNull String stockCode, @Param("endDate") LocalDateTime endDate, @Param("before") Long before); + List getExQfqDailyBefore(@Param("stockCode") @NotNull String tsCode, @Param("endDate") Temporal endDate, @Param("before") Long before); /** * 获取前复权日线数据 - * @param stockCode 股票代码,不允许为空 + * @param tsCode Tushare 股票代码,不允许为空 * @param startDate 开始日期(包含),留空则为上市第一日 * @param after 多少个交易日以前,null 则查询 startDate 以来所有数据 * @return */ - List getQfqDailyAfterTushare(@Param("stockCode") @NotNull String stockCode, @Param("startDate") LocalDateTime startDate, @Param("after") Long after); + List getQfqDailyAfter(@Param("stockCode") @NotNull String tsCode, @Param("startDate") Temporal startDate, @Param("after") Long after); } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockHolderDao.java b/src/main/java/link/at17/mid/tushare/dao/StockHolderDao.java index 7f762e6..c20964f 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockHolderDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockHolderDao.java @@ -1,6 +1,7 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; @@ -18,9 +19,9 @@ import java.util.List; @Component @Mapper -public interface StockHolderDao extends ITsTradeDate { +public interface StockHolderDao extends MPJBaseMapper, ITsTradeDate { @BatchInsert - int insertOrUpdateListTushare(@BatchList List list, @NonNull StockHolderType holderType); + int insertOrUpdateList(@BatchList List list, @NonNull StockHolderType holderType); @EvictAfterUpdate("tushare") @Cacheable("stockHolderDao.getAllByStockCode") diff --git a/src/main/java/link/at17/mid/tushare/dao/StockInfoDao.java b/src/main/java/link/at17/mid/tushare/dao/StockInfoDao.java index ce298aa..3ca7d01 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockInfoDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockInfoDao.java @@ -1,7 +1,7 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; @@ -16,7 +16,7 @@ import java.util.List; @Component @Mapper -public interface StockInfoDao extends BaseMapper { +public interface StockInfoDao extends MPJBaseMapper { @BatchInsert void insertOrUpdateList(@BatchList List list); StockInfo getStockInfoByStockCode(@Param("stockCode") String stockCode); diff --git a/src/main/java/link/at17/mid/tushare/dao/StockLimitDao.java b/src/main/java/link/at17/mid/tushare/dao/StockLimitDao.java index dca951c..b429d75 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockLimitDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockLimitDao.java @@ -1,7 +1,7 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; @@ -15,7 +15,7 @@ import java.util.List; @Component @Mapper -public interface StockLimitDao extends BaseMapper, ITsTradeDate { +public interface StockLimitDao extends MPJBaseMapper, ITsTradeDate { @BatchInsert - int insertOrUpdateListTushare(@BatchList List list); + int insertOrUpdateList(@BatchList List list); } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockMinuteDao.java b/src/main/java/link/at17/mid/tushare/dao/StockMinuteDao.java index bd1cde0..8a71e6d 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockMinuteDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/StockMinuteDao.java @@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import java.time.LocalDateTime; +import java.time.temporal.Temporal; import java.util.List; @Component @@ -24,7 +25,7 @@ public interface StockMinuteDao { * @param list */ @BatchInsert - void insertOrUpdateList(@NotNull StockSpan stockSpan, @BatchList List list); + int insertOrUpdateList(@BatchList List list, @NotNull StockSpan stockSpan); /** * 获取最新交易日
      @@ -57,7 +58,7 @@ public interface StockMinuteDao { * @param endDate 结束日期,留空则为最新一个交易日 * @return */ - List getQfqMinuteTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime startDate, LocalDateTime endDate); + List getQfqMinute(@NotNull String stockCode, @NotNull StockSpan stockSpan, Temporal startDate, Temporal endDate); /** * 获取前复权日线数据 * @param stockCode 股票代码,不允许为空 @@ -65,7 +66,7 @@ public interface StockMinuteDao { * @param before 多少个交易日以前,留空则查询上市以来所有数据 * @return */ - List getQfqMinuteBeforeTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime endDate, Long before); + List getQfqMinuteBefore(@NotNull String stockCode, @NotNull StockSpan stockSpan, Temporal endDate, Long before); /** * 获取前复权日线 Ex 数据 * @param stockCode 股票代码,不允许为空 @@ -73,5 +74,5 @@ public interface StockMinuteDao { * @param before 多少个交易日以前,留空则查询上市以来所有数据 * @return */ - List getExQfqMinuteBeforeTushare(@NotNull String stockCode, @NotNull StockSpan stockSpan, LocalDateTime endDate, Long before); + List getExQfqMinuteBefore(@NotNull String stockCode, @NotNull StockSpan stockSpan, Temporal endDate, Long before); } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockThsDailyDao.java b/src/main/java/link/at17/mid/tushare/dao/ThsDailyDao.java similarity index 87% rename from src/main/java/link/at17/mid/tushare/dao/StockThsDailyDao.java rename to src/main/java/link/at17/mid/tushare/dao/ThsDailyDao.java index a364d34..39843a5 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockThsDailyDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/ThsDailyDao.java @@ -17,13 +17,13 @@ import java.util.List; @Component @Mapper -public interface StockThsDailyDao extends ITsTradeDate { +public interface ThsDailyDao extends ITsTradeDate { /** * 批量插入/更新日线数据 * @param list */ @BatchInsert - void insertOrUpdateList(@BatchList List list); + int insertOrUpdateList(@BatchList List list); /** * 获取板块日线数据 * @param stockCode 板块代码,不允许为空 diff --git a/src/main/java/link/at17/mid/tushare/dao/StockThsListDao.java b/src/main/java/link/at17/mid/tushare/dao/ThsListDao.java similarity index 77% rename from src/main/java/link/at17/mid/tushare/dao/StockThsListDao.java rename to src/main/java/link/at17/mid/tushare/dao/ThsListDao.java index 0000ffe..b162dfb 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockThsListDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/ThsListDao.java @@ -2,7 +2,7 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.github.yulichang.base.MPJBaseMapper; import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; @@ -16,9 +16,9 @@ import java.util.List; @Component @Mapper -public interface StockThsListDao extends BaseMapper { +public interface ThsListDao extends MPJBaseMapper { @BatchInsert - void insertOrUpdateList(@BatchList List list); + int insertOrUpdateList(@BatchList List list); default List listByExchange(ThsStockMarket exchange){ return selectList(new LambdaQueryWrapper().eq(exchange != null, ThsStockInfo::getExchange, exchange)); } diff --git a/src/main/java/link/at17/mid/tushare/dao/StockThsMemberDao.java b/src/main/java/link/at17/mid/tushare/dao/ThsMemberDao.java similarity index 69% rename from src/main/java/link/at17/mid/tushare/dao/StockThsMemberDao.java rename to src/main/java/link/at17/mid/tushare/dao/ThsMemberDao.java index 6171259..7a4f420 100644 --- a/src/main/java/link/at17/mid/tushare/dao/StockThsMemberDao.java +++ b/src/main/java/link/at17/mid/tushare/dao/ThsMemberDao.java @@ -1,11 +1,9 @@ package link.at17.mid.tushare.dao; import com.alibaba.fastjson2.JSONObject; - import link.at17.mid.tushare.annotation.BatchInsert; import link.at17.mid.tushare.annotation.BatchList; import link.at17.mid.tushare.data.models.StockInfo; - import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Component; @@ -13,13 +11,13 @@ import java.util.List; @Component @Mapper -public interface StockThsMemberDao { +public interface ThsMemberDao { @BatchInsert - void insertOrUpdateList(@BatchList List list); + int insertOrUpdateList(@BatchList List list); List getThsMembers(String tsCode); /** - * 获取同花顺所属概念 - * @param tsCode + * 获取个股的同花顺所属概念,以逗号分隔 + * @param tsCode 个股的 Tushare 代码 * @return */ String getBelongings(String tsCode); diff --git a/src/main/java/link/at17/mid/tushare/dao/UpdatePlanDao.java b/src/main/java/link/at17/mid/tushare/dao/UpdatePlanDao.java new file mode 100644 index 0000000..767c938 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/dao/UpdatePlanDao.java @@ -0,0 +1,12 @@ +package link.at17.mid.tushare.dao; + +import org.apache.ibatis.annotations.Mapper; + +import com.github.yulichang.base.MPJBaseMapper; + +import link.at17.mid.tushare.data.models.UpdatePlan; + +@Mapper +public interface UpdatePlanDao extends MPJBaseMapper { + +} diff --git a/src/main/java/link/at17/mid/tushare/data/crawler/QueryWay.java b/src/main/java/link/at17/mid/tushare/data/crawler/QueryWay.java index 3842079..9eb61c0 100644 --- a/src/main/java/link/at17/mid/tushare/data/crawler/QueryWay.java +++ b/src/main/java/link/at17/mid/tushare/data/crawler/QueryWay.java @@ -1,11 +1,13 @@ package link.at17.mid.tushare.data.crawler; +import link.at17.mid.tushare.annotation.StaticAttribute; /** * 查询方式 * @author Barry * */ +@StaticAttribute public enum QueryWay { /** * 以日期更新,适用于已有从上市开始的部分数据的更新 diff --git a/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareClient.java b/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareClient.java index 78d184f..690266b 100644 --- a/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareClient.java +++ b/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareClient.java @@ -6,11 +6,18 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONException; import com.alibaba.fastjson2.JSONObject; +import link.at17.mid.tushare.component.OkHttpClientProvider; +import link.at17.mid.tushare.system.config.SystemConfig; +import link.at17.mid.tushare.system.util.SpringContextHolder; import lombok.extern.slf4j.Slf4j; import okhttp3.Call; import okhttp3.MediaType; @@ -20,13 +27,21 @@ import okhttp3.RequestBody; import okhttp3.Response; @Slf4j +@Component public class TushareClient { private static final String TUSHARE_URL = "http://api.tushare.pro"; - // private static String token = "83a82fadb0bbb803f008b31ce09479e5107f4aba3f28d5df2174c642"; - // private static String token = "c473f86ae2f5703f58eecf9864fa9ec91d67edbc01e3294f6a4f9c32"; - // private static String token = "ec3d7415bf3dfebf0f27b1e5a9805f809b083fb1f8590c8c2bdc633f"; - private static String token = "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509"; + + private static String getToken() { + SystemConfig systemConfig = SpringContextHolder.getBean(SystemConfig.class); + String token = systemConfig.getTushareToken(); + if (!StringUtils.isEmpty(token) && Pattern.matches("^[0-9a-f]{56}$", token)) { + return token; + } + else { + return "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509"; + } + } /** * 返回原始查询结果 @@ -34,8 +49,8 @@ public class TushareClient { * @return */ public static JSONObject query(TushareRequestBody tr) throws IOException, JSONException { - OkHttpClient okHttpClient = new OkHttpClient(); - tr.setToken(token); + OkHttpClient okHttpClient = OkHttpClientProvider.getInstance(); + tr.setToken(getToken()); Request request = new Request.Builder() .url(TUSHARE_URL) .post( diff --git a/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareCrawler.java b/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareCrawler.java index 4589e1a..ee04d7c 100644 --- a/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareCrawler.java +++ b/src/main/java/link/at17/mid/tushare/data/crawler/tushare/TushareCrawler.java @@ -1,53 +1,32 @@ package link.at17.mid.tushare.data.crawler.tushare; -import java.io.IOException; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.TreeSet; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.function.Function; import java.util.stream.Stream; -import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.Assert; -import com.alibaba.fastjson2.JSONException; import com.alibaba.fastjson2.JSONObject; -import link.at17.mid.tushare.dao.StockAdjustDao; -import link.at17.mid.tushare.dao.StockCalendarDao; -import link.at17.mid.tushare.dao.StockDailyBasicDao; -import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockHolderDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.dao.StockLimitDao; -import link.at17.mid.tushare.dao.StockMinuteDao; -import link.at17.mid.tushare.dao.StockThsDailyDao; -import link.at17.mid.tushare.dao.StockThsListDao; -import link.at17.mid.tushare.dao.StockThsMemberDao; import link.at17.mid.tushare.data.crawler.QueryWay; import link.at17.mid.tushare.data.crawler.RetryAndDelay; import link.at17.mid.tushare.data.models.StockInfo; -import link.at17.mid.tushare.data.models.ThsStockInfo; import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; -import link.at17.mid.tushare.enums.StockHolderType; +import link.at17.mid.tushare.data.service.StockCalendarService; +import link.at17.mid.tushare.data.service.StockInfoService; import link.at17.mid.tushare.enums.StockMarket; import link.at17.mid.tushare.enums.StockSpan; -import link.at17.mid.tushare.enums.ThsStockMarket; -import link.at17.mid.tushare.system.util.LocalDateTimeUtils; import lombok.extern.slf4j.Slf4j; /** @@ -61,32 +40,9 @@ public class TushareCrawler { @Autowired - private StockInfoDao stockInfoDao; + private StockInfoService stockInfoService; @Autowired - private StockDailyDao stockDailyDao; - @Autowired - private StockMinuteDao stockMinuteDao; - @Autowired - private StockDailyBasicDao stockDailyBasicDao; - @Autowired - private StockCalendarDao stockCalendarDao; - @Autowired - private StockAdjustDao stockAdjustDao; - @Autowired - private StockThsListDao stockThsListDao; - @Autowired - private StockThsMemberDao stockThsMemberDao; - @Autowired - private StockThsDailyDao stockThsDailyDao; - @Autowired - private StockHolderDao stockHolderDao; - @Autowired - private StockLimitDao stockLimitDao; - - @Autowired - private RedissonClient redis; - - private static final LocalDateTime THS_DAILY_BEGIN_LOCALDATE = LocalDateTime.of(2007, 8, 1, 0, 0, 0); + private StockCalendarService stockCalendarService; private static final int MAX_THREADS = 5; /** @@ -103,7 +59,7 @@ public class TushareCrawler { * @return * @see TushareCrawler#rollingQueryByStock */ - private List> rollingQueryByDate(TushareRequestBody baseRequest, ITsTradeDate iTradeDate, + public List> rollingQueryByDate(TushareRequestBody baseRequest, ITsTradeDate iTradeDate, Function, Boolean> processDataFunc, QueryWay queryWay, LocalDateTime start){ ExecutorService es = Executors.newFixedThreadPool(MAX_THREADS); RetryAndDelay retryAndDelay = new RetryAndDelay().setThreadNum(MAX_THREADS); @@ -122,7 +78,7 @@ public class TushareCrawler { queryWay = QueryWay.ByDateCrossCheck; return rollingQueryByDate(baseRequest, iTradeDate, processDataFunc, queryWay, start); } - start = stockCalendarDao.getGreatestLocalDateTime(null); + start = stockCalendarService.getGreatestLocalDateTime(); } if (queryWay == QueryWay.ByDateUpdate && iTradeDate != null) { @@ -137,7 +93,7 @@ public class TushareCrawler { LocalDateTime end = LocalDateTime.now().toLocalDate().atStartOfDay(); while(!start.isAfter(end)) { - if (all || stockCalendarDao.isOpen(start, (StockMarket)null)) { + if (all || stockCalendarService.isOpen(start, (StockMarket)null)) { TushareRequestBody rq = baseRequest.clone() .addDateParam("trade_date", start); executeResult.add(es.submit(new TushareResponseCallable(rq, processDataFunc) @@ -152,10 +108,10 @@ public class TushareCrawler { } // 交叉检查 final LocalDateTime currentDate = LocalDateTime.now(); - final List szStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate); - final List shStockCalendars = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SH, null, currentDate); - final List allStockCalendars = stockCalendarDao.getAllOpenDatesBetween((StockMarket)null, null, currentDate); - final List stockInfos = stockInfoDao.getStockListByListStatus(null); + final List szStockCalendars = stockCalendarService.getAllOpenDatesBetween(StockMarket.SZ, null, currentDate); + final List shStockCalendars = stockCalendarService.getAllOpenDatesBetween(StockMarket.SH, null, currentDate); + final List allStockCalendars = stockCalendarService.getAllOpenDatesBetween((StockMarket)null, null, currentDate); + final List stockInfos = stockInfoService.list(); TreeSet needUpdates = new TreeSet<>(); stockInfos.forEach(stockInfo -> { List stockTradeDates = iTradeDate.getAllTradeDates(stockInfo); @@ -204,7 +160,7 @@ public class TushareCrawler { * @return 每条请求的执行结果 * @see TushareCrawler#rollingQueryByDate */ - private List> rollingQueryByStock( + public List> rollingQueryByStock( TushareRequestBody baseRequest, List stockList, ITsTradeDate iTradeDate, @@ -269,7 +225,7 @@ public class TushareCrawler { StockMarket exchange = stockCode.endsWith("SZ") ? StockMarket.SZ : StockMarket.SH ; //stockCode.endsWith("SZ") ? "SZSE" : stockCode.endsWith("SH") ? "SSE" : null; // 计算 start 和 end 之间一共有多少个交易日 - long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end); + long dataBetween = stockCalendarService.countOpenDaysBetween(exchange, start, end); long dataPerDay = 1; if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) { // 分钟数,重新算下 dataBetween @@ -277,14 +233,14 @@ public class TushareCrawler { } dataBetween *= dataPerDay; long daySpan = singleMax / dataPerDay; - LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan); + LocalDateTime tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan); while (dataBetween > singleMax) { rq.addDateParam("end_date",tmpEndDate); executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc) .setRetryAndDelay(retryAndDelay))); start = tmpEndDate; - tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan); + tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan); rq.addDateParam("start_date", start); dataBetween -= daySpan * dataPerDay; } @@ -302,7 +258,7 @@ public class TushareCrawler { } else { // 交叉检查 - final List stockInfos = stockInfoDao.getStockListByListStatus(null); + final List stockInfos = stockInfoService.list(); for(StockInfo stockInfo : stockInfos) { StockMarket exchange = stockInfo.getExchange(); List missingDates = iTradeDate.getAllMissingDates(stockInfo); @@ -320,7 +276,7 @@ public class TushareCrawler { // = 2022-12-06 两天,成为范围以后将会变成更新 start 和 end 之间 // 的所有数据,这样将会大大增加不必要的更新请求。 - long dataBetween = stockCalendarDao.countOpenDaysBetween(exchange, start, end); + long dataBetween = stockCalendarService.countOpenDaysBetween(exchange, start, end); long dataPerDay = 1; if (stockSpan.compareTo(StockSpan.Daily) == -1 && stockSpan.compareTo(StockSpan.Minute) >= 0) { // 分钟数,重新算下 dataBetween @@ -329,7 +285,7 @@ public class TushareCrawler { dataBetween *= dataPerDay; long daySpan = singleMax / dataPerDay; - LocalDateTime tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan); + LocalDateTime tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan); while (dataBetween > singleMax) { rq.addDateParam("end_date", tmpEndDate); executeResult.add(es.submit(new TushareResponseCallable(rq.clone(), afterRespFunc) @@ -339,7 +295,7 @@ public class TushareCrawler { while (i < maxI && start.isBefore(missingDates.get(i + 1))) { start = missingDates.get(++i); } - tmpEndDate = stockCalendarDao.getOpenDateOffset(exchange, start, daySpan); + tmpEndDate = stockCalendarService.getOpenDateOffset(exchange, start, daySpan); rq.addDateParam("start_date", start); dataBetween -= daySpan * dataPerDay; } @@ -357,471 +313,4 @@ public class TushareCrawler { } return executeResult; } - - - - /** - * 股票列表
      - * 更新股票列表
      - * 接口:stock_basic,可以通过数据工具调试和查看数据
      - * 描述:获取基础信息数据,包括股票代码、名称、上市日期、退市日期等
      - * 积分:2000积分起
      - * 包括上市、退市和暂停上市,无权限(积分不足)状态下每小时最多访问该接口 1 次 - */ - public boolean updateStockList() { - try { - List stockInfos = new ArrayList<>(); - // Tushare 经常改请求参数的规则,2024/11/13 更新:默认不提供 list_status 时,默认值是 L - TushareRequestBody requestBody = new TushareRequestBody("stock_basic") - .addFields("ts_code,name,area,industry,market,list_status,list_date,delist_date"); - - requestBody.addParam("list_status", "L"); - stockInfos.addAll(TushareClient.queryList(requestBody)); - - requestBody.addParam("list_status", "D"); - stockInfos.addAll(TushareClient.queryList(requestBody)); - - requestBody.addParam("list_status", "P"); - stockInfos.addAll(TushareClient.queryList(requestBody)); - - stockInfoDao.insertOrUpdateList(stockInfos); - - log.info("更新股票列表完成"); - return true; - } catch (JSONException | IOException e) { - log.error("更新股票列表时发生错误", e); - return false; - } - } - - /** - * 同花顺概念和行业指数
      - * 接口:ths_index
      - * 描述:获取同花顺板块指数。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。
      - * 限量:本接口需获得600积分,单次最大5000,一次可提取全部数据,请勿循环提取。 - * @return - */ - public boolean updateThsList() { - try { - List thsIndexes = TushareClient.queryList(new TushareRequestBody("ths_index")); - stockThsListDao.insertOrUpdateList(thsIndexes); - log.info("更新同花顺板块列表完成"); - return true; - } catch (Exception e) { - log.error("更新同花顺板块列表时发生错误", e); - return false; - } - } - - /** - * 同花顺概念板块成分
      - * 接口:ths_member
      - * 描述:获取同花顺概念板块成分列表注:数据版权归属同花顺,如做商业用途,请主动联系同花顺。
      - * 限量:用户积累5000积分可调取,可按概念板块代码循环提取所有成分
      - */ - public void updateThsMember() { - TushareRequestBody baseRequest = new TushareRequestBody("ths_member").addFields("ts_code,con_code,con_name,weight,in_date,out_date,is_new"); - Function, Boolean> function = (t) -> { - stockThsMemberDao.insertOrUpdateList(t); - return true; - }; - List stockInfoList = stockThsListDao.listByExchange(null); - while (true) { - List> executeResult = rollingQueryByStock(baseRequest, stockInfoList, null, function, 5000L, QueryWay.ByStock, null); - - stockInfoList.clear(); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = result.getRequest(); - if (result.isFatal()) { - log.error("同花顺概念板块成分数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); - } - else if (!result.isSuccess()) { - log.warn("同花顺概念板块成分数据未获取:{}", request.toJSONString()); - stockInfoList.add(request.getJSONObject("params").to(ThsStockInfo.class)); - } - } catch (InterruptedException | ExecutionException e) { - log.error("同花顺概念板块成分数据执行结果时发生错误", e); - } - } - if (stockInfoList.size() == 0) { - log.info("同花顺概念板块成分更新完成"); - break; - } - log.info("重新获取未获取成功的同花顺概念板块成分"); - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - - /** - * 同花顺板块指数行情
      - * 接口:ths_daily
      - * 描述:获取同花顺板块指数行情。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。
      - * 限量:单次最大3000行数据,可根据指数代码、日期参数循环提取。
      - * @param queryWay - */ - public void updateThsDaily(QueryWay queryWay) { - TushareRequestBody baseRequest = new TushareRequestBody("ths_daily").addFields("ts_code,trade_date,close,open,high,low,pre_close,avg_price,change,pct_change,vol,turnover_rate,total_mv,float_mv"); - Function, Boolean> function = (t) -> { - stockThsDailyDao.insertOrUpdateList(t); - return true; - }; - - List> executeResult; - if (queryWay.compareTo(QueryWay.ByStock) >= 0) { - executeResult = rollingQueryByStock(baseRequest, stockThsListDao.listByExchange(ThsStockMarket.A), stockThsDailyDao, function, 3000L, queryWay, null); - } - else { - executeResult = rollingQueryByDate(baseRequest, stockThsDailyDao, function, queryWay, THS_DAILY_BEGIN_LOCALDATE); - } - log.info("同花顺板块指数行情更新完成"); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = result.getRequest(); - if (result.isFatal()) { - log.error("同花顺板块指数行情数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); - } - else if (!result.isSuccess()) { - log.warn("同花顺板块指数行情数据未获取:{}", request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("同花顺板块指数行情数据执行结果时发生错误", e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - - /** - * 更新日K数据
      - * 基础积分每分钟内最多调取500次,每次5000条数据,相当于23年历史,用户获得超过5000积分正常调取无频次限制。 - *
      - * 请先更新交易日历和股票列表后再调用该方法,否则可能造成股票日 K 数据缺失 - * @param queryWay 滚动查询方式 - * @see TushareCrawler#rollingQueryByDate - * @see TushareCrawler#rollingQueryByStock - */ - public void updateStockDaily(QueryWay queryWay) { - TushareRequestBody baseRequest = new TushareRequestBody("daily") - .addFields("ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"); - Function, Boolean> function = (t) -> { - stockDailyDao.insertOrUpdateList(t); - return true; - }; - List> executeResult; - if (queryWay.compareTo(QueryWay.ByStock) >= 0) { - executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockDailyDao, function, 5000L, queryWay, null); - } - else { - executeResult = rollingQueryByDate(baseRequest, stockDailyDao, function, queryWay, null); - } - log.info("日 K 数据更新完成"); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = f.get().getRequest(); - if (result.isFatal()) { - log.error("日 K 数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); - } - else if (!result.isSuccess()) { - log.warn("日 K 数据未获取:{}", request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("检查日 K 数据执行结果时发生错误", e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - /** - * 更新分钟K数据
      - *

      有权限时,每分钟500次,每次8000行数据,总量不限制

      - * 请先更新交易日历和股票列表后再调用该方法,否则可能造成股票分钟 K 数据缺失 - * @param queryWay 滚动查询方式,仅支持 StockSpan.ByStock... 系列 - * @param stockSpan 股票粒度,仅支持 StockSpan.Minute - StockSpan.SixtyMinute - * @see TushareCrawler#rollingQueryByDate - * @see TushareCrawler#rollingQueryByStock - * @see link.at17.mid.tushare.enums.StockSpan - */ - public void updateStockMinValue(QueryWay queryWay, StockSpan stockSpan) { - Assert.isTrue(Objects.nonNull(stockSpan), "stockSpan 不允许为空"); - Assert.isTrue(Objects.nonNull(stockSpan.getMin()), "不支持的 StockSpan:" + stockSpan + ", 仅支持分钟数据类型"); - Assert.isTrue(queryWay.compareTo(QueryWay.ByStock) >= 0, "不支持的 QueryWay!"); - String freq = stockSpan.getMin() + "min"; - TushareRequestBody baseRequest = new TushareRequestBody("stk_mins") - .addFields("ts_code,trade_time,open,close,high,low,vol,amount") - .addParam("freq", freq); - Function, Boolean> function = (t) -> { - stockMinuteDao.insertOrUpdateList(stockSpan, t); - return true; - }; - // 特殊重写:获取最新日期和所有交易日,该重写仅对更新数据生效 - ITsTradeDate minTradeDate = new ITsTradeDate() { - @Override - public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) { - return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo); - } - @Override - public List getAllTradeDates(ITsStockInfo stockInfo) { - return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo); - } - @Override - public List getAllMissingDates(ITsStockInfo stockInfo) { - return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo); - } - }; - List> executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), minTradeDate, function, 8000L, queryWay, stockSpan); - log.info("{} 分钟 K 数据更新完成", stockSpan.getMin()); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = f.get().getRequest(); - if (result.isFatal()) { - log.error("{} 分钟 K 数据未获取:{},发生致命错误,将不会重试:{}", stockSpan.getMin(), request.toJSONString()); - } - else if (!result.isSuccess()) { - log.warn("{} 分钟 K 数据未获取:{}", stockSpan.getMin(), request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("检查 {} 分钟 K 数据执行结果时发生错误\r\n{}", stockSpan.getMin(), e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - - /** - * 获取个股每日指标 - *
      - * 接口:daily_basic
      - * 更新时间:交易日每日15点~17点之间
      - * 描述:获取全部股票每日重要的基本面指标,可用于选股分析、报表展示等。
      - * 积分:用户需要至少600积分才可以调取,具体请参阅 - * 积分获取办法
      - *
      - */ - public void updateDailyBasic(QueryWay queryWay) { - TushareRequestBody baseRequest = new TushareRequestBody("daily_basic"); - baseRequest.addFields("ts_code,trade_date,close,turnover_rate,turnover_rate_f,volume_ratio,pe,pe_ttm,pb,ps,ps_ttm,dv_ratio,dv_ttm,total_share,float_share,free_share,total_mv,circ_mv"); - Function, Boolean> function = (t) -> { - stockDailyBasicDao.insertOrUpdateList(t); - return true; - }; - List> executeResult; - if (queryWay.compareTo(QueryWay.ByStock) >= 0) { - executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockDailyBasicDao, function, 5000L, queryWay, null); - } - else { - executeResult = rollingQueryByDate(baseRequest, stockDailyBasicDao, function, queryWay, null); - } - log.info("每日指标数据更新完成"); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = f.get().getRequest(); - if (result.isFatal()) { - log.error("每日指标数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); - } - else if (!result.isSuccess()) { - log.warn("每日指标数据未获取:{}", request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("检查每日指标数据执行结果时发生错误", e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - - /** - * Tushare 复权因子
      - *
      - * 接口:adj_factor
      - * 更新时间:早上9点30分
      - * 描述:获取股票复权因子,可提取单只股票全部历史复权因子,也可以提取单日全部股票的复权因子。
      - *
      - * 虽然文档没说,但当这个接口请求单支股的所有数据时,也是 5000 条限制
      - * 请先更新交易日历和股票列表后再调用该方法,否则可能造成股票复权因子缺失 - */ - public void updateStockAdjustTushare(QueryWay queryWay) { - - TushareRequestBody baseRequest = new TushareRequestBody("adj_factor"); - Function, Boolean> function = (t) -> { - stockAdjustDao.insertOrUpdateListTushare(t); - return true; - }; - List> executeResult; - if (queryWay.compareTo(QueryWay.ByStock) >= 0) { - executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockAdjustDao, function, 5000L, queryWay, null); - } - else { - executeResult = rollingQueryByDate(baseRequest, stockAdjustDao, function, queryWay, null); - } - log.info("复权数据更新完成"); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = f.get().getRequest(); - if (result.isFatal()) { - log.error("复权数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); - } - else if (!result.isSuccess()) { - log.info("复权数据未获取:{}", request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("检查复权数据执行结果时发生错误", e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - - /** - * Tushare 涨跌停列表
      - *
        - *
      • 接口:limit_list_d
        - *
      • 描述:获取沪深A股每日涨跌停、炸板数据情况,数据从2020年开始
        - *
      • 限量:单次最大可以获取500条数据,可通过日期或者股票循环提取
        - *
      • 积分:120积分可查看数据,5000积分每分钟可以请求200次,8000积分以上每分钟500次,具体请参阅积分获取办法
        - *
      - */ - public void updateStockLimit(QueryWay queryWay) { - - TushareRequestBody baseRequest = new TushareRequestBody("limit_list_d"); - Function, Boolean> function = (t) -> { - stockLimitDao.insertOrUpdateListTushare(t); - return true; - }; - List> executeResult; - if (queryWay.compareTo(QueryWay.ByStock) >= 0) { - executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockLimitDao, function, 500L, queryWay, null); - } - else { - executeResult = rollingQueryByDate(baseRequest, stockLimitDao, function, queryWay, LocalDateTime.of(2019, 11, 28, 0, 0, 0)); - } - log.info("涨跌停数据更新完成"); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = f.get().getRequest(); - if (result.isFatal()) { - log.error("涨跌停数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); - } - else if (!result.isSuccess()) { - log.info("涨跌停数据未获取:{}", request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("检查涨跌停数据执行结果时发生错误", e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - /** - * 前十大股东/前十大流通股东
      - *
      - * 接口:top10_holders/top10_floatholders
      - * 更新时间:不定时,报告日
      - * 描述:获取上市公司前十大股东数据,包括持有数量和比例等信息/获取上市公司前十大流通股东数据,不包括比例信息
      - *
      - * 文档说单次 100 条限制,但实际测试有单次 5000 条
      - * 请先更新交易日历和股票列表后再调用该方法
      - * 如果是 A+H 或者 A+B 股,可能存在同股票存在多个同名股东,且持股数据不一致。需要仔细研究如何清洗和使用 - */ - public void updateStockHolder(StockHolderType holderType) { - - boolean isFloat = holderType.getIsFloat() == 1; - TushareRequestBody baseRequest = new TushareRequestBody(isFloat ? "top10_floatholders" : "top10_holders"); - Function, Boolean> function = (t) -> { - Map map = new HashMap<>(); - for (JSONObject jo : t) { - String holderName = jo.getString("holder_name"); - String[] temp = {jo.getString("ts_code"), jo.getString("end_date"), holderName, holderType.name()}; - String key = String.join("_", temp); - Integer holderOffset = 1; - while (map.containsKey(key)) { - log.warn("存在重复的 key {},开始重命名", key); - temp[2] = holderName + '^' + holderOffset++; - jo.put("holder_name", temp[2]); - key = String.join("_", temp); - } - map.put(key, jo); - } - stockHolderDao.insertOrUpdateListTushare(t, holderType); - return true; - }; - List> executeResult; - executeResult = rollingQueryByStock(baseRequest, stockInfoDao.getStockListByListStatus(null), stockHolderDao, function, 5000L, QueryWay.ByStock, null); - String banner = "十大" + (isFloat ? "流通股东" : "股东"); - log.info("{}数据更新完成", banner); - for (Future f : executeResult) { - try { - TushareCrawlerResult result = f.get(); - JSONObject request = f.get().getRequest(); - if (result.isFatal()) { - log.error("{}数据未获取,发生致命错误,将不会重试:{}", banner, request.toJSONString()); - } - else if (!result.isSuccess()) { - log.info("{}数据未获取:{}", banner, request.toJSONString()); - } - } catch (InterruptedException | ExecutionException e) { - log.error("检查" + banner + "数据执行结果时发生错误", e); - } - } - if (baseRequest.hasFingerprint()) { - redis.getBucket(baseRequest.getFingerprint()).delete(); - } - } - - /** - * 更新交易日历 - *

      - * 更新当前交易日历到本年度的最后一天 - *

      - * @param stockMarkets 市场类型,若未提供则默认更新上交所和深交所 - */ - public boolean updateStockCalendar(StockMarket...stockMarkets) { - LocalDate lastDayOfThisYear = LocalDateTimeUtils.getLastDayOfThisYear(); - if (stockMarkets == null || stockMarkets.length == 0) { - stockMarkets = new StockMarket[] {StockMarket.SZ, StockMarket.SH}; - } - for (StockMarket stockMarket : stockMarkets) { - - log.debug("正在更新 {} 交易所的交易日历", stockMarket); - - LocalDate latest = stockCalendarDao.getLatestLocalDate(stockMarket); - if (latest != null && latest.equals(lastDayOfThisYear)){ - log.debug("{} 交易所的日历已更新到本年度最后一日,跳过", stockMarket); - continue; - } - TushareRequestBody rq = new TushareRequestBody("trade_cal") - .addParam("exchange", stockMarket.getExchangeCode()) - .addDateParam("end_date", lastDayOfThisYear) - .addFields("exchange,cal_date,is_open,pretrade_date"); - if (latest != null) { - rq.addDateParam("start_date", latest); - } - try { - List list = TushareClient.queryList(rq); - stockCalendarDao.insertOrUpdateList(list); - } catch (JSONException e) { - log.error("JSONException", e); - return false; - } catch (IOException e) { - log.error("IOException", e); - return false; - } - } - return true; - } } diff --git a/src/main/java/link/at17/mid/tushare/data/models/StockInfo.java b/src/main/java/link/at17/mid/tushare/data/models/StockInfo.java index e22d8e0..8bf0243 100644 --- a/src/main/java/link/at17/mid/tushare/data/models/StockInfo.java +++ b/src/main/java/link/at17/mid/tushare/data/models/StockInfo.java @@ -7,6 +7,7 @@ import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; import link.at17.mid.tushare.enums.ListStatus; @@ -16,6 +17,7 @@ import lombok.experimental.Accessors; @Data @Accessors(chain=true) +@TableName("stock_info") public class StockInfo implements ITsStockInfo { private static final Pattern EM_CODE_PATTERN = Pattern.compile("^(0|1|90)\\.(.*?)$"); diff --git a/src/main/java/link/at17/mid/tushare/data/models/UpdateLog.java b/src/main/java/link/at17/mid/tushare/data/models/UpdateLog.java new file mode 100644 index 0000000..4f21d6c --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/models/UpdateLog.java @@ -0,0 +1,8 @@ +package link.at17.mid.tushare.data.models; + +import lombok.Data; + +@Data +public class UpdateLog { + +} diff --git a/src/main/java/link/at17/mid/tushare/data/models/UpdateMethodInfo.java b/src/main/java/link/at17/mid/tushare/data/models/UpdateMethodInfo.java new file mode 100644 index 0000000..4168095 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/models/UpdateMethodInfo.java @@ -0,0 +1,48 @@ +package link.at17.mid.tushare.data.models; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonView; + +import link.at17.mid.tushare.data.typehandler.JsonListTypeHandler; +import link.at17.mid.tushare.system.util.EncryptUtils; +import lombok.Data; + +@Data +public class UpdateMethodInfo { + + private String methodName; + + private String declaringClassName; + + private String name; + + @TableField(typeHandler = JsonListTypeHandler.class) + private List params = new ArrayList<>(); + + private String id; + + public String getId() { + if (StringUtils.isBlank(id) && !StringUtils.isAnyBlank(methodName, declaringClassName)) { + id = EncryptUtils.sha256(declaringClassName + '$' + methodName); + } + return id; + } + + @Data + public static class UpdateParamInfo { + private String fullTypeName; + private String typeName; + private String name; + @JsonIgnore + private Class typeClass; + private List allowedEnumValues; + private Object value; + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/models/UpdatePlan.java b/src/main/java/link/at17/mid/tushare/data/models/UpdatePlan.java new file mode 100644 index 0000000..f43f656 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/models/UpdatePlan.java @@ -0,0 +1,72 @@ +package link.at17.mid.tushare.data.models; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.validation.annotation.Validated; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import link.at17.mid.tushare.data.typehandler.UpdateMethodInfoListTypeHandler; +import link.at17.mid.tushare.data.validator.ValidCron; +import link.at17.mid.tushare.data.validator.ValidUpdateMethodInfo; +import lombok.Data; + +@Data +@TableName(value="update_plan", autoResultMap = true) // 使用自定义类型转换器必须使 autoResultMap = true +@NotNull +@Validated +public class UpdatePlan { + + /** + * id + */ + @TableId(type=IdType.AUTO) + private Integer id; + + /** + * 计划任务表达式 + */ + @ValidCron + @NotBlank + private String cronExpr; + + /** + * 计划名称 + */ + @NotBlank + private String name; + + /** + * 计划内方法列表 + */ + @TableField(typeHandler=UpdateMethodInfoListTypeHandler.class) + @NotEmpty + @Valid + private List<@ValidUpdateMethodInfo UpdateMethodInfo> methods = new ArrayList<>(); + + /** + * 是否进行交易日检查 + */ + @NotNull + private Boolean openDayCheck = false; + + /** + * 是否启用 + */ + @NotNull + private Boolean enabled = true; + + /** + * 是否有效 + */ + @TableField(exist=false) + private Boolean valid = true; + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockAdjustService.java b/src/main/java/link/at17/mid/tushare/data/service/StockAdjustService.java new file mode 100644 index 0000000..1c5ebc2 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockAdjustService.java @@ -0,0 +1,96 @@ +package link.at17.mid.tushare.data.service; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockAdjustDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockAdjustService { + + @Autowired + StockInfoService stockInfoService; + + @Autowired + StockAdjustDao stockAdjustDao; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + RedissonClient redis; + + /** + * 用于插入从 Tushare 获取的内容 + * @param list + */ + public void insertOrUpdateList(List list) { + stockAdjustDao.insertOrUpdateList(list); + } + + + /** + * Tushare 复权因子
      + *
      + * 接口:adj_factor
      + * 更新时间:早上9点30分
      + * 描述:获取股票复权因子,可提取单只股票全部历史复权因子,也可以提取单日全部股票的复权因子。
      + *
      + * 虽然文档没说,但当这个接口请求单支股的所有数据时,也是 5000 条限制
      + * 请先更新交易日历和股票列表后再调用该方法,否则可能造成股票复权因子缺失 + */ + @UpdateMethod(name="股票复权因子", order=4) + public boolean updateData( + @AllowedEnum({"ByDateUpdate", "ByStockCrossCheck"}) + QueryWay queryWay) { + + TushareRequestBody baseRequest = new TushareRequestBody("adj_factor"); + Function, Boolean> function = (t) -> { + stockAdjustDao.insertOrUpdateList(t); + return true; + }; + List> executeResult; + if (queryWay.compareTo(QueryWay.ByStock) >= 0) { + executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), stockAdjustDao, function, 5000L, queryWay, null); + } + else { + executeResult = tushareCrawler.rollingQueryByDate(baseRequest, stockAdjustDao, function, queryWay, null); + } + log.info("复权数据更新完成"); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = f.get().getRequest(); + if (result.isFatal()) { + log.error("复权数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); + } + else if (!result.isSuccess()) { + log.info("复权数据未获取:{}", request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("检查复权数据执行结果时发生错误", e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockCalendarService.java b/src/main/java/link/at17/mid/tushare/data/service/StockCalendarService.java new file mode 100644 index 0000000..5722681 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockCalendarService.java @@ -0,0 +1,350 @@ +package link.at17.mid.tushare.data.service; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.Temporal; +import java.util.List; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockCalendarDao; +import link.at17.mid.tushare.data.crawler.tushare.TushareClient; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockCalendar; +import link.at17.mid.tushare.enums.StockMarket; +import link.at17.mid.tushare.service.BaseServiceImpl; +import link.at17.mid.tushare.system.util.LocalDateTimeUtils; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockCalendarService extends BaseServiceImpl { + + private static final StockMarket[] UPDATE_SUPPORTED_STOCK_MARKETS = new StockMarket[] {StockMarket.SZ, StockMarket.SH}; + + /** + * 交易日历
      + * 更新交易日历 + *

      + * 描述:获取各大交易所交易日历数据,默认提取的是上交所
      + * 积分:需2000积分 + *

      + * @param stockMarkets 市场类型,若未提供则默认更新上交所和深交所 + */ + @UpdateMethod(name="交易日历", order=0) + public boolean updateData() { + + LocalDate lastDayOfThisYear = LocalDateTimeUtils.getLastDayOfThisYear(); + + for (StockMarket stockMarket : UPDATE_SUPPORTED_STOCK_MARKETS) { + + log.debug("正在更新 {} 交易所的交易日历", stockMarket); + + LocalDate latest = getLatestLocalDate(stockMarket); + if (latest != null && latest.equals(lastDayOfThisYear)){ + log.debug("{} 交易所的日历已更新到本年度最后一日,跳过", stockMarket); + continue; + } + TushareRequestBody rq = new TushareRequestBody("trade_cal") + .addParam("exchange", stockMarket.getExchangeCode()) + .addDateParam("end_date", lastDayOfThisYear) + .addFields("exchange,cal_date,is_open,pretrade_date"); + if (latest != null) { + rq.addDateParam("start_date", latest); + } + try { + List list = TushareClient.queryList(rq); + baseMapper.insertOrUpdateList(list); + } catch (JSONException e) { + log.error("JSONException", e); + return false; + } catch (IOException e) { + log.error("IOException", e); + return false; + } + } + return true; + } + + /** + * 插入从 Tushare 获取的数据 + * @param list + * @return 插入是否成功 + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(baseMapper.insertOrUpdateList(list)); + } + + /** + * 获取指定证交所两个日期(含)之间的交易日个数
      + *

      + * 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate}
      + * 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒 + *

      例: + *

      • + * startDate : LocalDateTime 2021-08-16 01:20:30
        + * endDate   : LocalDateTime 2021-08-20 19:20:30
        + * 返回 5 + *
      • countOpenDaysBetween(StockMarket.SH, startDate, endDate) 其中
        + * startDate : LocalDateTime 2021-08-16 01:20:30
        + * endDate   : LocalDateTime 2021-08-16 12:34:56
        + * 返回 1 + *
      • countOpenDaysBetween(StockMarket.SH, startDate, endDate) 其中
        + * startDate : LocalDateTime 2021-08-15 01:20:30
        + * endDate   : LocalDateTime 2021-08-16 12:34:56
        + * 返回 1 + *
      + *

      + * @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重计算 + * @param startDate 开始日期 + * @param endDate 结束日期 + * @return + + */ + public long countOpenDaysBetween(StockMarket exchange, Temporal startDate, Temporal endDate) { + return baseMapper.countOpenDaysBetween(exchange, startDate, endDate); + } + + /** + * 获取指定证交所两个日期(含)之间的所有交易日
      + * 存储在数据库内的类型为无时分秒的 timestamp 类型,可视作 {@code java.time.LocalDate}
      + * 若传入的是 java.time.LocalDateTime 类型,会忽略时分秒 + *

      例: + *

      • countOpenDaysBetween(StockMarket.SH, startDate, endDate) 其中
        + * startDate : LocalDateTime 2021-08-16 01:20:30
        + * endDate  : LocalDateTime 2021-08-20 19:20:30
        + *

        + *

        + * 返回: + *

          + *
        • 2021-08-16 + *
        • 2021-08-17 + *
        • 2021-08-18 + *
        • 2021-08-19 + *
        • 2021-08-20 + *
        + *

        + * @param exchange 股市类型枚举,若提供的枚举不为 SZ 或 SH,则采用 SZ + SH 的所有交易日并去重获得结果 + * @param startDate 开始日期(含) + * @param endDate 结束日期(含) + * @return + + */ + public List getAllOpenDatesBetween(StockMarket exchange, Temporal startDate, Temporal endDate){ + return baseMapper.getAllOpenDatesBetween(exchange, startDate, endDate); + } + + /** + * 获取指定证交所指定日期偏移的指定交易日 + * @param exchange + * @param date + * @param offset + * @return + */ + public LocalDateTime getOpenDateOffset(StockMarket exchange, Temporal date, long offset) { + return baseMapper.getOpenDateOffset(exchange, date, offset); + } + + + /** + * 获取指定证交所指定日期(含)以后的所有交易日 + * @param exchange + * @param sence 留空则查询所有 + * @return + */ + public List getAllOpenDateSence(StockMarket exchange, @Nullable Temporal sence) { + return baseMapper.getAllOpenDateSence(exchange, sence); + } + + /** + * 判断指定日是否为指定交易所的交易日 + * @param date 会截断时分秒,只取年月日 + * @param exchange 目前只支持 SSE(深交所)和 SHSE(上交所) + * @return + * @see #isOpen(Temporal) + */ + public boolean isOpen(@NonNull Temporal date, @Nullable StockMarket exchange) { + return baseMapper.isOpen(date, exchange); + } + + /** + * 判断指定日是否为任意交易所的交易日 + * @param date 会截断时分秒,只取年月日 + * @return + * @see #isOpen(Temporal, StockMarket) + */ + public boolean isOpen(@NonNull Temporal date) { + return baseMapper.isOpen(date, null); + } + + /** + * 获取数据库内指定证交所的最早一个交易日历 + *

        仅为日期,并未指定是否是开市日 + * @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定) + * @return + */ + public StockCalendar getGreatest(@Nullable StockMarket stockMarket) { + return baseMapper.getGreatest(stockMarket); + } + + /** + * 获取数据库内指定证交所的最早一个交易日历 + *

        仅为日期,并未指定是否是开市日。取最早一条(市场不确定) + * @see #getGreatest(StockMarket) + * @return + */ + public StockCalendar getGreatest() { + return getGreatest(null); + } + + /** + * 获取数据库内指定证交所的最早一个交易日历的日期 + *

        仅为日期,并未指定是否是开市日。取最早一条(市场不确定) + * @see #getGreatest + * @return {@code LocalDate} 或 {@code null} + */ + public LocalDate getGreatestLocalDate() { + return getGreatestLocalDate(null); + } + + /** + * 获取数据库内指定证交所的最早一个交易日历的日期 + *

        仅为日期,并未指定是否是开市日 + * @see #getGreatest + * @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定) + * @return {@code LocalDate} 或 {@code null} + */ + public LocalDate getGreatestLocalDate(@Nullable StockMarket stockMarket) { + StockCalendar stockCalendar = getGreatest(stockMarket); + if (stockCalendar != null) { + return stockCalendar.getDate().atStartOfDay().toLocalDate(); + } + return null; + } + + /** + * 获取数据库内指定证交所的最早一个交易日历的日期时间 + *

        仅为日期,并未指定是否是开市日。取最早一条(市场不确定) + * @see #getGreatest + * @return {@code LocalDate} 或 {@code null} + */ + public LocalDateTime getGreatestLocalDateTime() { + return getGreatestLocalDateTime(null); + } + + /** + * 获取数据库内指定证交所的最早一个交易日历的日期时间 + *

        仅为日期,并未指定是否是开市日 + * @see #getGreatest + * @param stockMarket 指定证交所,若为 {@code null} 则取最早一条(市场不确定) + * @return {@code LocalDateTime} 或 {@code null} + */ + public LocalDateTime getGreatestLocalDateTime(@Nullable StockMarket stockMarket) { + StockCalendar stockCalendar = getGreatest(stockMarket); + if (stockCalendar != null) { + return stockCalendar.getDate().atStartOfDay(); + } + return null; + } + + + /** + * 获取数据库内指定证交所的最新一个交易日历 + *

        仅为日期,并未指定是否是开市日 + * @param stockMarket 指定证交所,若为 {@code null} 则取最新一条(市场不确定) + * @return + */ + public StockCalendar getLatest(@Nullable StockMarket stockMarket) { + return baseMapper.getLatest(stockMarket); + } + + /** + * 获取数据库内指定证交所的最近一个交易日历 + *

        仅为日期,并未指定是否是开市日。取最近一条(市场不确定) + * @see #getLatest(StockMarket) + * @return + */ + public StockCalendar getLatest() { + return getLatest(null); + } + + + /** + * 获取数据库内指定证交所的最近一个交易日历的日期 + *

        仅为日期,并未指定是否是开市日 + * @see #getGreatest + * @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定) + * @return {@code LocalDate} 或 {@code null} + */ + public LocalDate getLatestLocalDate(@Nullable StockMarket stockMarket) { + StockCalendar stockCalendar = getLatest(stockMarket); + if (stockCalendar != null) { + return stockCalendar.getDate().atStartOfDay().toLocalDate(); + } + return null; + } + + /** + * 获取数据库内指定证交所的最近一个交易日历的日期 + *

        仅为日期,并未指定是否是开市日。取最近一条(市场不确定) + * @see #getGreatest + * @return {@code LocalDate} 或 {@code null} + */ + public LocalDate getLatestLocalDate() { + return getLatestLocalDate(null); + } + + /** + * 获取数据库内指定证交所的最近一个交易日历的日期时间 + *

        仅为日期时间,并未指定是否是开市日 + * @see #getGreatest + * @param stockMarket 指定证交所,若为 {@code null} 则取最近一条(市场不确定) + * @return {@code LocalDateTime} 或 {@code null} + */ + public LocalDateTime getLatestLocalDateTime(@Nullable StockMarket stockMarket) { + StockCalendar stockCalendar = getLatest(stockMarket); + if (stockCalendar != null) { + return stockCalendar.getDate().atStartOfDay(); + } + return null; + } + + + /** + * 获取数据库内指定证交所的最近一个交易日历的日期时间 + *

        仅为日期时间,并未指定是否是开市日。取最近一条(市场不确定) + * @see #getGreatest + * @return {@code LocalDateTime} 或 {@code null} + */ + public LocalDateTime getLatestLocalDateTime() { + return getLatestLocalDateTime(null); + } + + /** + * 查询今日是否开市,任意市场开市都返回 true + * @see #todayIsOpen(StockMarket) + * @return + */ + public boolean todayIsOpen() { + return baseMapper.isOpen(LocalDateTime.now(), null); + } + + + /** + * 查询今日是否是开市日 + * @param stockMarket 指定市场,为 null 则任意市场开市都返回 true + * @return + */ + public boolean todayIsOpen(StockMarket stockMarket) { + return baseMapper.isOpen(LocalDateTime.now(), stockMarket); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockDailyBasicService.java b/src/main/java/link/at17/mid/tushare/data/service/StockDailyBasicService.java new file mode 100644 index 0000000..0646d24 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockDailyBasicService.java @@ -0,0 +1,114 @@ +package link.at17.mid.tushare.data.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockDailyBasicDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockValueEx; +import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; +import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import link.at17.mid.tushare.service.BaseServiceImpl; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockDailyBasicService extends BaseServiceImpl implements ITsTradeDate { + + @Autowired + StockInfoService stockInfoService; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + RedissonClient redis; + + /** + * 更新由 Tushare 获取的数据 + * @param list + * @return + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(baseMapper.insertOrUpdateList(list)); + } + + @Override + public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) { + return baseMapper.getLatestTradeDate(stockInfo); + } + + @Override + public List getAllTradeDates(ITsStockInfo stockInfo) { + return baseMapper.getAllTradeDates(stockInfo); + } + + @Override + public List getAllMissingDates(ITsStockInfo stockInfo) { + return baseMapper.getAllMissingDates(stockInfo); + } + + /** + * 获取个股每日指标 + *

        + * 接口:daily_basic
        + * 更新时间:交易日每日15点~17点之间
        + * 描述:获取全部股票每日重要的基本面指标,可用于选股分析、报表展示等。
        + * 积分:用户需要至少600积分才可以调取,具体请参阅 + * 积分获取办法
        + *
        + */ + @UpdateMethod(name="每日基本指标", order=3) + public boolean updateData( + @AllowedEnum({"ByDateUpdate", "ByStockCrossCheck"}) + QueryWay queryWay) { + TushareRequestBody baseRequest = new TushareRequestBody("daily_basic"); + baseRequest.addFields("ts_code,trade_date,close,turnover_rate,turnover_rate_f,volume_ratio,pe,pe_ttm,pb,ps,ps_ttm,dv_ratio,dv_ttm,total_share,float_share,free_share,total_mv,circ_mv"); + Function, Boolean> function = (t) -> { + insertOrUpdateList(t); + return true; + }; + List> executeResult; + if (queryWay.compareTo(QueryWay.ByStock) >= 0) { + executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), this, function, 5000L, queryWay, null); + } + else { + executeResult = tushareCrawler.rollingQueryByDate(baseRequest, this, function, queryWay, null); + } + log.info("每日指标数据更新完成"); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = f.get().getRequest(); + if (result.isFatal()) { + log.error("每日指标数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); + } + else if (!result.isSuccess()) { + log.warn("每日指标数据未获取:{}", request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("检查每日指标数据执行结果时发生错误", e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockDailyService.java b/src/main/java/link/at17/mid/tushare/data/service/StockDailyService.java new file mode 100644 index 0000000..4718879 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockDailyService.java @@ -0,0 +1,177 @@ +package link.at17.mid.tushare.data.service; + +import java.time.LocalDateTime; +import java.time.temporal.Temporal; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockDailyDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockValue; +import link.at17.mid.tushare.data.models.StockValueEx; +import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; +import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import link.at17.mid.tushare.service.BaseServiceImpl; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockDailyService extends BaseServiceImpl implements ITsTradeDate { + + @Autowired + StockInfoService stockInfoService; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + RedissonClient redis; + + /** + * 更新由 Tushare 获取的数据 + * @param list + * @return + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(baseMapper.insertOrUpdateList(list)); + } + + /** + * 获取除权日线数据 + 基本行情数据 + * @param tsCode Tushare 股票代码,不允许为空 + * @param endDate 结束日期(包含),留空则为最新一个交易日 + * @param before 多少个交易日以前,留空则查询上市至 endDate 以来所有数据 + * @return + */ + public List getExDailyBefore(String tsCode, Temporal endDate, Long before) { + return baseMapper.getExDailyBefore(tsCode, endDate, before); + } + /** + * 获取前复权日线数据 + * @param tsCode Tushare 股票代码,不允许为空 + * @param startDate 开始日期,留空则为股票上市日起 + * @param endDate 结束日期,null 则为最新一个交易日 + * @return + */ + public List getQfqDaily(String tsCode, Temporal startDate, Temporal endDate) { + return baseMapper.getQfqDaily(tsCode, startDate, endDate); + } + /** + * 获取前复权日线数据 + * @param tsCode Tushare 股票代码,不允许为空 + * @param endDate 结束日期,留空则为最新一个交易日 + * @param before 多少个交易日以前,null 则查询上市以来所有数据 + * @return + */ + public List getQfqDailyBefore(String tsCode, Temporal endDate, Long before) { + return baseMapper.getQfqDailyBefore(tsCode, endDate, before); + } + /** + * 获取前复权日线数据 + 基本行情数据 + * @param tsCode Tushare 股票代码,不允许为空 + * @param startDate 开始日期(包含),留空则为上市第一日 + * @param after 多少个交易日以前,null 则查询 startDate 以来所有数据 + * @return + */ + public List getExQfqDailyAfter(String tsCode, Temporal startDate, Long after) { + return baseMapper.getExQfqDailyAfter(tsCode, startDate, after); + } + /** + * 获取前复权日线数据 + 基本行情数据 + * @param tsCode Tushare 股票代码,不允许为空 + * @param endDate 结束日期,留空则为最新一个交易日 + * @param before 多少个交易日以前,null 则查询上市以来所有数据 + * @return + */ + public List getExQfqDailyBefore(String tsCode, Temporal endDate, Long before) { + return baseMapper.getExQfqDailyBefore(tsCode, endDate, before); + } + /** + * 获取前复权日线数据 + * @param tsCode Tushare 股票代码,不允许为空 + * @param startDate 开始日期(包含),留空则为上市第一日 + * @param after 多少个交易日以前,null 则查询 startDate 以来所有数据 + * @return + */ + public List getQfqDailyAfter(String tsCode, Temporal startDate, Long after) { + return baseMapper.getQfqDailyAfter(tsCode, startDate, after); + } + + @Override + public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) { + return baseMapper.getLatestTradeDate(stockInfo); + } + + @Override + public List getAllTradeDates(ITsStockInfo stockInfo) { + return baseMapper.getAllTradeDates(stockInfo); + } + + @Override + public List getAllMissingDates(ITsStockInfo stockInfo) { + return baseMapper.getAllMissingDates(stockInfo); + } + + /** + * 更新日K数据
        + * 基础积分每分钟内最多调取500次,每次5000条数据,相当于23年历史,用户获得超过5000积分正常调取无频次限制。 + *
        + * 请先更新交易日历和股票列表后再调用该方法,否则可能造成股票日 K 数据缺失 + * @param queryWay 滚动查询方式 + * @see TushareCrawler#rollingQueryByDate + * @see TushareCrawler#rollingQueryByStock + */ + + @UpdateMethod(name="日线数据", order=2) + public boolean updateData( + @AllowedEnum({"ByDateUpdate", "ByStockCrossCheck", "ByDateAll"}) + QueryWay queryWay) { + TushareRequestBody baseRequest = new TushareRequestBody("daily") + .addFields("ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount"); + Function, Boolean> function = (t) -> { + insertOrUpdateList(t); + return true; + }; + List> executeResult; + if (queryWay.compareTo(QueryWay.ByStock) >= 0) { + executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), this, function, 5000L, queryWay, null); + } + else { + executeResult = tushareCrawler.rollingQueryByDate(baseRequest, this, function, queryWay, null); + } + log.info("日 K 数据更新完成"); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = f.get().getRequest(); + if (result.isFatal()) { + log.error("日 K 数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); + } + else if (!result.isSuccess()) { + log.warn("日 K 数据未获取:{}", request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("检查日 K 数据执行结果时发生错误", e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockHolderService.java b/src/main/java/link/at17/mid/tushare/data/service/StockHolderService.java new file mode 100644 index 0000000..da19370 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockHolderService.java @@ -0,0 +1,138 @@ +package link.at17.mid.tushare.data.service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockHolderDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockHolder; +import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; +import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; +import link.at17.mid.tushare.enums.StockHolderType; +import link.at17.mid.tushare.service.BaseServiceImpl; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockHolderService extends BaseServiceImpl implements ITsTradeDate { + + @Autowired + RedissonClient redis; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + StockInfoService stockInfoService; + + + /** + * 前十大股东/前十大流通股东
        + *
        + * 接口:top10_holders/top10_floatholders
        + * 更新时间:不定时,报告日
        + * 描述:获取上市公司前十大股东数据,包括持有数量和比例等信息/获取上市公司前十大流通股东数据,不包括比例信息
        + *
        + * 文档说单次 100 条限制,但实际测试有单次 5000 条
        + * 请先更新交易日历和股票列表后再调用该方法
        + * 如果是 A+H 或者 A+B 股,可能存在同股票存在多个同名股东,且持股数据不一致。需要仔细研究如何清洗和使用 + */ + // TODO: A+H 或者 A+B 股是否还需要重命名? + @UpdateMethod(name="十大股东数据", order=10) + public boolean updateData() { + + StockHolderType[] holderTypes = new StockHolderType[] { + StockHolderType.TOP10, + StockHolderType.TOP10Float + }; + + for (StockHolderType holderType : holderTypes) { + + boolean isFloat = holderType.getIsFloat() == 1; + TushareRequestBody baseRequest = new TushareRequestBody(isFloat ? "top10_floatholders" : "top10_holders"); + Function, Boolean> function = (t) -> { + Map map = new HashMap<>(); + for (JSONObject jo : t) { + String holderName = jo.getString("holder_name"); + String[] temp = {jo.getString("ts_code"), jo.getString("end_date"), holderName, holderType.name()}; + String key = String.join("_", temp); + Integer holderOffset = 1; + while (map.containsKey(key)) { + log.warn("存在重复的 key {},开始重命名", key); + temp[2] = holderName + '^' + holderOffset++; + jo.put("holder_name", temp[2]); + key = String.join("_", temp); + } + map.put(key, jo); + } + insertOrUpdateList(t, holderType); + return true; + }; + List> executeResult; + executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), baseMapper, function, 5000L, QueryWay.ByStock, null); + String banner = "十大" + (isFloat ? "流通股东" : "股东"); + log.info("{}数据更新完成", banner); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = f.get().getRequest(); + if (result.isFatal()) { + log.error("{}数据未获取,发生致命错误,将不会重试:{}", banner, request.toJSONString()); + } + else if (!result.isSuccess()) { + log.info("{}数据未获取:{}", banner, request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("检查" + banner + "数据执行结果时发生错误", e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + } + + return true; + } + + /** + * 插入从 Tushare 抓取的内容 + * + * @param list + * @return + */ + public boolean insertOrUpdateList(List list, StockHolderType holderType) { + return SqlHelper.retBool(baseMapper.insertOrUpdateList(list, holderType)); + } + + @Override + public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) { + return baseMapper.getLatestTradeDate(stockInfo); + } + + @Override + public List getAllTradeDates(ITsStockInfo stockInfo) { + return baseMapper.getAllTradeDates(stockInfo); + } + + @Override + public List getAllMissingDates(ITsStockInfo stockInfo) { + return baseMapper.getAllMissingDates(stockInfo); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockInfoService.java b/src/main/java/link/at17/mid/tushare/data/service/StockInfoService.java new file mode 100644 index 0000000..66eced8 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockInfoService.java @@ -0,0 +1,105 @@ +package link.at17.mid.tushare.data.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.github.yulichang.query.MPJLambdaQueryWrapper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockInfoDao; +import link.at17.mid.tushare.data.crawler.tushare.TushareClient; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockInfo; +import link.at17.mid.tushare.enums.ListStatus; +import link.at17.mid.tushare.service.BaseServiceImpl; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockInfoService extends BaseServiceImpl { + + + @Override + public List list() { + return baseMapper.selectList(null); + } + + /** + * 用这个方法的话,传入的 Wrapper 需要设置别名 i + *

        + * 如果用 QueryWrapper:  + *

        + *  {@code ew.eq("i.ts_code", "000001.SZ")} + *

        + * 如果用 MPJLambdaQueryWrapper: + *

        + *  {@code ew.setAlias("i").eq(StockInfo::getTsCode, "000001.SZ")} + */ + @Override + public List list(Wrapper ew) { + return baseMapper.selectList(ew); + } + + /** + * 用于插入从 Tushare 获取的内容 + * @param list + */ + public void insertOrUpdateList(List list) { + baseMapper.insertOrUpdateList(list); + } + + /** + * 根据上市状态列举对应所有股票 + * @param listStatus + * @return + */ + public List listByListStatus(ListStatus listStatus) { + MPJLambdaQueryWrapper ew = new MPJLambdaQueryWrapper<>(); + return baseMapper.selectList(ew.setAlias("i").eq(StockInfo::getListStatus, listStatus)); + } + + /** + * 股票列表
        + * 更新股票列表
        + * 接口:stock_basic,可以通过数据工具调试和查看数据
        + * 描述:获取基础信息数据,包括股票代码、名称、上市日期、退市日期等
        + * 积分:2000积分起
        + * 包括上市、退市和暂停上市,无权限(积分不足)状态下每小时最多访问该接口 1 次 + * + * + * @param queryWay 查询方法,本例中为 null,因为更新股票列表不需要指定 QueryWay + */ + + @UpdateMethod(name="股票列表", order=1) + public boolean updateData() { + try { + List stockInfos = new ArrayList<>(); + // Tushare 经常改请求参数的规则,2024/11/13 更新:默认不提供 list_status 时,默认值是 L + TushareRequestBody requestBody = new TushareRequestBody("stock_basic") + .addFields("ts_code,name,area,industry,market,list_status,list_date,delist_date"); + + requestBody.addParam("list_status", "L"); + stockInfos.addAll(TushareClient.queryList(requestBody)); + + requestBody.addParam("list_status", "D"); + stockInfos.addAll(TushareClient.queryList(requestBody)); + + requestBody.addParam("list_status", "P"); + stockInfos.addAll(TushareClient.queryList(requestBody)); + + insertOrUpdateList(stockInfos); + + log.info("更新股票列表完成"); + return true; + } catch (JSONException | IOException e) { + log.error("更新股票列表时发生错误", e); + return false; + } + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockLimitService.java b/src/main/java/link/at17/mid/tushare/data/service/StockLimitService.java new file mode 100644 index 0000000..3d1fbaa --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockLimitService.java @@ -0,0 +1,115 @@ +package link.at17.mid.tushare.data.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockLimitDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockLimit; +import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; +import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import link.at17.mid.tushare.service.BaseServiceImpl; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockLimitService extends BaseServiceImpl implements ITsTradeDate { + + @Autowired + RedissonClient redis; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + StockInfoService stockInfoService; + + + /** + * Tushare 涨跌停列表
        + *

          + *
        • 接口:limit_list_d
          + *
        • 描述:获取沪深A股每日涨跌停、炸板数据情况,数据从2020年开始
          + *
        • 限量:单次最大可以获取500条数据,可通过日期或者股票循环提取
          + *
        • 积分:120积分可查看数据,5000积分每分钟可以请求200次,8000积分以上每分钟500次,具体请参阅积分获取办法
          + *
        + */ + @UpdateMethod(name="涨跌停数据", order=5) + public boolean updateData( + @AllowedEnum({"ByDateUpdate", "ByStockCrossCheck", "ByDateAll"}) + QueryWay queryWay) { + + TushareRequestBody baseRequest = new TushareRequestBody("limit_list_d"); + Function, Boolean> function = (t) -> { + insertOrUpdateList(t); + return true; + }; + List> executeResult; + if (queryWay.compareTo(QueryWay.ByStock) >= 0) { + executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoService.list(), this, + function, 500L, queryWay, null); + } else { + executeResult = tushareCrawler.rollingQueryByDate(baseRequest, this, function, queryWay, + LocalDateTime.of(2019, 11, 28, 0, 0, 0)); + } + log.info("涨跌停数据更新完成"); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = f.get().getRequest(); + if (result.isFatal()) { + log.error("涨跌停数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); + } else if (!result.isSuccess()) { + log.info("涨跌停数据未获取:{}", request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("检查涨跌停数据执行结果时发生错误", e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + + /** + * 插入从 Tushare 抓取的内容 + * + * @param list + * @return + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(baseMapper.insertOrUpdateList(list)); + } + + @Override + public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) { + return baseMapper.getLatestTradeDate(stockInfo); + } + + @Override + public List getAllTradeDates(ITsStockInfo stockInfo) { + return baseMapper.getAllTradeDates(stockInfo); + } + + @Override + public List getAllMissingDates(ITsStockInfo stockInfo) { + return baseMapper.getAllMissingDates(stockInfo); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/StockMinuteService.java b/src/main/java/link/at17/mid/tushare/data/service/StockMinuteService.java new file mode 100644 index 0000000..6f3baea --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/StockMinuteService.java @@ -0,0 +1,200 @@ +package link.at17.mid.tushare.data.service; + +import java.time.LocalDateTime; +import java.time.temporal.Temporal; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.StockMinuteDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.StockValue; +import link.at17.mid.tushare.data.models.StockValueEx; +import link.at17.mid.tushare.data.models.interfaces.ITsStockInfo; +import link.at17.mid.tushare.data.models.interfaces.ITsTradeDate; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import link.at17.mid.tushare.enums.StockSpan; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class StockMinuteService { + + @Autowired + StockMinuteDao stockMinuteDao; + + @Autowired + StockInfoService stockInfoService; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + RedissonClient redis; + + /** + * 更新由 Tushare 获取的数据 + * + * @param list + * @return + */ + public boolean insertOrUpdateList(List list, StockSpan stockSpan) { + return SqlHelper.retBool(stockMinuteDao.insertOrUpdateList(list, stockSpan)); + } + + /** + * 获取最新交易日
        + * 获取到股票的最新交易日 + * + * @param stockSpan 分钟线频率 + * @param stockInfo + * @return + */ + public LocalDateTime getLatestTradeDate(StockSpan stockSpan, ITsStockInfo stockInfo) { + return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo); + } + + /** + * 获取指定 freq 下的所有交易日 + * + * @param stockSpan 分钟线频率 + * @param stockInfo + * @return + */ + public List getAllTradeDates(StockSpan stockSpan, ITsStockInfo stockInfo) { + return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo); + } + + /** + * 获取指定 freq 下的数据缺失日,包括分钟数据不全日
        + * 如:60 分钟频率下,一日内 K 线数应为 240/60 + 1 = 5 条,则小于 5 条的日期都将被列为缺失日期 + * + * @param stockSpan 分钟线频率 + * @param stockInfo + * @return + */ + public List getAllMissingDates(StockSpan stockSpan, ITsStockInfo stockInfo) { + return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo); + } + + /** + * 获取前复权日线数据 + * + * @param stockCode 股票代码,不允许为空 + * @param stockSpan 分钟线频率 + * @param startDate 开始日期,留空则为股票上市日起 + * @param endDate 结束日期,留空则为最新一个交易日 + * @return + */ + public List getQfqMinute(String stockCode, StockSpan stockSpan, Temporal startDate, Temporal endDate) { + return stockMinuteDao.getQfqMinute(stockCode, stockSpan, startDate, endDate); + } + + /** + * 获取前复权日线数据 + * + * @param stockCode 股票代码,不允许为空 + * @param endDate 结束日期,留空则为最新一个交易日 + * @param before 多少个交易日以前,留空则查询上市以来所有数据 + * @return + */ + public List getQfqMinuteBefore(String stockCode, StockSpan stockSpan, Temporal endDate, Long before) { + return stockMinuteDao.getQfqMinuteBefore(stockCode, stockSpan, endDate, before); + } + + /** + * 获取前复权日线 Ex 数据 + * + * @param stockCode 股票代码,不允许为空 + * @param endDate 结束日期,留空则为最新一个交易日 + * @param before 多少个交易日以前,留空则查询上市以来所有数据 + * @return + */ + public List getExQfqMinuteBefore(String stockCode, StockSpan stockSpan, Temporal endDate, + Long before) { + return stockMinuteDao.getExQfqMinuteBefore(stockCode, stockSpan, endDate, before); + } + + /** + * 更新分钟K数据
        + *

        + * 有权限时,每分钟500次,每次8000行数据,总量不限制 + *

        + * 请先更新交易日历和股票列表后再调用该方法,否则可能造成股票分钟 K 数据缺失 + * + * @param queryWay 滚动查询方式,仅支持 StockSpan.ByStock... 系列 + * @param stockSpan 股票粒度,仅支持 StockSpan.Minute - StockSpan.SixtyMinute + * @see TushareCrawler#rollingQueryByDate + * @see TushareCrawler#rollingQueryByStock + * @see link.at17.mid.tushare.enums.StockSpan + */ + @UpdateMethod(name="分钟 K 数据", order=9) + public boolean updateData( + @AllowedEnum({"ByDateUpdate", "ByStockCrossCheck", "ByDateAll" }) + QueryWay queryWay, + @AllowedEnum({"Minute", "Minute5", "Minute15", "Minute30", "Minute60"}) + StockSpan stockSpan) { + Assert.isTrue(Objects.nonNull(stockSpan), "stockSpan 不允许为空"); + Assert.isTrue(Objects.nonNull(stockSpan.getMin()), "不支持的 StockSpan:" + stockSpan + ", 仅支持分钟数据类型"); + Assert.isTrue(queryWay.compareTo(QueryWay.ByStock) >= 0, "不支持的 QueryWay!"); + String freq = stockSpan.getMin() + "min"; + TushareRequestBody baseRequest = new TushareRequestBody("stk_mins") + .addFields("ts_code,trade_time,open,close,high,low,vol,amount").addParam("freq", freq); + Function, Boolean> function = (t) -> { + stockMinuteDao.insertOrUpdateList(t, stockSpan); + return true; + }; + // 特殊重写:获取最新日期和所有交易日,该重写仅对更新数据生效 + ITsTradeDate minTradeDate = new ITsTradeDate() { + @Override + public LocalDateTime getLatestTradeDate(ITsStockInfo stockInfo) { + return stockMinuteDao.getLatestTradeDate(stockSpan, stockInfo); + } + + @Override + public List getAllTradeDates(ITsStockInfo stockInfo) { + return stockMinuteDao.getAllTradeDates(stockSpan, stockInfo); + } + + @Override + public List getAllMissingDates(ITsStockInfo stockInfo) { + return stockMinuteDao.getAllMissingDates(stockSpan, stockInfo); + } + }; + List> executeResult = tushareCrawler.rollingQueryByStock(baseRequest, + stockInfoService.list(), minTradeDate, function, 8000L, queryWay, stockSpan); + log.info("{} 分钟 K 数据更新完成", stockSpan.getMin()); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = f.get().getRequest(); + if (result.isFatal()) { + log.error("{} 分钟 K 数据未获取:{},发生致命错误,将不会重试:{}", stockSpan.getMin(), request.toJSONString()); + } else if (!result.isSuccess()) { + log.warn("{} 分钟 K 数据未获取:{}", stockSpan.getMin(), request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("检查 {} 分钟 K 数据执行结果时发生错误\r\n{}", stockSpan.getMin(), e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/ThsDailyService.java b/src/main/java/link/at17/mid/tushare/data/service/ThsDailyService.java new file mode 100644 index 0000000..2e34b8c --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/ThsDailyService.java @@ -0,0 +1,100 @@ +package link.at17.mid.tushare.data.service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.ThsDailyDao; +import link.at17.mid.tushare.dao.ThsListDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import link.at17.mid.tushare.enums.ThsStockMarket; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class ThsDailyService { + + private static final LocalDateTime THS_DAILY_BEGIN_LOCALDATE = LocalDateTime.of(2007, 8, 1, 0, 0, 0); + + @Autowired + ThsDailyDao thsDailyDao; + + @Autowired + ThsListDao thsListDao; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + RedissonClient redis; + + /** + * 同花顺板块指数日线行情
        + * 接口:ths_daily
        + * 描述:获取同花顺板块指数行情。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信:waditu_a
        + * 限量:单次最大3000行数据(需6000积分),可根据指数代码、日期参数循环提取。
        + * @param queryWay + * @return + */ + @UpdateMethod(name="同花顺板块指数日线行情", order=8) + public boolean updateData( + @AllowedEnum({"ByDateUpdate", "ByStockCrossCheck"}) + QueryWay queryWay) { + TushareRequestBody baseRequest = new TushareRequestBody("ths_daily").addFields("ts_code,trade_date,close,open,high,low,pre_close,avg_price,change,pct_change,vol,turnover_rate,total_mv,float_mv"); + Function, Boolean> function = (t) -> { + thsDailyDao.insertOrUpdateList(t); + return true; + }; + + List> executeResult; + if (queryWay.compareTo(QueryWay.ByStock) >= 0) { + executeResult = tushareCrawler.rollingQueryByStock(baseRequest, thsListDao.listByExchange(ThsStockMarket.A), thsDailyDao, function, 3000L, queryWay, null); + } + else { + executeResult = tushareCrawler.rollingQueryByDate(baseRequest, thsDailyDao, function, queryWay, THS_DAILY_BEGIN_LOCALDATE); + } + log.info("同花顺板块指数行情更新完成"); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = result.getRequest(); + if (result.isFatal()) { + log.error("同花顺板块指数行情数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); + } + else if (!result.isSuccess()) { + log.warn("同花顺板块指数行情数据未获取:{}", request.toJSONString()); + } + } catch (InterruptedException | ExecutionException e) { + log.error("同花顺板块指数行情数据执行结果时发生错误", e); + } + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + + /** + * 插入从 Tushare 获取的数据 + * @param list + * @return 插入是否成功 + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(thsDailyDao.insertOrUpdateList(list)); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/ThsListService.java b/src/main/java/link/at17/mid/tushare/data/service/ThsListService.java new file mode 100644 index 0000000..3be42a7 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/ThsListService.java @@ -0,0 +1,51 @@ +package link.at17.mid.tushare.data.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.ThsListDao; +import link.at17.mid.tushare.data.crawler.tushare.TushareClient; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.ThsStockInfo; +import link.at17.mid.tushare.service.BaseServiceImpl; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class ThsListService extends BaseServiceImpl { + + /** + * 同花顺概念和行业指数
        + * 接口:ths_index
        + * 描述:获取同花顺板块指数。注:数据版权归属同花顺,如做商业用途,请主动联系同花顺,如需帮助请联系微信migedata 。
        + * 限量:本接口需获得600积分,单次最大5000,一次可提取全部数据,请勿循环提取。 + * @return + */ + @UpdateMethod(name="同花顺板块指数列表", order=6) + public boolean updateData() { + try { + List thsIndexes = TushareClient.queryList(new TushareRequestBody("ths_index")); + insertOrUpdateList(thsIndexes); + log.info("更新同花顺板块列表完成"); + return true; + } catch (Exception e) { + log.error("更新同花顺板块列表时发生错误", e); + return false; + } + } + + /** + * 插入从 Tushare 获取的数据 + * @param list + * @return 插入是否成功 + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(baseMapper.insertOrUpdateList(list)); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/service/ThsMemberService.java b/src/main/java/link/at17/mid/tushare/data/service/ThsMemberService.java new file mode 100644 index 0000000..4e649be --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/service/ThsMemberService.java @@ -0,0 +1,96 @@ +package link.at17.mid.tushare.data.service; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; + +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.baomidou.mybatisplus.extension.toolkit.SqlHelper; + +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.dao.ThsListDao; +import link.at17.mid.tushare.dao.ThsMemberDao; +import link.at17.mid.tushare.data.crawler.QueryWay; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.crawler.tushare.TushareCrawlerResult; +import link.at17.mid.tushare.data.crawler.tushare.TushareRequestBody; +import link.at17.mid.tushare.data.models.ThsStockInfo; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class ThsMemberService { + + @Autowired + ThsMemberDao thsMemberDao; + + @Autowired + ThsListDao thsListDao; + + @Autowired + TushareCrawler tushareCrawler; + + @Autowired + RedissonClient redis; + + + /** + * 同花顺概念板块成分
        + * 接口:ths_member
        + * 描述:获取同花顺概念板块成分列表注:数据版权归属同花顺,如做商业用途,请主动联系同花顺。
        + * 限量:用户积累5000积分可调取,每分钟可调取200次,可按概念板块代码循环提取所有成分 + */ + @UpdateMethod(name="同花顺概念板块成分", order=7) + public boolean updateData() { + TushareRequestBody baseRequest = new TushareRequestBody("ths_member").addFields("ts_code,con_code,con_name,weight,in_date,out_date,is_new"); + Function, Boolean> function = (t) -> { + thsMemberDao.insertOrUpdateList(t); + return true; + }; + List stockInfoList = thsListDao.listByExchange(null); + while (true) { + List> executeResult = tushareCrawler.rollingQueryByStock(baseRequest, stockInfoList, null, function, 5000L, QueryWay.ByStock, null); + + stockInfoList.clear(); + for (Future f : executeResult) { + try { + TushareCrawlerResult result = f.get(); + JSONObject request = result.getRequest(); + if (result.isFatal()) { + log.error("同花顺概念板块成分数据未获取,发生致命错误,将不会重试:{}", request.toJSONString()); + } + else if (!result.isSuccess()) { + log.warn("同花顺概念板块成分数据未获取:{}", request.toJSONString()); + stockInfoList.add(request.getJSONObject("params").to(ThsStockInfo.class)); + } + } catch (InterruptedException | ExecutionException e) { + log.error("同花顺概念板块成分数据执行结果时发生错误", e); + } + } + if (stockInfoList.size() == 0) { + log.info("同花顺概念板块成分更新完成"); + break; + } + log.info("重新获取未获取成功的同花顺概念板块成分"); + } + if (baseRequest.hasFingerprint()) { + redis.getBucket(baseRequest.getFingerprint()).delete(); + } + return true; + } + + /** + * 插入从 Tushare 获取的数据 + * @param list + * @return 插入是否成功 + */ + public boolean insertOrUpdateList(List list) { + return SqlHelper.retBool(thsMemberDao.insertOrUpdateList(list)); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/typehandler/JsonListTypeHandler.java b/src/main/java/link/at17/mid/tushare/data/typehandler/JsonListTypeHandler.java new file mode 100644 index 0000000..11bddfb --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/typehandler/JsonListTypeHandler.java @@ -0,0 +1,67 @@ +package link.at17.mid.tushare.data.typehandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.extern.slf4j.Slf4j; + +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +@MappedTypes(List.class) +@MappedJdbcTypes(JdbcType.VARCHAR) +@Slf4j +public class JsonListTypeHandler extends BaseTypeHandler> { + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + try { + ps.setString(i, mapper.writeValueAsString(parameter)); + } catch (JsonProcessingException | SQLException e) { + log.error("将 {} 转换成 JSON String 并存储时错误", parameter, e); + } + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + String json = rs.getString(columnName); + try { + return mapper.readValue(json, new TypeReference>() {}); + } catch (JsonProcessingException e) { + log.error("将 JSON String {} 转换成 List 时错误", json, e); + return new ArrayList<>(); + } + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + try { + return mapper.readValue(rs.getString(columnIndex), new TypeReference>() {}); + } catch (JsonProcessingException | SQLException e) { + log.error("将 ResultSet {} 转换成 List 时错误", rs, e); + return new ArrayList<>(); + } + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + try { + return mapper.readValue(cs.getString(columnIndex), new TypeReference>() {}); + } catch (JsonProcessingException | SQLException e) { + log.error("将 CallableStatement {} 转换成 List 时错误", cs, e); + return new ArrayList<>(); + } + } +} diff --git a/src/main/java/link/at17/mid/tushare/data/typehandler/UpdateMethodInfoListTypeHandler.java b/src/main/java/link/at17/mid/tushare/data/typehandler/UpdateMethodInfoListTypeHandler.java new file mode 100644 index 0000000..ac77a3e --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/typehandler/UpdateMethodInfoListTypeHandler.java @@ -0,0 +1,40 @@ +package link.at17.mid.tushare.data.typehandler; + +import java.util.List; + +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; + +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import link.at17.mid.tushare.data.models.UpdateMethodInfo; +import lombok.extern.slf4j.Slf4j; + +import org.apache.ibatis.type.JdbcType; + +@MappedTypes(List.class) +@MappedJdbcTypes(JdbcType.VARCHAR) +@Slf4j +public class UpdateMethodInfoListTypeHandler extends AbstractJsonTypeHandler> { + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + protected List parse(String json) { + try { + return mapper.readValue(json, new TypeReference>() {}); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + protected String toJson(List obj) { + try { + return mapper.writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/data/util/CryptoUtil.java b/src/main/java/link/at17/mid/tushare/data/util/CryptoUtil.java deleted file mode 100644 index 7388bb0..0000000 --- a/src/main/java/link/at17/mid/tushare/data/util/CryptoUtil.java +++ /dev/null @@ -1,21 +0,0 @@ -package link.at17.mid.tushare.data.util; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import org.apache.commons.codec.binary.Hex; - -public class CryptoUtil { - - public static String getSHA256Str(String str) { - MessageDigest messageDigest; - String encdeStr = ""; - try { - messageDigest = MessageDigest.getInstance("SHA-256"); - byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8)); - encdeStr = Hex.encodeHexString(hash); - } catch (NoSuchAlgorithmException e) {} - return encdeStr; - } -} diff --git a/src/main/java/link/at17/mid/tushare/data/validator/AllowedEnum.java b/src/main/java/link/at17/mid/tushare/data/validator/AllowedEnum.java new file mode 100644 index 0000000..fb8da0f --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/AllowedEnum.java @@ -0,0 +1,19 @@ +package link.at17.mid.tushare.data.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = AllowedEnumValidator.class) +public @interface AllowedEnum { + String message() default "非法枚举值"; + Class[] groups() default {}; + Class[] payload() default {}; + String[] value(); // 允许的枚举名 +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/data/validator/AllowedEnumValidator.java b/src/main/java/link/at17/mid/tushare/data/validator/AllowedEnumValidator.java new file mode 100644 index 0000000..36641a7 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/AllowedEnumValidator.java @@ -0,0 +1,23 @@ +package link.at17.mid.tushare.data.validator; + +import java.util.Set; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * 允许枚举的验证器 + */ +public class AllowedEnumValidator implements IValidator, ConstraintValidator> { + private Set allowed; + + @Override + public void initialize(AllowedEnum anno) { + allowed = Set.of(anno.value()); + } + + @Override + public boolean isValid(Enum val, ConstraintValidatorContext ctx) { + return val == null || allowed.contains(val.name()); + } +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/data/validator/CronValidator.java b/src/main/java/link/at17/mid/tushare/data/validator/CronValidator.java new file mode 100644 index 0000000..2cf43f9 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/CronValidator.java @@ -0,0 +1,18 @@ +package link.at17.mid.tushare.data.validator; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.scheduling.support.CronExpression; + +public class CronValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null || value.isBlank()) return true; // 空值交由 @NotBlank 处理 + try { + CronExpression.parse(value); + return true; + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/data/validator/IValidator.java b/src/main/java/link/at17/mid/tushare/data/validator/IValidator.java new file mode 100644 index 0000000..d3d0b76 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/IValidator.java @@ -0,0 +1,17 @@ +package link.at17.mid.tushare.data.validator; + +import jakarta.validation.ConstraintValidatorContext; + +/** + * 快速提供自定义错误信息返回 + */ +public interface IValidator { + + public default boolean invalid(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + return false; + } + +} diff --git a/src/main/java/link/at17/mid/tushare/data/validator/UpdateMethodInfoValidator.java b/src/main/java/link/at17/mid/tushare/data/validator/UpdateMethodInfoValidator.java new file mode 100644 index 0000000..25c113d --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/UpdateMethodInfoValidator.java @@ -0,0 +1,63 @@ +package link.at17.mid.tushare.data.validator; + +import java.util.List; +import java.util.Objects; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import link.at17.mid.tushare.data.models.UpdateMethodInfo; +import link.at17.mid.tushare.data.models.UpdateMethodInfo.UpdateParamInfo; +import link.at17.mid.tushare.service.UpdateMethodService; + +@Component +public class UpdateMethodInfoValidator implements IValidator, ConstraintValidator { + + @Autowired + private UpdateMethodService updateMethodService; // 内存维护的合法方法表 + + @Override + public boolean isValid(UpdateMethodInfo methodInfo, ConstraintValidatorContext context) { + if (methodInfo == null) return true; + + // 1. 校验 id 是否存在 + if (methodInfo.getId() == null) { + return invalid(context, "方法 id 为空"); + } + + // 2. 校验 id 在内存中是否存储 + UpdateMethodInfo validMethod = updateMethodService.findById(methodInfo.getId()); + if (validMethod == null) { + return invalid(context, "方法 id 不存在: " + methodInfo.getId()); + } + + String name = validMethod.getName(); + + // 3. 校验参数数量与名称 + List incoming = methodInfo.getParams(); + List reference = validMethod.getParams(); + + if (incoming == null || incoming.size() != reference.size()) { + return invalid(context, name + " 参数数量不一致: " + methodInfo.getId()); + } + + for (int i = 0; i < incoming.size(); i++) { + UpdateParamInfo in = incoming.get(i); + UpdateParamInfo ref = reference.get(i); + + if (!Objects.equals(in.getName(), ref.getName())) { + return invalid(context, name + " 参数名不匹配: " + in.getName()); + } + + if (ref.getAllowedEnumValues() != null && + in.getValue() != null && + !ref.getAllowedEnumValues().contains(String.valueOf(in.getValue()))) { + return invalid(context, name + " 参数值非法: " + in.getName() + " : " + in.getValue()); + } + } + + return true; + } +} diff --git a/src/main/java/link/at17/mid/tushare/data/validator/ValidCron.java b/src/main/java/link/at17/mid/tushare/data/validator/ValidCron.java new file mode 100644 index 0000000..19fc625 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/ValidCron.java @@ -0,0 +1,17 @@ +package link.at17.mid.tushare.data.validator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = CronValidator.class) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidCron { + + String message() default "无效的 Cron 表达式"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/data/validator/ValidUpdateMethodInfo.java b/src/main/java/link/at17/mid/tushare/data/validator/ValidUpdateMethodInfo.java new file mode 100644 index 0000000..21f98cc --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/data/validator/ValidUpdateMethodInfo.java @@ -0,0 +1,20 @@ +package link.at17.mid.tushare.data.validator; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ElementType.TYPE, ElementType.TYPE_USE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = UpdateMethodInfoValidator.class) +@Documented +public @interface ValidUpdateMethodInfo { + String message() default "非法的 UpdatePlan"; + Class[] groups() default {}; + Class[] payload() default {}; +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/dto/JsonViews.java b/src/main/java/link/at17/mid/tushare/dto/JsonViews.java new file mode 100644 index 0000000..8c745b6 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/dto/JsonViews.java @@ -0,0 +1,6 @@ +package link.at17.mid.tushare.dto; + +public class JsonViews { + public static class Public {} // 给前端用 + public static class Internal {} // 存库用 +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/dto/LayPageReq.java b/src/main/java/link/at17/mid/tushare/dto/LayPageReq.java new file mode 100644 index 0000000..911e3e8 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/dto/LayPageReq.java @@ -0,0 +1,43 @@ +package link.at17.mid.tushare.dto; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 后台管理分页查询,适配 layui + * @author Doghole + * + * @param + */ +@Accessors(chain = true) +@Data +@EqualsAndHashCode(callSuper=false) +public class LayPageReq extends Page { + + private static final long serialVersionUID = 3471995935637905622L; + + public LayPageReq setPage(Long page) { + current = page; + return this; + } + + public Long getPage() { + return current; + } + + public LayPageReq setLimit(Long limit) { + size = limit; + return this; + } + + public Long getLimit() { + return size; + } + + Integer start; + + T condition; +} diff --git a/src/main/java/link/at17/mid/tushare/enums/ListStatus.java b/src/main/java/link/at17/mid/tushare/enums/ListStatus.java index 3e3efdf..e441418 100644 --- a/src/main/java/link/at17/mid/tushare/enums/ListStatus.java +++ b/src/main/java/link/at17/mid/tushare/enums/ListStatus.java @@ -5,8 +5,17 @@ import com.baomidou.mybatisplus.annotation.EnumValue; import lombok.Getter; public enum ListStatus { + /** + * 上市 + */ LIST("L"), + /** + * 退市 + */ DELIST("D"), + /** + * 停牌 + */ PAUSE("P"); @Getter diff --git a/src/main/java/link/at17/mid/tushare/enums/UpdateLogType.java b/src/main/java/link/at17/mid/tushare/enums/UpdateLogType.java new file mode 100644 index 0000000..529547f --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/enums/UpdateLogType.java @@ -0,0 +1,9 @@ +package link.at17.mid.tushare.enums; + +public enum UpdateLogType { + + INFO, + ERROR, + SUCCESS + +} diff --git a/src/main/java/link/at17/mid/tushare/service/BaseServiceImpl.java b/src/main/java/link/at17/mid/tushare/service/BaseServiceImpl.java new file mode 100644 index 0000000..962534d --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/service/BaseServiceImpl.java @@ -0,0 +1,88 @@ +/** + * + */ +package link.at17.mid.tushare.service; + +import java.util.List; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.github.yulichang.base.MPJBaseMapper; +import com.github.yulichang.base.MPJBaseServiceImpl; +import com.github.yulichang.interfaces.MPJBaseJoin; + +/** + * BaseServiceImpl + *
        基本业务实现 + *
        支持联查 Wrapper: {@code MPJLambdaWrapper} + * @author Doghole + */ +public abstract class BaseServiceImpl, T> extends MPJBaseServiceImpl { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public T getOne(Wrapper wrapper) { + return (wrapper instanceof MPJBaseJoin) ? + (T) selectJoinOne(currentModelClass(), (MPJBaseJoin)wrapper) : super.getOne(wrapper); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public long count(Wrapper wrapper) { + return (wrapper instanceof MPJBaseJoin) ? + baseMapper.selectJoinCount((MPJBaseJoin) wrapper): super.count(wrapper); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public List list(Wrapper wrapper) { + return (wrapper instanceof MPJBaseJoin) ? + baseMapper.selectJoinList(currentModelClass(), (MPJBaseJoin)wrapper): super.list(wrapper); + } + + + /** + * 给子类用的方法 + * @param + * @param clazz + * @param wrapper + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public E getOneJoin(Class clazz, MPJBaseJoin wrapper) { + return (E) baseMapper.selectJoinOne(clazz, (MPJBaseJoin)wrapper); + } + + /** + * 给子类用的方法 + * @param + * @param clazz + * @param wrapper + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public List listJoin(Class clazz, MPJBaseJoin wrapper) { + return baseMapper.selectJoinList(clazz, (MPJBaseJoin)wrapper); + } + + /** + * 基于 Mybatis-Plus 的分页,返回对应 Page 和 Wrapper 的 Page + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + public > E page(E page, Wrapper wrapper) { + return (wrapper instanceof MPJBaseJoin) ? + (E) baseMapper.selectJoinPage(page, this.currentModelClass(), (MPJBaseJoin)wrapper): + super.page(page, wrapper); + } + + /** + * 基于 Mybatis-Plus 的分页,返回对应 Page<T> 和 Wrapper<T> 的 Page
        + * 给子类用的方法 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public , S extends T> E joinPage(E page, Class clazz, MPJBaseJoin wrapper) { + return (E) baseMapper.selectJoinPage(page, clazz, (MPJBaseJoin)wrapper); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/service/StatService.java b/src/main/java/link/at17/mid/tushare/service/StatService.java new file mode 100644 index 0000000..6d71cd7 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/service/StatService.java @@ -0,0 +1,11 @@ +package link.at17.mid.tushare.service; + +import org.springframework.stereotype.Service; + +/** + * 统计和状态服务 + */ +@Service +public class StatService { + +} diff --git a/src/main/java/link/at17/mid/tushare/service/UpdateMethodService.java b/src/main/java/link/at17/mid/tushare/service/UpdateMethodService.java new file mode 100644 index 0000000..bfa342b --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/service/UpdateMethodService.java @@ -0,0 +1,194 @@ +package link.at17.mid.tushare.service; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.reflections.Reflections; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import jakarta.annotation.PostConstruct; +import link.at17.mid.tushare.annotation.UpdateMethod; +import link.at17.mid.tushare.data.models.UpdateMethodInfo; +import link.at17.mid.tushare.data.models.UpdateMethodInfo.UpdateParamInfo; +import link.at17.mid.tushare.data.validator.AllowedEnum; +import link.at17.mid.tushare.system.util.SpringBeanDetector; +import link.at17.mid.tushare.system.util.SpringContextHolder; +import lombok.extern.slf4j.Slf4j; + + +@Service +@Slf4j +@Lazy(false) +@Validated +public class UpdateMethodService { + + @Autowired + Reflections reflections; + + /** + * 存放初始化 UpdateMethodInfo 的缓存,主要用于通过 id 来找到原始记录 + */ + Map updateMethodInfoCaches = new LinkedHashMap<>(); + + @PostConstruct + void postConstruct() { + + // 从 reflections 拿信息 + List updateMethods = + new ArrayList<>(reflections.getMethodsAnnotatedWith(UpdateMethod.class)); + updateMethods.sort(Comparator.comparingInt(m -> m.getAnnotation(UpdateMethod.class).order())); + + methodLabel: + for (Method method : updateMethods) { + + UpdateMethodInfo info = new UpdateMethodInfo(); + + // 获取 UpdateMethod 注解基本信息 + UpdateMethod um = method.getAnnotation(UpdateMethod.class); + String name = um.name(); + info.setName(name); + info.setMethodName(method.getName()); + Class declaringClass = method.getDeclaringClass(); + info.setDeclaringClassName(declaringClass.getName()); + + // 判断该 Class 是否交由 Spring 管理 + boolean managedBySpring = SpringBeanDetector.isSpringManagedClass(declaringClass); + if (!managedBySpring) { + // TODO: 非 Spring 管理类的成员方法 / 静态方法的数据更新? + log.warn("方法 {} 所属类 {} 不归属于 Spring 管理,目前暂不支持作为更新服务候选项", + method.getName(), declaringClass.getSimpleName()); + continue; + } + + // 获取该方法的所有参数并读取 + List paramInfos = new ArrayList<>(); + for (Parameter p : method.getParameters()) { + + UpdateMethodInfo.UpdateParamInfo updateParamInfo = new UpdateMethodInfo.UpdateParamInfo(); + updateParamInfo.setName(p.getName()); + Class parameterType = p.getType(); + updateParamInfo.setFullTypeName(parameterType.getName()); + updateParamInfo.setTypeName(parameterType.getSimpleName()); + updateParamInfo.setTypeClass(parameterType); + + if (parameterType.isEnum()) { + + // 当前枚举下的所有枚举值 + List allEnums = Arrays.stream(parameterType.getEnumConstants()) + .map(Object::toString) + .toList(); + + AllowedEnum allowedEnum = p.getAnnotation(AllowedEnum.class); + if (allowedEnum == null) { + // 未指定 AllowedEnum,将所有枚举放到 allowedEnumValues 里 + updateParamInfo.setAllowedEnumValues(allEnums); + } + else { + // 指定了 AllowedEnum,将允许的枚举放到 allowedEnumValues 里 + String[] allowedEnums = allowedEnum.value(); + List allowedEnumValues = new ArrayList<>(); + for (String specificEnum : allowedEnums) { + if (allEnums.contains(specificEnum)) { + allowedEnumValues.add(specificEnum); + } + else { + log.warn("枚举类 {} 不存在指定的枚举值 {},将忽略", parameterType.getSimpleName(), specificEnum); + } + } + updateParamInfo.setAllowedEnumValues(allowedEnumValues); + } + paramInfos.add(updateParamInfo); + continue; + } + else { + log.warn("方法 {} 参数 {} 非枚举类,目前暂不支持"); + continue methodLabel; + } + } + info.setParams(paramInfos); + updateMethodInfoCaches.put(info.getId(), info); + } + } + + /** + * 获取潜在的数据更新方法。这些方法通过反射扫描得来 + * @return + */ + + public List getPotentialUpdateMethodInfos() { + return new ArrayList<>(this.updateMethodInfoCaches.values()); + } + + /** + * 根据 UpdateMethodInfo 的 id 查找原始方法模板 + * @param id + * @return + */ + public UpdateMethodInfo findById(String id) { + return this.updateMethodInfoCaches.get(id); + } + + /** + * 填充从外部来的 UpdateMethodInfo + *

        + * 注意这里的 incoming 必须是通过 @Valid 校验的 + * @param incoming + */ + public void fillUpdateMethodInfo(@Validated UpdateMethodInfo incoming) { + // 填充 method 信息 + UpdateMethodInfo valid = findById(incoming.getId()); + BeanUtils.copyProperties(valid, incoming, "params"); + if (valid.getParams() == null || valid.getParams().size() == 0) return; + for (int i = 0; i < valid.getParams().size(); i++) { + UpdateParamInfo in = incoming.getParams().get(i); + UpdateParamInfo ref = valid.getParams().get(i); + BeanUtils.copyProperties(ref, in, "value"); + } + } + + /** + * 执行方法 + * @param updateMethodInfo + * @throws InvocationTargetException + * @throws IllegalAccessException + * @throws SecurityException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + */ + @SuppressWarnings("unchecked") + public void execute(@Validated UpdateMethodInfo updateMethodInfo) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException { + Class declaringClass = Class.forName(updateMethodInfo.getDeclaringClassName()); + Object declaringClassInstance = SpringContextHolder.getBean(declaringClass); + boolean hasParams = !updateMethodInfo.getParams().isEmpty(); + Method method; + + if (hasParams) { + @SuppressWarnings("rawtypes") + Class[] paramTypes = new Class[updateMethodInfo.getParams().size()]; + Object[] paramValues = new Object[updateMethodInfo.getParams().size()]; + for (int i = 0; i < updateMethodInfo.getParams().size(); i++) { + UpdateParamInfo param = updateMethodInfo.getParams().get(i); + paramTypes[i] = Class.forName(param.getFullTypeName()); + // 转换 parameterValue + paramValues[i] = Enum.valueOf(paramTypes[i], param.getValue().toString()); + } + method = declaringClass.getDeclaredMethod(updateMethodInfo.getMethodName(), paramTypes); + method.invoke(declaringClassInstance, paramValues); + } + else { + method = declaringClass.getDeclaredMethod(updateMethodInfo.getMethodName()); + method.invoke(declaringClassInstance); + } + } + +} diff --git a/src/main/java/link/at17/mid/tushare/service/UpdatePlanService.java b/src/main/java/link/at17/mid/tushare/service/UpdatePlanService.java new file mode 100644 index 0000000..0d4a165 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/service/UpdatePlanService.java @@ -0,0 +1,162 @@ +package link.at17.mid.tushare.service; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Set; + +import org.quartz.CronScheduleBuilder; +import org.quartz.CronTrigger; +import org.quartz.JobBuilder; +import org.quartz.JobDataMap; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.matchers.GroupMatcher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Valid; +import jakarta.validation.Validator; +import link.at17.mid.tushare.dao.UpdatePlanDao; +import link.at17.mid.tushare.data.models.UpdateMethodInfo; +import link.at17.mid.tushare.data.models.UpdatePlan; +import link.at17.mid.tushare.task.job.UpdatePlanJob; +import lombok.extern.slf4j.Slf4j; + +@Service +@Lazy(false) +@DependsOn({"updateMethodService", "updateMethodInfoValidator"}) +@Slf4j +@Validated +public class UpdatePlanService extends BaseServiceImpl { + + public static final String JOB_GROUP_NAME = "UpdatePlanGroup"; + + @Resource(name="scheduler") + Scheduler scheduler; + + @Autowired + UpdateMethodService updateMethodService; + + @Autowired + Validator validator; + + @PostConstruct + void postConstruct() throws SchedulerException { + // 从数据库加载所有已保存 UpdatePlan 并逐一判断其有效性 + List updatePlans = list(); + for (UpdatePlan updatePlan : updatePlans) { + Set> v = validator.validate(updatePlan); + if (!v.isEmpty()) { + v.forEach(violation -> { + log.error("UpdatePlan {} 发生错误: {}", updatePlan.getName(), violation.getMessage()); + }); + // 设置 UpdatePlan 为无效 + updatePlan.setValid(false); + saveOrUpdate(updatePlan); + } + } + updatePlans = list(); + updatePlans.forEach(updatePlan -> { + try { + rescheduleTask(updatePlan); + } catch (SchedulerException e) { + log.error("从 UpdatePlan 数据表编排任务失败,id = {}, name = {}", updatePlan.getId(), updatePlan.getName(), e); + } + }); + } + + /** + * 删除指定的任务 + * @param updatePlan + * @throws SchedulerException + */ + public void deleteTask(UpdatePlan updatePlan) throws SchedulerException { + deleteTask(updatePlan.getId()); + } + + /** + * 删除指定的任务 + * @param id UpdatePlan::getId + * @throws SchedulerException + */ + public void deleteTask(Integer id) throws SchedulerException { + JobKey jobKey = JobKey.jobKey(id.toString(), JOB_GROUP_NAME); + if (scheduler.checkExists(jobKey)) { + scheduler.deleteJob(jobKey); + } + } + + /** + * 根据 UpdatePlan 实例新建或更新任务 + * @param updatePlan + * @throws SchedulerException + */ + public void rescheduleTask(@Validated UpdatePlan updatePlan) throws SchedulerException { + JobKey jobKey = JobKey.jobKey(updatePlan.getId().toString(), JOB_GROUP_NAME); + TriggerKey triggerKey = TriggerKey.triggerKey(updatePlan.getId().toString(), JOB_GROUP_NAME); + + deleteTask(updatePlan.getId()); + + if (!updatePlan.getEnabled() || !updatePlan.getValid()) { + return; + } + + JobDataMap jobData = new JobDataMap(); + jobData.put("updatePlan", updatePlan); + + JobDetail job = JobBuilder.newJob(UpdatePlanJob.class) + .withIdentity(jobKey) + .usingJobData(jobData) + .build(); + + CronTrigger trigger = TriggerBuilder.newTrigger() + .withIdentity(triggerKey) + .withSchedule(CronScheduleBuilder.cronSchedule(updatePlan.getCronExpr())) + .build(); + + scheduler.scheduleJob(job, trigger); + log.debug("已(重新)编排任务 [{}]{}, cronExpr = \"{}\"", + updatePlan.getId(), updatePlan.getName(), updatePlan.getCronExpr()); + + log.debug("当前所有任务:"); + Set jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals(JOB_GROUP_NAME)); + jobKeys.forEach(jk -> { + UpdatePlan relative = getById(Integer.valueOf(jk.getName())); + log.debug("UpdatePlan [{}]{}, cronExpr = \"{}\"", + relative.getId(), relative.getName(), relative.getCronExpr()); + }); + } + + /** + * 执行方法 + * @param updatePlan + * @throws ClassNotFoundException + * @throws NoSuchMethodException + * @throws SecurityException + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + public void execute(@Valid UpdatePlan updatePlan) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, InvocationTargetException { + for (UpdateMethodInfo info : updatePlan.getMethods()) { + updateMethodService.execute(info); + } + } + + @Override + public List list() { + return list(new LambdaQueryWrapper().orderByAsc(UpdatePlan::getId)); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/system/util/SpringBeanDetector.java b/src/main/java/link/at17/mid/tushare/system/util/SpringBeanDetector.java new file mode 100644 index 0000000..e7c5517 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/system/util/SpringBeanDetector.java @@ -0,0 +1,44 @@ +package link.at17.mid.tushare.system.util; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.*; +import org.springframework.web.bind.annotation.*; +import org.springframework.context.annotation.Configuration; +import org.aspectj.lang.annotation.Aspect; + +import java.util.List; + +public class SpringBeanDetector { + + @SuppressWarnings("rawtypes") + private static final List SPRING_COMPONENT_ANNOTATIONS = List.of( + Component.class, + Service.class, + Repository.class, + Controller.class, + RestController.class, + Configuration.class, + ControllerAdvice.class, + RestControllerAdvice.class, + Aspect.class + ); + + @SuppressWarnings("unchecked") + public static boolean isSpringManagedClass(Class clazz) { + for (@SuppressWarnings("rawtypes") Class ann : SPRING_COMPONENT_ANNOTATIONS) { + if (AnnotationUtils.findAnnotation(clazz, ann) != null) { + return true; + } + } + return false; + } + + @SuppressWarnings("unchecked") + public static String findMatchedAnnotation(Class clazz) { + for (@SuppressWarnings("rawtypes") Class ann : SPRING_COMPONENT_ANNOTATIONS) { + if (AnnotationUtils.findAnnotation(clazz, ann) != null) { + return ann.getSimpleName(); + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/system/util/SpringContextHolder.java b/src/main/java/link/at17/mid/tushare/system/util/SpringContextHolder.java index 99cc114..9d181ad 100644 --- a/src/main/java/link/at17/mid/tushare/system/util/SpringContextHolder.java +++ b/src/main/java/link/at17/mid/tushare/system/util/SpringContextHolder.java @@ -5,6 +5,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.lang3.Validate; @@ -33,7 +34,15 @@ import lombok.extern.slf4j.Slf4j; public class SpringContextHolder implements ApplicationContextAware, DisposableBean { private static ApplicationContext applicationContext = null; - + + /** + * 实现ApplicationContextAware接口, 注入Context到静态变量中. + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + SpringContextHolder.applicationContext = applicationContext; + } + /** * 取得存储在静态变量中的ApplicationContext. */ @@ -58,6 +67,14 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB assertContextInjected(); return applicationContext.getBean(requiredType); } + + /** + * 从静态变量applicationContext中取得Beans, 自动转型为所赋值对象的类型的 Map. + */ + public static Map getBeansOfType(Class requiredType) { + assertContextInjected(); + return applicationContext.getBeansOfType(requiredType); + } /** * 清除SpringContextHolder中的ApplicationContext为Null. @@ -69,13 +86,6 @@ public class SpringContextHolder implements ApplicationContextAware, DisposableB applicationContext = null; } - /** - * 实现ApplicationContextAware接口, 注入Context到静态变量中. - */ - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - SpringContextHolder.applicationContext = applicationContext; - } /** * 实现DisposableBean接口, 在Context关闭时清理静态变量. diff --git a/src/main/java/link/at17/mid/tushare/task/TaskSchedulerFactory.java b/src/main/java/link/at17/mid/tushare/task/AutowireCapableJobFactory.java similarity index 55% rename from src/main/java/link/at17/mid/tushare/task/TaskSchedulerFactory.java rename to src/main/java/link/at17/mid/tushare/task/AutowireCapableJobFactory.java index 9846f95..b758c7f 100644 --- a/src/main/java/link/at17/mid/tushare/task/TaskSchedulerFactory.java +++ b/src/main/java/link/at17/mid/tushare/task/AutowireCapableJobFactory.java @@ -5,18 +5,20 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.scheduling.quartz.AdaptableJobFactory; import org.springframework.stereotype.Component; +/** + * Job 工厂,使得 Job 实例能获取 Spring 管理的 Bean + *

        + * 如某 UpdatePlan implements Job, 在其 execute 方法中需要调用 updatePlanService, 则必须要实例能获取 updatePlanService + */ @Component -public class TaskSchedulerFactory extends AdaptableJobFactory { - - // 需要使用这个BeanFactory对Qurartz创建好Job实例进行后续处理,属于Spring的技术范畴. +public class AutowireCapableJobFactory extends AdaptableJobFactory { + @Autowired private AutowireCapableBeanFactory capableBeanFactory; protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { - // 首先,调用父类的方法创建好Quartz所需的Job实例 Object jobInstance = super.createJobInstance(bundle); - // 然后,使用BeanFactory为创建好的Job实例进行属性自动装配并将其纳入到Spring容器的管理之中,属于Spring的技术范畴. - capableBeanFactory.autowireBean(jobInstance); + capableBeanFactory.autowireBean(jobInstance); // 自动注入 Spring Bean return jobInstance; } } \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/task/job/DailyUpdateDataJob.java b/src/main/java/link/at17/mid/tushare/task/job/DailyUpdateDataJob.java deleted file mode 100644 index ca77c51..0000000 --- a/src/main/java/link/at17/mid/tushare/task/job/DailyUpdateDataJob.java +++ /dev/null @@ -1,93 +0,0 @@ -package link.at17.mid.tushare.task.job; - -import org.apache.commons.lang3.ArrayUtils; -import java.util.Calendar; - -import org.apache.commons.lang3.time.DateUtils; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; - -import link.at17.mid.tushare.dao.StockCalendarDao; -import link.at17.mid.tushare.data.crawler.QueryWay; -import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; -import link.at17.mid.tushare.data.models.StockCalendar; -import link.at17.mid.tushare.enums.StockHolderType; -import link.at17.mid.tushare.enums.StockMarket; -import link.at17.mid.tushare.system.util.SpringContextHolder; -import lombok.extern.slf4j.Slf4j; - -@Component -@Slf4j -public class DailyUpdateDataJob implements Job { - - - @Autowired - TushareCrawler tushareCrawler; - - @Autowired - StockCalendarDao stockCalendarDao; - - public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { - - - log.info("每日定时数据更新开始"); - - try { - String[] profile = SpringContextHolder.getApplicationContext().getEnvironment().getActiveProfiles(); - if (ArrayUtils.contains(profile, "remote")) { - log.info("当前环境为远程调试环境,不参与每日数据更新"); - return; - } - } - catch (Exception e) { - log.error("获取当前 active profile 失败", e); - return; - } - - - StockCalendar szLatestCal = stockCalendarDao.getLatest(StockMarket.SZ); - StockCalendar shLatestCal = stockCalendarDao.getLatest(StockMarket.SH); - if (szLatestCal == null || shLatestCal == null) { - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); - } - - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); - - // 查询当日是否是交易日 - Boolean todayIsOpen = stockCalendarDao.exists(new LambdaQueryWrapper() - .eq(StockCalendar::getDate, DateUtils.truncate(Calendar.getInstance().getTime(), Calendar.DATE)) - .eq(StockCalendar::getIsOpen, true)); - - if (!todayIsOpen) { - log.info("当日非交易日,忽略更新"); - return; - } - - int updateStockListRetry = 5; - while (--updateStockListRetry > 0) { - if (tushareCrawler.updateStockList()) { - break; - } - } - if (updateStockListRetry == 0) { - // updateStockList failed - log.warn("updateStockList 尝试更新失败,将在下一结算日后更新"); - } - tushareCrawler.updateStockDaily(QueryWay.ByDateUpdate); - tushareCrawler.updateDailyBasic(QueryWay.ByDateUpdate); - tushareCrawler.updateStockAdjustTushare(QueryWay.ByDateUpdate); - tushareCrawler.updateStockLimit(QueryWay.ByDateUpdate); - - tushareCrawler.updateThsDaily(QueryWay.ByDateUpdate); - tushareCrawler.updateThsMember(); - tushareCrawler.updateStockHolder(StockHolderType.TOP10Float); - - log.info("每日定时更新数据完成"); - } - -} diff --git a/src/main/java/link/at17/mid/tushare/task/job/UpdatePlanJob.java b/src/main/java/link/at17/mid/tushare/task/job/UpdatePlanJob.java new file mode 100644 index 0000000..7f58d43 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/task/job/UpdatePlanJob.java @@ -0,0 +1,61 @@ +package link.at17.mid.tushare.task.job; + +import java.lang.reflect.InvocationTargetException; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import link.at17.mid.tushare.data.models.UpdatePlan; +import link.at17.mid.tushare.data.service.StockCalendarService; +import link.at17.mid.tushare.service.UpdatePlanService; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +@Validated +public class UpdatePlanJob implements Job { + + @Autowired + private UpdatePlanService updatePlanService; + + @Autowired + private StockCalendarService stockCalendarService; + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + UpdatePlan plan = (UpdatePlan) context.getMergedJobDataMap().get("updatePlan"); + + if (!plan.getEnabled()) { + log.info("任务 [{}]{} 未启用,忽略任务", plan.getId(), plan.getName()); + return; + } + + if (!plan.getValid()) { + log.info("任务 [{}]{} 不合法,忽略任务", plan.getId(), plan.getName()); + return; + } + + // 交易日检查 + if (plan.getOpenDayCheck()) { + Boolean todayIsOpen = stockCalendarService.todayIsOpen(); + if (!todayIsOpen) { + log.info("任务 [{}]{} 开启了交易日检查,当日非交易日,忽略任务", plan.getId(), plan.getName()); + return; + } + } + + try { + log.info("任务 [{}]{} 开始执行...", plan.getId(), plan.getName()); + updatePlanService.execute(plan); + log.info("任务 [{}]{} 执行完毕", plan.getId(), plan.getName()); + } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException + | InvocationTargetException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/link/at17/mid/tushare/task/scheduler/CacheDailyEvictionScheduler.java b/src/main/java/link/at17/mid/tushare/task/scheduler/CacheDailyEvictionScheduler.java index 9ac6f5b..ab5082d 100644 --- a/src/main/java/link/at17/mid/tushare/task/scheduler/CacheDailyEvictionScheduler.java +++ b/src/main/java/link/at17/mid/tushare/task/scheduler/CacheDailyEvictionScheduler.java @@ -7,10 +7,10 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import link.at17.mid.tushare.cache.CacheEvictionJob; import link.at17.mid.tushare.task.TaskConstants; -import link.at17.mid.tushare.task.TaskSchedulerFactory; +import link.at17.mid.tushare.task.AutowireCapableJobFactory; @Component -public class CacheDailyEvictionScheduler extends TaskSchedulerFactory { +public class CacheDailyEvictionScheduler extends AutowireCapableJobFactory { @Resource(name="scheduler") Scheduler scheduler; diff --git a/src/main/java/link/at17/mid/tushare/task/scheduler/DailyUpdateDataScheduler.java b/src/main/java/link/at17/mid/tushare/task/scheduler/DailyUpdateDataScheduler.java deleted file mode 100644 index e304e11..0000000 --- a/src/main/java/link/at17/mid/tushare/task/scheduler/DailyUpdateDataScheduler.java +++ /dev/null @@ -1,31 +0,0 @@ -package link.at17.mid.tushare.task.scheduler; - -import org.quartz.*; -import org.springframework.stereotype.Component; - -import jakarta.annotation.PostConstruct; -import jakarta.annotation.Resource; -import link.at17.mid.tushare.task.TaskSchedulerFactory; -import link.at17.mid.tushare.task.job.DailyUpdateDataJob; - -@Component -public class DailyUpdateDataScheduler extends TaskSchedulerFactory { - - @Resource(name="scheduler") - private Scheduler scheduler; - - @PostConstruct - public void startScheduler() throws SchedulerException { - //创建调度器Schedule - //创建JobDetail实例,并与HelloWordlJob类绑定 - - JobDetail jobDetail = JobBuilder.newJob(DailyUpdateDataJob.class).withIdentity("cronJob").build(); - //创建触发器Trigger实例(每天3点执行) - CronTrigger cronTrigger = - TriggerBuilder.newTrigger().withIdentity("cronTrigger") - .withSchedule(CronScheduleBuilder.cronSchedule("0 0 18 * * ? ")).build(); - //开始执行 - scheduler.scheduleJob(jobDetail, cronTrigger); - scheduler.start(); - } -} \ No newline at end of file diff --git a/src/main/java/link/at17/mid/tushare/web/controller/ManageController.java b/src/main/java/link/at17/mid/tushare/web/controller/ManageController.java index 156b45b..43257d9 100644 --- a/src/main/java/link/at17/mid/tushare/web/controller/ManageController.java +++ b/src/main/java/link/at17/mid/tushare/web/controller/ManageController.java @@ -40,8 +40,16 @@ public class ManageController { return "admin/manage/views/index.html"; } + @GetMapping("/manage/demo-index") + private String demoIndex() { + return "admin/manage/views/demo-index.html"; + } + @GetMapping("/manage/{*routine}") private String routine(@PathVariable String routine) { + routine = routine.replaceAll("\\/+", "/"); + routine = routine.replaceAll("\\\\+", "/"); + routine = routine.replaceFirst("\\/", ""); return "admin/manage/views/" + routine; } diff --git a/src/main/java/link/at17/mid/tushare/web/controller/UpdateMethodController.java b/src/main/java/link/at17/mid/tushare/web/controller/UpdateMethodController.java new file mode 100644 index 0000000..11a3f8a --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/web/controller/UpdateMethodController.java @@ -0,0 +1,52 @@ +package link.at17.mid.tushare.web.controller; + +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.Validator; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import link.at17.mid.tushare.data.models.UpdateMethodInfo; +import link.at17.mid.tushare.data.models.UpdatePlan; +import link.at17.mid.tushare.dto.LayPageReq; +import link.at17.mid.tushare.dto.LayPageResp; +import link.at17.mid.tushare.service.UpdateMethodService; +import link.at17.mid.tushare.service.UpdatePlanService; +import lombok.extern.slf4j.Slf4j; + +@Controller +@RequestMapping("/admin/manage/reviews/update-methods") +@Slf4j +public class UpdateMethodController extends BaseController { + + @Autowired + Validator validator; + + @Autowired + UpdateMethodService updateMethodService; + + @Autowired + UpdatePlanService updatePlanService; + + @GetMapping("/plan-list") + public String planList() { + return "admin/manage/reviews/plans/plan-list"; + } + + @GetMapping("/method-list-resp") + @ResponseBody + public Collection updateMethodInfoList() { + return updateMethodService.getPotentialUpdateMethodInfos(); + } + + @GetMapping("/list") + @ResponseBody + public LayPageResp list(LayPageReq req) { + return new LayPageResp<>(updatePlanService.page(req)); + } + +} diff --git a/src/main/java/link/at17/mid/tushare/web/controller/UpdatePlanController.java b/src/main/java/link/at17/mid/tushare/web/controller/UpdatePlanController.java new file mode 100644 index 0000000..40d6214 --- /dev/null +++ b/src/main/java/link/at17/mid/tushare/web/controller/UpdatePlanController.java @@ -0,0 +1,105 @@ +package link.at17.mid.tushare.web.controller; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableFieldInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; + +import link.at17.mid.tushare.data.models.UpdatePlan; +import link.at17.mid.tushare.dto.LayPageReq; +import link.at17.mid.tushare.dto.LayPageResp; +import link.at17.mid.tushare.dto.R; +import link.at17.mid.tushare.service.UpdateMethodService; +import link.at17.mid.tushare.service.UpdatePlanService; +import link.at17.mid.tushare.web.exception.RException; +import lombok.extern.slf4j.Slf4j; + +@Controller +@RequestMapping("/admin/manage/reviews/plans") +@Slf4j +@Validated +public class UpdatePlanController extends BaseController { + + @Autowired + Validator validator; + + @Autowired + UpdateMethodService updateMethodService; + + @Autowired + UpdatePlanService updatePlanService; + + @GetMapping("/plan-list") + public String planList() { + return "admin/manage/reviews/plans/plan-list"; + } + + @GetMapping("/list") + @ResponseBody + public LayPageResp list(LayPageReq req) { + return new LayPageResp<>(updatePlanService.page(req, new LambdaQueryWrapper().orderByAsc(UpdatePlan::getId))); + } + + @GetMapping("/get") + @ResponseBody + public R get(Integer id) { + UpdatePlan plan; + if (id == null) { + plan = new UpdatePlan(); + return R.ok(plan); + } + plan = updatePlanService.getById(id); + return R.judge(plan != null, plan, "无法找到对应 ID 的 Plan"); + } + + @PostMapping("/save") + @ResponseBody + public R save(@Validated @RequestBody UpdatePlan updatePlan) { + updatePlan.getMethods().forEach(method -> updateMethodService.fillUpdateMethodInfo(method)); + updatePlan.setValid(true); + return R.judge(updatePlanService.saveOrUpdate(updatePlan)); + } + + @PostMapping("/updateBool") + @ResponseBody + public R updateBool(Integer id, String name, Boolean value) { + if (!List.of("enabled", "openDayCheck").contains(name)) { + throw RException.badRequest("非法字段名" + name); + } + if (value == null) { + throw RException.badRequest("不允许空值"); + } + + TableInfo tableInfo = TableInfoHelper.getTableInfo(UpdatePlan.class); + String idField = tableInfo.getKeyColumn(); + String dbField = tableInfo.getFieldList().stream() + .filter(f -> f.getProperty().equals(name)) + .findFirst() + .map(TableFieldInfo::getColumn) + .orElse(null); + return R.judge(updatePlanService.update(new UpdateWrapper().eq(idField, id).set(dbField, value))); + } + + @GetMapping("/execute") + @ResponseBody + public void execute(Integer id) throws ClassNotFoundException, NoSuchMethodException, SecurityException, IllegalAccessException, InvocationTargetException { + UpdatePlan updatePlan = updatePlanService.getById(id); + updatePlanService.execute(updatePlan); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cf30286..50157c3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,10 +27,10 @@ mybatis-plus: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging.level: - link.at17.mid.tushare: info + link.at17.mid.tushare: debug link.at17.mid.tushare.test: debug - org.springframework.security: debug - org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager: trace + # org.springframework.security: debug + # org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager: trace spring: devtools: @@ -50,7 +50,7 @@ spring: profiles.active: local session.timeout: 86400 thymeleaf: - prefix: classpath:/webpage/ + prefix: classpath:/templates/ suffix: .html mode: HTML encoding: UTF-8 diff --git a/src/main/resources/conf/system/system.json b/src/main/resources/conf/system/system.json index 2f194a4..614c2b4 100644 --- a/src/main/resources/conf/system/system.json +++ b/src/main/resources/conf/system/system.json @@ -1,8 +1,8 @@ { - "tushareToken" : "123", - "proxyType" : "SOCKS", + "tushareToken" : "6f284d9246bad80c3eff946f3ecae8442072b1e60652785f66007509", + "proxyType" : "DIRECT", "proxyHost" : "", "proxyPort" : 1, - "ignoreHttpsVerification" : true, - "proxyUrl" : "socks5://:1" + "ignoreHttpsVerification" : false, + "proxyUrl" : null } \ No newline at end of file diff --git a/src/main/resources/mappers/StockAdjust.xml b/src/main/resources/mappers/StockAdjust.xml index 7db528f..c2a19ac 100644 --- a/src/main/resources/mappers/StockAdjust.xml +++ b/src/main/resources/mappers/StockAdjust.xml @@ -3,7 +3,7 @@ "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > - + INSERT INTO stock_adjust_factor_tushare (ts_code, trade_date, adj_factor) VALUES diff --git a/src/main/resources/mappers/StockCalendar.xml b/src/main/resources/mappers/StockCalendar.xml index 1aa02c1..070cb1c 100644 --- a/src/main/resources/mappers/StockCalendar.xml +++ b/src/main/resources/mappers/StockCalendar.xml @@ -123,7 +123,7 @@ - SELECT DISTINCT "date" FROM diff --git a/src/main/resources/mappers/StockDaily.xml b/src/main/resources/mappers/StockDaily.xml index 6ed8b4b..03a4607 100644 --- a/src/main/resources/mappers/StockDaily.xml +++ b/src/main/resources/mappers/StockDaily.xml @@ -101,7 +101,7 @@ d.vol, d.amount, d.pct_chg, d."change" - SELECT FROM @@ -157,7 +157,7 @@ s.trade_date ASC - SELECT * FROM ( SELECT @@ -207,7 +207,7 @@ - SELECT FROM @@ -254,7 +254,7 @@ - SELECT , b.turnover_rate AS turnoverRate, @@ -325,7 +325,7 @@ - SELECT * FROM ( SELECT , @@ -398,7 +398,7 @@ ) f ORDER BY f.trade_date ASC - SELECT * FROM ( SELECT d.*, d.trade_date as "date", diff --git a/src/main/resources/mappers/StockHolder.xml b/src/main/resources/mappers/StockHolder.xml index 2844f2f..f017d2f 100644 --- a/src/main/resources/mappers/StockHolder.xml +++ b/src/main/resources/mappers/StockHolder.xml @@ -3,7 +3,7 @@ "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > - + INSERT INTO stock_holder (ts_code, ann_date, end_date, holder_name, hold_amount, hold_ratio, holder_type) VALUES diff --git a/src/main/resources/mappers/StockInfo.xml b/src/main/resources/mappers/StockInfo.xml index a0e5499..cdcf1dc 100644 --- a/src/main/resources/mappers/StockInfo.xml +++ b/src/main/resources/mappers/StockInfo.xml @@ -36,12 +36,12 @@ stock_info i - INNER JOIN stock_ths_member m ON (m.con_code = i.ts_code + LEFT JOIN stock_ths_member m ON (m.con_code = i.ts_code AND m.con_code = #{stockCode} ) - INNER JOIN stock_ths_list l ON l.ts_code = m.ts_code + LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code WHERE 1 = 1 @@ -56,9 +56,9 @@ i.*, STRING_AGG(l."name", ', ' ORDER BY l.list_date DESC) AS ths_belongings FROM - stock_info i - INNER JOIN stock_ths_member m ON m.con_code = i.ts_code - INNER JOIN stock_ths_list l ON l.ts_code = m.ts_code + stock_info i + LEFT JOIN stock_ths_member m ON m.con_code = i.ts_code + LEFT JOIN stock_ths_list l ON l.ts_code = m.ts_code WHERE 1 = 1 @@ -67,4 +67,34 @@ GROUP BY i.ts_code ORDER BY i.ts_code ASC + + + + diff --git a/src/main/resources/mappers/StockLimit.xml b/src/main/resources/mappers/StockLimit.xml index 9e378a4..b94dd44 100644 --- a/src/main/resources/mappers/StockLimit.xml +++ b/src/main/resources/mappers/StockLimit.xml @@ -3,7 +3,7 @@ "https://mybatis.org/dtd/mybatis-3-mapper.dtd" > - + INSERT INTO stock_limit (trade_date, ts_code, industry, name, close, pct_chg, amount, limit_amount, float_mv, total_mv, turnover_ratio, fd_amount, first_time, last_time, open_times, up_stat, limit_times, "limit") VALUES diff --git a/src/main/resources/mappers/StockMinute.xml b/src/main/resources/mappers/StockMinute.xml index cf772ee..c54d139 100644 --- a/src/main/resources/mappers/StockMinute.xml +++ b/src/main/resources/mappers/StockMinute.xml @@ -108,7 +108,7 @@ m.vol, m.amount, m.freq - SELECT FROM @@ -165,7 +165,7 @@ m.trade_time ASC - SELECT * FROM ( SELECT @@ -216,7 +216,7 @@ ) f ORDER BY f.date ASC - SELECT * FROM ( SELECT , diff --git a/src/main/resources/mappers/StockThsDaily.xml b/src/main/resources/mappers/ThsDaily.xml similarity index 95% rename from src/main/resources/mappers/StockThsDaily.xml rename to src/main/resources/mappers/ThsDaily.xml index 44f6cc3..3ba385b 100644 --- a/src/main/resources/mappers/StockThsDaily.xml +++ b/src/main/resources/mappers/ThsDaily.xml @@ -1,7 +1,7 @@ - + INSERT INTO stock_ths_daily diff --git a/src/main/resources/mappers/StockThsList.xml b/src/main/resources/mappers/ThsList.xml similarity index 90% rename from src/main/resources/mappers/StockThsList.xml rename to src/main/resources/mappers/ThsList.xml index 838721e..08035a3 100644 --- a/src/main/resources/mappers/StockThsList.xml +++ b/src/main/resources/mappers/ThsList.xml @@ -1,7 +1,7 @@ - + INSERT INTO stock_ths_list diff --git a/src/main/resources/mappers/StockThsMember.xml b/src/main/resources/mappers/ThsMember.xml similarity index 93% rename from src/main/resources/mappers/StockThsMember.xml rename to src/main/resources/mappers/ThsMember.xml index b6d9fc9..95e5247 100644 --- a/src/main/resources/mappers/StockThsMember.xml +++ b/src/main/resources/mappers/ThsMember.xml @@ -1,7 +1,7 @@ - + INSERT INTO stock_ths_member diff --git a/src/main/resources/postgresql-verich-public-structure.sql b/src/main/resources/postgresql-verich-public-structure.sql index 207f4ad..a785f3a 100644 --- a/src/main/resources/postgresql-verich-public-structure.sql +++ b/src/main/resources/postgresql-verich-public-structure.sql @@ -12,10 +12,21 @@ Target Server Version : 170000 (170000) File Encoding : 65001 - Date: 14/10/2025 15:08:02 + Date: 27/10/2025 23:25:57 */ +-- ---------------------------- +-- Sequence structure for update_plan_id_seq +-- ---------------------------- +DROP SEQUENCE IF EXISTS "public"."update_plan_id_seq"; +CREATE SEQUENCE "public"."update_plan_id_seq" +INCREMENT 1 +MINVALUE 1 +MAXVALUE 2147483647 +START 1 +CACHE 1; + -- ---------------------------- -- Table structure for stock_adjust_factor_tushare -- ---------------------------- @@ -106,7 +117,7 @@ DROP TABLE IF EXISTS "public"."stock_holder"; CREATE TABLE "public"."stock_holder" ( "ts_code" varchar(11) COLLATE "pg_catalog"."default" NOT NULL, "ann_date" date NOT NULL, - "end_date" date NOT NULL, + "end_date" timestamp(0) NOT NULL, "holder_name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, "hold_amount" numeric(23,3) NOT NULL, "hold_ratio" numeric(23,3) DEFAULT NULL::numeric, @@ -199,7 +210,7 @@ CREATE TABLE "public"."stock_minute" ( DROP TABLE IF EXISTS "public"."stock_ths_daily"; CREATE TABLE "public"."stock_ths_daily" ( "ts_code" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, - "trade_date" date NOT NULL, + "trade_date" timestamp(0) NOT NULL, "close" numeric(23,3) DEFAULT NULL::numeric, "open" numeric(23,3) DEFAULT NULL::numeric, "high" numeric(23,3) DEFAULT NULL::numeric, @@ -224,7 +235,7 @@ CREATE TABLE "public"."stock_ths_list" ( "name" varchar(255) COLLATE "pg_catalog"."default", "count" int4, "exchange" varchar(255) COLLATE "pg_catalog"."default", - "list_date" date, + "list_date" timestamp(0), "type" varchar(5) COLLATE "pg_catalog"."default" ) ; @@ -244,6 +255,26 @@ CREATE TABLE "public"."stock_ths_member" ( ) ; +-- ---------------------------- +-- Table structure for update_plan +-- ---------------------------- +DROP TABLE IF EXISTS "public"."update_plan"; +CREATE TABLE "public"."update_plan" ( + "id" int4 NOT NULL GENERATED BY DEFAULT AS IDENTITY ( +INCREMENT 1 +MINVALUE 1 +MAXVALUE 2147483647 +START 1 +CACHE 1 +), + "cron_expr" varchar(255) COLLATE "pg_catalog"."default", + "name" varchar(255) COLLATE "pg_catalog"."default", + "methods" text COLLATE "pg_catalog"."default", + "open_day_check" bool, + "enabled" bool +) +; + -- ---------------------------- -- Table structure for web_user -- ---------------------------- @@ -258,6 +289,13 @@ CREATE TABLE "public"."web_user" ( ) ; +-- ---------------------------- +-- Alter sequences owned by +-- ---------------------------- +ALTER SEQUENCE "public"."update_plan_id_seq" +OWNED BY "public"."update_plan"."id"; +SELECT setval('"public"."update_plan_id_seq"', 2, true); + -- ---------------------------- -- Primary Key structure for table stock_adjust_factor_tushare -- ---------------------------- @@ -341,6 +379,11 @@ ALTER TABLE "public"."stock_ths_list" ADD CONSTRAINT "stock_ths_list_pkey" PRIMA -- ---------------------------- ALTER TABLE "public"."stock_ths_member" ADD CONSTRAINT "stock_ths_member_pkey" PRIMARY KEY ("ts_code", "con_code", "con_name"); +-- ---------------------------- +-- Primary Key structure for table update_plan +-- ---------------------------- +ALTER TABLE "public"."update_plan" ADD CONSTRAINT "update_plan_pkey" PRIMARY KEY ("id"); + -- ---------------------------- -- Primary Key structure for table web_user -- ---------------------------- diff --git a/src/main/resources/static/admin/res/adminui/dist/css/res/logo.png b/src/main/resources/static/admin/res/adminui/dist/css/res/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c52a28d2f97f3ffb2441033bcff48bafa028b1cc GIT binary patch literal 3562 zcmcIn2UHW;8V;a{1%U{g(N&ANUWgaRg~gXk z@sL2ygYh9!1&T%|TZbqhegKRBJXj=_v9TTbg;+o=U}HV093n?y5BrIoLY469P**NL zG=R?(V6B$|Rv|2uKnf!e5F(YxRICs-c8HgSwl%{9EHDH?0@zqvjX}VR;||!%l`z1> zgE&5sNCN0gJVb+NWF~D1Kq8VT1R|LLl5j*ii^5=$$iVOqi<(mkge(t-Wy8kM9vkb2 zAPN?N5F8wg4<_T~N)Z8MGMNM-i9jOZPy|l3NrpfnIGJkR2!jKx;w!}pL@bv98b*jG z4@B5lRO(kBq>52mnQAyrXv7F1kb(f>i5j0qfE>8f=1Bp10!UgFp zGMz;tSr8d4BJnGfgI0n7LZCl@saz0bk;yC~_#a@oKrGz!cc2^&%UPyEAQ>Nac3@*s zQFyUfz~T{kAdN|5;wVIjiK8+pFpkF{3ULsX!XT5W5Q9mk4CzAWJIMKgn)qqve=S^r zoR4yhhKL1I_(BqyM8?68fQMs3C>w(eQgL(!Oe8WXBnX6rBW!L;F}ijj$?sV;sst#b zkRqV*g$z1QAmA}@FpUD@cq9QI?UMLB5-g+%X>=?g;Io8sr4&L7MJ$CxFhL;`VS#_E zv6oBaN{(EB#+!~ZEpyg zJ@adk5&l?zL+^gKzoMxe8bUX1O>^|PKs%!c4lF|hr9_XDH48uD&{L|{*}<0U*U+wR zjPdoHQdStyUQosC6Pc3_xTtT>m)g~ydVHUj!-<$;V&t(!%t>8mw)6wn@Tbl>od$-b zo^u?|gnw{1)R2xuqfAM7S~aU9Kp# zsz0TNe<=|EEV8U~Bl%WYbeTA;+PFoVN}~<5l(&X3^Qa&@v98Eg1`IYQCk~;vX^a`d*XHV zPKMR~+ZLy~T?z*-WW4G>@_KsKo!93ueP5)NX=S~V%iRMH4qs}_3QQ|WKX0Xt9ID?> zD0nCn0B}9JcG3<Bp(j%BZ+4S!K%yN^i}^3v%4u0#;*YI958{%R7|Vel|8|iEi8K9cSiGyz%kDJNe@B zoV%0P+v+G5S0<{Rdl6HgV>;%~sgBWEq7Z!2f>4i%9$`D*+3j|jb;sDZ&Ny3qtZn@m z82p(2Df8jSR@2rQCs;q%+LKOyo-_X9>DCuNhzhzR1#58FGa?6%gjL3y=(rHSrmIsV#)pA+j>5xj3;cC1<6FlOCY1+Op0cTqpRg=L+w z@(4KZMb19Hoq_Mk6Sc3N7ku~WO2Vw&gYHJ@#a%?*gK5Z-WJQaRn;_zOfFJI;eA$~j zsN8){c1bH;8XGG1YDfxLH{ertEwlEl$$pCo6LSk{zJIWOHGzP$lhmXq~~vQZDvHqoT%@#4d-XyJl)$(SYnd-%D`f+PRX0e z7MvSlEvvtjl>OGy8yGjO(`#P4pSkww^4iA6^IMuD-W-gzr=?`BB9M$!8~NXY;3lvQxQrK?>H|hqHb8m$~l?H~TjW`psEdM4)bK-Q0+a zOU&-XxKZ8inv`#-v!iHpjT!Qw56g;J zQt+OgXOS>eHEo$eOmlCa8v;yt)@s`l^4_MSm}k(gO<_N`4DI`LXlh=ILGP;9326t5 z2HHz>oF*Tpy}z)m2kfGPbwa z-rWch_URjN~*KEADz@&1Sa+4A=aG1Id^=C;R&T!uu)b52jDrAbcmOCEuQd z+$oHRjIU6Kg+D4xZ>$+pyxYNCCNVD35$136coNwVxfSzv+bmlXHT-=>vF2;c+0oVE JqTP?-{{ng_vkw3O literal 0 HcmV?d00001 diff --git a/src/main/resources/static/admin/res/modules/cron.js b/src/main/resources/static/admin/res/modules/cron.js new file mode 100644 index 0000000..4b4191b --- /dev/null +++ b/src/main/resources/static/admin/res/modules/cron.js @@ -0,0 +1,1198 @@ +/** + @ Name:layui.cron Cron表达式解析器 + @ Author:贝哥哥 + @ License:MIT + */ + +layui.define(['lay', 'element', 'form'], +function(exports) { //假如该组件依赖 layui.form + var $ = layui.$, + layer = layui.layer, + lay = layui.lay, + element = layui.element, + form = layui.form + + //字符常量 + , + MOD_NAME = 'cron', + ELEM = '.layui-cron', + THIS = 'layui-this', + SHOW = 'layui-show', + HIDE = 'layui-hide' + + , + ELEM_STATIC = 'layui-cron-static', + ELEM_FOOTER = 'layui-cron-footer', + ELEM_CONFIRM = '.cron-btns-confirm', + ELEM_HINT = 'layui-cron-hint' + + , + ELEM_RUN_HINT = 'layui-cron-run-hint' + + //外部接口 + , + cron = { + v: '2.0.0' // cron 组件当前版本 + , + index: layui.cron ? (layui.cron.index + 10000) : 0 // corn 实例标识 + //设置全局项 + , + set: function(options) { + var that = this; + that.config = $.extend({}, + that.config, options); + return that; + } + + //事件监听 + , + on: function(events, callback) { + return layui.onevent.call(this, MOD_NAME, events, callback); + } + + //主体CSS等待事件 + , + ready: function(fn, that) { + var cssPath = that.config.cssPath; + layui.link(cssPath, fn, "cron"); //此处的“cron”要对应 cron.css 中的样式: html #layuicss-cron{} + return this; + } + } + + //操作当前实例 + , + thisIns = function() { + var that = this, + options = that.config, + id = options.id || options.index; + + return { + //提示框 + hint: function(content) { + that.hint.call(that, content); + }, + config: options + } + } + + //构造器,创建实例 + , + Class = function(options) { + var that = this; + that.index = ++cron.index; + that.config = $.extend({}, + that.config, cron.config, options); + cron.ready(function() { + that.init(); + }, that); + }; + + //默认配置 + Class.prototype.config = { + value: null // 当前表达式值,每秒执行一次 + , + isInitValue: true //用于控制是否自动向元素填充初始值(需配合 value 参数使用) + , + lang: "cn" //语言,只支持cn/en,即中文和英文 + , + tabs: [{ + key: 'seconds', + range: '0-59' + }, + { + key: 'minutes', + range: '0-59' + }, + { + key: 'hours', + range: '0-23' + }, + { + key: 'days', + range: '1-31' + }, + { + key: 'months', + range: '1-12' + }, + { + key: 'weeks', + range: '1-7' + }, + { + key: 'years' + }], + defaultCron: { + seconds: "*", + minutes: "*", + hours: "*", + days: "*", + months: "*", + weeks: "?", + years: "" + }, + trigger: "click" //呼出控件的事件 + , + btns: ['run', 'confirm'] //右下角显示的按钮,会按照数组顺序排列 + , + position: null //控件定位方式定位, 默认absolute,支持:fixed/absolute/static + , + zIndex: null //控件层叠顺序 + , + show: false //是否直接显示,如果设置 true,则默认直接显示控件 + , + showBottom: true //是否显示底部栏 + , + done: null //控件选择完毕后的回调,点击运行/确定也均会触发 + , + run: null // 最近运行时间接口 + , + cssPath: layui.cache.base + "cron/cron.css?v=" + cron.v + }; + + //多语言 + Class.prototype.lang = function() { + var that = this, + options = that.config, + text = { + cn: { + tabs: [{ + title: "秒" + }, + { + title: "分" + }, + { + title: "时" + }, + { + title: "日" + }, + { + title: "月" + }, + { + title: "周", + rateBegin: "第", + rateMid: "周的星期", + rateEnd: "" + }, + { + title: "年" + }], + every: "每", + unspecified: "不指定", + period: "周期", + periodFrom: "从", + rate: "按照", + rateBegin: "从", + rateMid: "开始,每", + rateEnd: "执行一次", + weekday: "工作日", + weekdayPrefix: "每月", + weekdaySuffix: "号最近的那个工作日", + lastday: "本月最后一日", + lastweek: "本月最后一个星期", + custom: "指定", + tools: { + confirm: '确定', + run: '运行' + }, + formatError: ['Cron格式不合法', '
        已为你重置'] + }, + en: { + tabs: [{ + title: "Seconds" + }, + { + title: "Minutes" + }, + { + title: "Hours" + }, + { + title: "Days" + }, + { + title: "Months" + }, + { + title: "Weeks" + }, + { + title: "Years" + }], + every: "Every ", + unspecified: "Unspecified", + period: "Period", + periodFrom: "From", + rate: "According to", + rateBegin: "begin at", + rateMid: ", every", + rateEnd: " execute once", + weekday: "Weekday", + weekdayPrefix: "Every month at ", + weekdaySuffix: "号最近的那个工作日", + lastday: "Last day of the month", + lastweek: "本月最后一个星期", + custom: "Custom", + tools: { + confirm: 'Confirm', + run: 'Run' + }, + formatError: ['The cron format error', '
        It has been reset'] + } + }; + return text[options.lang] || text['cn']; + }; + + //初始准备 + Class.prototype.init = function() { + var that = this, + options = that.config, + isStatic = options.position === 'static'; + + options.elem = lay(options.elem); + + options.eventElem = lay(options.eventElem); + + if (!options.elem[0]) return; + + //如果不是input|textarea元素,则默认采用click事件 + if (!that.isInput(options.elem[0])) { + if (options.trigger === 'focus') { + options.trigger = 'click'; + } + } + + // 设置渲染所绑定元素的唯一KEY + if (!options.elem.attr('lay-key')) { + options.elem.attr('lay-key', that.index); + options.eventElem.attr('lay-key', that.index); + } + + // 当前实例主面板ID + that.elemID = 'layui-icon' + options.elem.attr('lay-key'); + + //默认赋值 + if (options.value && options.isInitValue) { + that.setValue(options.value); + } + if (!options.value) { + options.value = options.elem[0].value || ''; + } + options.cron = getCronObj(options.value) || lay.extend({}, options.defaultCron); + + if (options.show || isStatic) that.render(); + isStatic || that.events(); + + }; + + function getCronObj(value) { + var cronArr = value.split(' '); + if (cronArr.length >= 6) { + return { + seconds: cronArr[0], + minutes: cronArr[1], + hours: cronArr[2], + days: cronArr[3], + months: cronArr[4], + weeks: cronArr[5], + years: "", + }; + } + return null + } + + // 控件主体渲染 + Class.prototype.render = function() { + var that = this, + options = that.config, + lang = that.lang(), + isStatic = options.position === 'static', + tabFilter = 'cron-tab' + options.elem.attr('lay-key') + //主面板 + , + elem = that.elem = lay.elem('div', { + id: that.elemID, + 'class': ['layui-cron', isStatic ? (' ' + ELEM_STATIC) : ''].join('') + }) + // tab 内容区域 + , + elemTab = that.elemTab = lay.elem('div', { + 'class': 'layui-tab layui-tab-card', + 'lay-filter': tabFilter + }), + tabHead = lay.elem('ul', { + 'class': 'layui-tab-title' + }), + tabContent = lay.elem('div', { + 'class': 'layui-tab-content' + }) + + //底部区域 + , + divFooter = that.footer = lay.elem('div', { + 'class': ELEM_FOOTER + }); + + + options.cron = getCronObj(this.bindElem.value) || lay.extend({}, options.defaultCron); + if (options.zIndex) elem.style.zIndex = options.zIndex; + + // 生成tab 内容区域 + elemTab.appendChild(tabHead); + elemTab.appendChild(tabContent); + lay.each(lang.tabs, + function(i, item) { + // 表头 + var li = lay.elem('li', { + 'class': i === 0 ? THIS: "", + 'lay-id': i + }); + li.innerHTML = item.title; + tabHead.appendChild(li); + + // 表体 + tabContent.appendChild(that.getTabContentChildElem(i)); + }); + + // 主区域 + elemMain = that.elemMain = lay.elem('div', { + 'class': 'layui-cron-main' + }); + elemMain.appendChild(elemTab); + + //生成底部栏 + lay(divFooter).html(function() { + var html = [], + btns = []; + lay.each(options.btns, + function(i, item) { + var title = lang.tools[item] || 'btn'; + btns.push('' + title + ''); + }); + html.push('

        '); + return html.join(''); + } ()); + + //插入到主区域 + elem.appendChild(elemMain); + + options.showBottom && elem.appendChild(divFooter); + + //移除上一个控件 + that.remove(Class.thisElemCron); + + //如果是静态定位,则插入到指定的容器中,否则,插入到body + isStatic ? options.elem.append(elem) : (document.body.appendChild(elem), that.position()); + + that.checkCron(); + + that.elemEvent(); // 主面板事件 + Class.thisElemCron = that.elemID; + + form.render(); + + } + + // 渲染 tab 子控件 + Class.prototype.getTabContentChildElem = function(index) { + var that = this, + options = that.config, + tabItem = options.tabs[index], + tabItemKey = tabItem.key, + lang = that.lang(), + tabItemLang = lang.tabs[index], + cron = options.cron, + formFilter = 'cronForm' + tabItemKey + options.elem.attr('lay-key'), + data = function() { + if (cron[tabItemKey].indexOf('-') != -1) { + // 周期数据 + var arr = cron[tabItemKey].split('-'); + return { + type: 'range', + start: arr[0], + end: arr[1] + }; + } + if (cron[tabItemKey].indexOf('/') != -1) { + // 频率数据 + var arr = cron[tabItemKey].split('/'); + return { + type: 'rate', + begin: arr[0], + rate: arr[1] + }; + } + if (cron[tabItemKey].indexOf(',') != -1 || /^\+?[0-9][0-9]*$/.test(cron[tabItemKey])) { + // 按照指定执行 + var arr = cron[tabItemKey].split(',').map(Number); + return { + type: 'custom', + values: arr + }; + } + if (cron[tabItemKey].indexOf('W') != -1) { + // 最近的工作日 + var value = cron[tabItemKey].replace('W', ''); + return { + type: 'weekday', + value: value + }; + } + if (index === 3 && cron[tabItemKey] === 'L') { + // 本月最后一日 + return { + type: 'lastday', + value: 'L' + }; + } + if (index === 5 && cron[tabItemKey].indexOf('L') != -1) { + // 本月最后一个周 value + var value = cron[tabItemKey].replace('L', ''); + return { + type: 'lastweek', + value: value + }; + } + if (cron[tabItemKey] === '*') { + // 每次 + return { + type: 'every', + value: '*' + }; + } + if (cron[tabItemKey] === '?' || cron[tabItemKey] === undefined || cron[tabItemKey] === '') { + // 不指定 + return { + type: 'unspecified', + value: cron[tabItemKey] + }; + } + } (), + rangeData = function() { + if (tabItem.range) { + var arr = tabItem.range.split('-'); + return { + min: parseInt(arr[0]), + max: parseInt(arr[1]) + }; + } + } (); + var elem = lay.elem('div', { + 'class': 'layui-tab-item layui-form ' + (index === 0 ? SHOW: ""), + 'lay-filter': formFilter + }); + + // 每次 + elem.appendChild(function() { + var everyRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'every', + 'title': lang.every + tabItemLang.title + }); + if (data.type === 'every') { + lay(everyRadio).attr('checked', true); + } + var everyDiv = lay.elem('div', { + 'class': 'cron-row' + }); + everyDiv.appendChild(everyRadio); + return everyDiv; + } ()); + + // 不指定,从日开始 + if (index >= 3) { + elem.appendChild(function() { + var unspecifiedRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'unspecified', + 'title': lang.unspecified + }); + if (data.type === 'unspecified') { + lay(unspecifiedRadio).attr('checked', true); + } + var unspecifiedDiv = lay.elem('div', { + 'class': 'cron-row' + }); + unspecifiedDiv.appendChild(unspecifiedRadio); + return unspecifiedDiv; + } ()); + } + + // 周期 + var rangeChild = [function() { + var rangeRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'range', + 'title': lang.period + }); + if (data.type === 'range') { + lay(rangeRadio).attr('checked', true); + } + return rangeRadio; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = lang.periodFrom; + return elem; + } (), + function() { + var elem = lay.elem('input', { + 'class': 'cron-input', + 'type': 'number', + 'name': 'rangeStart', + 'value': data.start || '' + }); + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = '-'; + return elem; + } (), + function() { + var elem = lay.elem('input', { + 'class': 'cron-input', + 'type': 'number', + 'name': 'rangeEnd', + 'value': data.end || '' + }); + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = tabItemLang.title; + return elem; + } ()] + + , + rangeDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(rangeChild, + function(i, item) { + rangeDiv.appendChild(item); + }); + if (tabItem.range) { + var rangeTip = lay.elem('div', { + 'class': 'cron-tips' + }); + rangeTip.innerHTML = ['(', tabItem.range, ')'].join(''); + rangeDiv.appendChild(rangeTip); + } + elem.appendChild(rangeDiv); + + // 频率,年没有 + if (index < 6) { + var rateChild = [function() { + var rateRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'rate', + 'title': lang.rate + }); + if (data.type === 'rate') { + lay(rateRadio).attr('checked', true); + } + return rateRadio; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = tabItemLang.rateBegin || lang.rateBegin; + return elem; + } (), + function() { + var elem = lay.elem('input', { + 'class': 'cron-input', + 'type': 'number', + 'name': 'begin', + 'value': data.begin || '' + }); + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = tabItemLang.rateMid || (tabItemLang.title + lang.rateMid); + return elem; + } (), + function() { + var elem = lay.elem('input', { + 'class': 'cron-input', + 'type': 'number', + 'name': 'rate', + 'value': data.rate || '' + }); + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = undefined != tabItemLang.rateEnd ? tabItemLang.rateEnd: (tabItemLang.title + lang.rateEnd); + if (undefined != tabItemLang.rateEnd && tabItemLang.rateEnd === '') { + lay(elem).addClass(HIDE); + } + return elem; + } ()] + + , + rateDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(rateChild, + function(i, item) { + rateDiv.appendChild(item); + }); + if (tabItem.range) { + var rateTip = lay.elem('div', { + 'class': 'cron-tips' + }); + if (index === 5) { + // 周 + rateTip.innerHTML = '(1-4/1-7)'; + } else { + rateTip.innerHTML = ['(', rangeData.min, '/', (rangeData.max + (index <= 2 ? 1 : 0)), ')'].join(''); + } + rateDiv.appendChild(rateTip); + } + elem.appendChild(rateDiv); + } + + // 特殊:日(最近的工作日、最后一日),周(最后一周) + if (index === 3) { + // 日 + // 最近的工作日 + var weekChild = [function() { + var weekRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'weekday', + 'title': lang.weekday + }); + if (data.type === 'weekday') { + lay(weekRadio).attr('checked', true); + } + return weekRadio; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = lang.weekdayPrefix; + return elem; + } (), + function() { + var elem = lay.elem('input', { + 'class': 'cron-input', + 'type': 'number', + 'name': 'weekday', + 'value': data.value || '' + }); + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-input-mid' + }); + elem.innerHTML = lang.weekdaySuffix; + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-tips' + }); + elem.innerHTML = ['(', tabItem.range, ')'].join(''); + return elem; + } ()] + + , + weekDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(weekChild, + function(i, item) { + weekDiv.appendChild(item); + }); + elem.appendChild(weekDiv); + + // 本月最后一日 + elem.appendChild(function() { + var lastRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'lastday', + 'title': lang.lastday + }); + if (data.type === 'lastday') { + lay(lastRadio).attr('checked', true); + } + var lastDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lastDiv.appendChild(lastRadio); + return lastDiv; + } ()); + + } + + if (index === 5) { + // 本月最后一个周几 + var lastWeekChild = [function() { + var lastWeekRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'lastweek', + 'title': lang.lastweek + }); + if (data.type === 'lastweek') { + lay(lastWeekRadio).attr('checked', true); + } + return lastWeekRadio; + } (), + function() { + var elem = lay.elem('input', { + 'class': 'cron-input', + 'type': 'number', + 'name': 'lastweek', + 'value': data.value || '' + }); + return elem; + } (), + function() { + var elem = lay.elem('div', { + 'class': 'cron-tips' + }); + elem.innerHTML = ['(', tabItem.range, ')'].join(''); + return elem; + } ()] + + , + lastWeekDiv = lay.elem('div', { + 'class': 'cron-row' + }); + lay.each(lastWeekChild, + function(i, item) { + lastWeekDiv.appendChild(item); + }); + elem.appendChild(lastWeekDiv); + + } + + // 指定 + if (index <= 5) { + elem.appendChild(function() { + var customRadio = lay.elem('input', { + 'name': tabItemKey + '[type]', + 'type': 'radio', + 'value': 'custom', + 'title': lang.custom + }); + if (data.type === 'custom') { + lay(customRadio).attr('checked', true); + } + var customDiv = lay.elem('div', { + 'class': 'cron-row' + }); + customDiv.appendChild(customRadio); + return customDiv; + } ()); + + // 指定数值,时分秒显示两位数,自动补零 + elem.appendChild(function() { + var customGrid = lay.elem('div', { + 'class': 'cron-grid' + }); + var i = rangeData.min; + while (i <= rangeData.max) { + // 时分秒显示两位数,自动补零 + var gridItemValue = index <= 2 ? lay.digit(i, 2) : i; + var gridItem = lay.elem('input', { + 'type': 'checkbox', + 'title': gridItemValue, + 'lay-skin': 'primary', + 'name': tabItemKey + '[custom]', + 'value': i + }); + if (data.values && data.values.includes(i)) { + lay(gridItem).attr('checked', true); + } + customGrid.appendChild(gridItem); + i++; + } + return customGrid; + } ()); + } + + return elem; + } + + //是否输入框 + Class.prototype.isInput = function(elem) { + return /input|textarea/.test(elem.tagName.toLocaleLowerCase()); + }; + + // 绑定的元素事件处理 + Class.prototype.events = function() { + var that = this, + options = that.config + + //绑定呼出控件事件 + , + showEvent = function(elem, bind) { + elem.on(options.trigger, + function() { + bind && (that.bindElem = this); + that.render(); + }); + }; + + if (!options.elem[0] || options.elem[0].eventHandler) return; + + showEvent(options.elem, 'bind'); + showEvent(options.eventElem); + + //绑定关闭控件事件 + lay(document).on('click', + function(e) { + if (e.target === options.elem[0] || e.target === options.eventElem[0] || e.target === lay(options.closeStop)[0]) { + return; + } + that.remove(); + }).on('keydown', + function(e) { + if (e.keyCode === 13) { + if (lay('#' + that.elemID)[0] && that.elemID === Class.thisElemDate) { + e.preventDefault(); + lay(that.footer).find(ELEM_CONFIRM)[0].click(); + } + } + }); + + //自适应定位 + lay(window).on('resize', + function() { + if (!that.elem || !lay(ELEM)[0]) { + return false; + } + that.position(); + }); + + options.elem[0].eventHandler = true; + }; + + // 主面板事件 + Class.prototype.elemEvent = function() { + var that = this, + options = that.config, + tabFilter = 'cron-tab' + options.elem.attr('lay-key'); + + // 阻止主面板点击冒泡,避免因触发文档事件而关闭主面 + lay(that.elem).on('click', + function(e) { + lay.stope(e); + }); + + // tab选项卡切换 + var lis = lay(that.elemTab).find('li'); + lis.on('click', + function() { + var layid = lay(this).attr('lay-id'); + if (undefined === layid) { + return; + } + element.tabChange(tabFilter, layid); + }); + + // cron选项点击 + form.on('radio', + function(data) { + var $parent = data.othis.parent(); + var formFilter = $parent.parent().attr('lay-filter'); + var formData = form.val(formFilter); + var radioType = data.value; + if ('range' === radioType) { + // 范围 + form.val(formFilter, { + rangeStart: formData.rangeStart || 0, + rangeEnd: formData.rangeEnd || 2 + }); + } + if ('rate' === radioType) { + // 频率 + form.val(formFilter, { + begin: formData.begin || 0, + rate: formData.rate || 2 + }); + } + if ('custom' === radioType) { + // custom + var $grid = $parent.next(); + if ($grid.find(':checkbox:checked').length <= 0) { + $grid.children(':checkbox:first').next().click() + } + } + if ('weekday' === radioType) { + // weekday + form.val(formFilter, { + weekday: formData.weekday || 1 + }); + } + if ('lastweek' === radioType) { + // lastweek + form.val(formFilter, { + lastweek: formData.lastweek || 1 + }); + } + + }); + + //点击底部按钮 + lay(that.footer).find('span').on('click', + function() { + var type = lay(this).attr('lay-type'); + that.tool(this, type); + }); + }; + + //底部按钮点击事件 + Class.prototype.tool = function(btn, type) { + var that = this, + options = that.config, + lang = that.lang(), + isStatic = options.position === 'static', + active = { + //运行 + run: function() { + var value = that.parse(); + var loading = layer.load(); + $.get(options.run, { + cron: value + }, + function(res) { + layer.close(loading); + if (res.code !== 0) { + return that.hint(res.msg); + } + that.runHint(res.data); + }, + 'json').fail(function() { + layer.close(loading); + that.hint('服务器异常!'); + }); + } + + //确定 + , + confirm: function() { + var value = that.parse(); + that.done([value]); + that.setValue(value).remove() + } + }; + active[type] && active[type](); + }; + + //执行 done/change 回调 + Class.prototype.done = function(param, type) { + var that = this, + options = that.config; + + param = param || [that.parse()]; + typeof options[type || 'done'] === 'function' && options[type || 'done'].apply(options, param); + + return that; + }; + + // 解析cron表达式 + Class.prototype.parse = function() { + var that = this, + options = that.config, + valueArr = []; + + lay.each(options.tabs, + function(index, item) { + var key = item.key; + var formFilter = 'cronForm' + key + options.elem.attr('lay-key'); + var formData = form.val(formFilter); + var radioType = (key + '[type]'); + var current = ""; + if (formData[radioType] === 'every') { + // 每次 + current = "*"; + } + if (formData[radioType] === 'range') { + // 范围 + current = formData.rangeStart + "-" + formData.rangeEnd; + } + if (formData[radioType] === 'rate') { + // 频率 + current = formData.begin + "/" + formData.rate; + } + if (formData[radioType] === 'custom') { + // 指定 + var checkboxName = (item.key + '[custom]'); + var customArr = []; + $('input[name="' + checkboxName + '"]:checked').each(function() { + customArr.push($(this).val()); + }); + current = customArr.join(','); + } + if (formData[radioType] === 'weekday') { + // 每月 formData.weekday 号最近的那个工作日 + current = formData.weekday + "W"; + } + if (formData[radioType] === 'lastday') { + // 本月最后一日 + current = "L"; + } + if (formData[radioType] === 'lastweek') { + // 本月最后星期 + current = formData.lastweek + "L"; + } + + if (formData[radioType] === 'unspecified' && index != 6) { + // 不指定 + current = "?"; + } + if (current !== "") { + valueArr.push(current); + options.cron[key] = current; + } + }); + return valueArr.join(' '); + }; + + //控件移除 + Class.prototype.remove = function(prev) { + var that = this, + options = that.config, + elem = lay('#' + (prev || that.elemID)); + if (!elem[0]) return that; + + if (!elem.hasClass(ELEM_STATIC)) { + that.checkCron(function() { + elem.remove(); + }); + } + return that; + }; + + //定位算法 + Class.prototype.position = function() { + var that = this, + options = that.config; + lay.position(that.bindElem || options.elem[0], that.elem, { + position: options.position + }); + return that; + }; + + //提示 + Class.prototype.hint = function(content) { + var that = this, + options = that.config, + div = lay.elem('div', { + 'class': ELEM_HINT + }); + + if (!that.elem) return; + + div.innerHTML = content || ''; + lay(that.elem).find('.' + ELEM_HINT).remove(); + that.elem.appendChild(div); + + clearTimeout(that.hinTimer); + that.hinTimer = setTimeout(function() { + lay(that.elem).find('.' + ELEM_HINT).remove(); + }, + 3000); + }; + + //运行提示 + Class.prototype.runHint = function(runList) { + var that = this, + options = that.config, + div = lay.elem('div', { + 'class': ELEM_RUN_HINT + }); + // debugger; + if (!that.elem || !runList || !runList.length) return; + + lay(div).html(function() { + var html = []; + lay.each(runList, + function(i, item) { + html.push('
        ' + item + '
        '); + }); + return html.join(''); + } ()); + + lay(that.elem).find('.' + ELEM_RUN_HINT).remove(); + that.elem.appendChild(div); + }; + + //赋值 + Class.prototype.setValue = function(value = '') { + var that = this, + options = that.config, + elem = that.bindElem || options.elem[0], + valType = that.isInput(elem) ? 'val': 'html' + + options.position === 'static' || lay(elem)[valType](value || ''); + + return this; + }; + + //cron校验 + Class.prototype.checkCron = function(fn) { + var that = this, + options = that.config, + lang = that.lang(), + elem = that.bindElem || options.elem[0], + value = that.isInput(elem) ? elem.value: (options.position === 'static' ? '': elem.innerHTML) + + , + checkValid = function(value = "") { + +}; + + // cron 值,多个空格替换为一个空格,去掉首尾空格 + value = value || options.value; + if (typeof value === 'string') { + value = value.replace(/\s+/g, ' ').replace(/^\s|\s$/g, ''); + } + + if (fn === 'init') return checkValid(value), + that; + + value = that.parse(); + if (value) { + that.setValue(value); + } + fn && fn(); + return that; + }; + + //核心入口 + cron.render = function(options) { + var ins = new Class(options); + return thisIns.call(ins); + }; + + exports('cron', cron); +}); \ No newline at end of file diff --git a/src/main/resources/static/admin/res/modules/cron/cron.css b/src/main/resources/static/admin/res/modules/cron/cron.css new file mode 100644 index 0000000..caf6bb5 --- /dev/null +++ b/src/main/resources/static/admin/res/modules/cron/cron.css @@ -0,0 +1,195 @@ +/** + @ Name:layui.cron Cron表达式解析器 + @ Author:贝哥哥 + @ License:MIT + */ + + +/* 样式加载完毕的标识 */ +html #layuicss-cron { + display: none; + position: absolute; + width: 1989px; +} + +/* 初始化 */ +.layui-cron * { + margin: 0; + padding: 0; +} + +/* 主体结构 */ +.layui-cron, +.layui-cron * { + box-sizing: border-box; +} + +.layui-cron { + position: absolute; + z-index: 66666666; + margin: 5px 0; + border-radius: 2px; + font-size: 14px; + -webkit-animation-duration: 0.2s; + animation-duration: 0.2s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +/* .layui-cron-main{width: 272px;} */ +.layui-cron-header *, +.layui-cron-content .btn { + transition-duration: .3s; + -webkit-transition-duration: .3s; +} + +/* 微微往下滑入 */ +@keyframes cron-downbit { + 0% { + opacity: 0.3; + transform: translate3d(0, -5px, 0); + } + + 100% { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + + +.layui-cron{animation-name: cron-downbit;} +.layui-cron-static{ position: relative; z-index: 0; display: inline-block; margin: 0; -webkit-animation: none; animation: none;} + + +/* 主体结构 */ +.layui-cron-content{position: relative; padding: 10px; -moz-user-select: none; -webkit-user-select: none; -ms-user-select: none;} + + +/* 底部结构 */ +.layui-cron-footer{position: relative; height: 46px; line-height: 26px; padding: 10px 20px;border-top: 1px solid whitesmoke;} +.layui-cron-footer span{margin-right: 15px; display: inline-block; cursor: pointer; font-size: 12px;} +.layui-cron-footer span:hover{color: #5FB878;} +.cron-footer-btns{position: absolute; right: 10px; top: 10px;} +.cron-footer-btns span{height: 26px; line-height: 26px; margin: 0 0 0 -1px; padding: 0 10px; border: 1px solid #C9C9C9; background-color: #fff; white-space: nowrap; vertical-align: top; border-radius: 2px;} + + +/* 提示 */ +.layui-cron-hint{position: absolute; top: 115px; left: 50%; width: 250px; margin-left: -125px; line-height: 20px; padding: 15px; text-align: center; font-size: 12px; color: #FF5722;} + + +/* 默认简约主题 */ +.layui-cron, .layui-cron-hint{border: 1px solid #d2d2d2; box-shadow: 0 2px 4px rgba(0,0,0,.12); background-color: #fff; color: #666;} +.layui-cron-content{border-top: none 0; border-bottom: none 0;} + +/* tab */ +.layui-cron .layui-tab-card{ + border: none; + box-shadow: none; +} + +.layui-cron .layui-tab-card > .layui-tab-title li{ + min-width: 70px; + margin-left: 0; + margin-right: 0; +} +.layui-cron .layui-tab-content{ + padding: 10px; + height: 230px; + overflow-y: scroll; +} + +/* form */ +.layui-cron .cron-input-mid { + display: inline-block; + vertical-align: middle; + padding: 0 12px; + height: 28px; + line-height: 28px; + box-sizing: border-box; +} +.layui-cron .cron-input { + display: inline-block; + vertical-align: middle; + padding: 0 8px; + background-color: #fff; + border: 1px solid #ccc; + height: 28px; + line-height: 28px; + box-sizing: border-box; + width: 48px; + text-align: right; + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; +} + +/* 谷歌 */ +.layui-cron input::-webkit-outer-spin-button, +.layui-cron input::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +/* 火狐 */ +.layui-cron input[type="number"]{ + -moz-appearance: textfield; +} + +.layui-cron .cron-input:focus { + outline: 0; + border: 1px solid #01AAED; + box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%), 0 0 4px 0px #01aaed; + translate: 1s; +} +.layui-cron .cron-tips { + color: grey; + line-height: 28px; + height: 28px; + display: inline-block; + vertical-align: middle; + margin-left: 5px; +} + +.layui-cron .layui-form-radio{ + margin-right: 10px; +} + +.layui-cron .cron-row{ + display: flex; + align-items: center; +} +.layui-cron .cron-row+.cron-row{ + margin-top: 10px; +} + +.layui-cron .cron-grid{ + display: flex; + flex-wrap: wrap; + align-items: center; + width: 480px; + padding-left: 10px; + padding-top: 4px; +} + +.layui-cron .cron-grid .layui-form-checkbox{ + padding-left: 22px; + margin-bottom: 4px; +} +.layui-cron .cron-grid .layui-form-checkbox[lay-skin="primary"] span{ + padding-right: 13px; + min-width: 29px; +} + +/* 提示 */ +.layui-cron-hint{position: absolute; top: 115px; left: 50%; width: 250px; margin-left: -125px; line-height: 20px; padding: 15px; text-align: center; font-size: 12px; color: #FF5722;} + +.layui-cron-run-hint{ + max-height: 104px; + overflow-y: scroll; + padding: 10px; + padding-top: 0; +} +.cron-run-list+.cron-run-list{ + margin-top: 4px; +} \ No newline at end of file diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..cf3b9cc03528c71d5722768db4b7a16bc3f0bdb8 GIT binary patch literal 5430 zcmeHL*>6-;7{AlbcDI?9(kW$Wr^9qMqQQNoMOw;(f(Rmz5*3I@N?Y2x1s@axQ4vBk zJ{Z(!l*9#Sp{z>ci~oX&QDTf5UNrjPgBln5^Y@*5XF45b+L;0M#U@|R?RU=ke&2S_ z`Q2)oN2}55>NJi~ZM#>~5}Kw(qt4$SP=AoR6m@*$3P;aD)2LrbrAYQCwM0Ws?&Nbh zlD{YS>_Kv=O$k#%B}C?I;&~=9<(Zg?Wpfbj{~G4tPp}5Q=lH{<4Dea$euUls4cfcr zBQ1U|sma+&b_6(g5&p-3z({eCIg7TEou(6K){hP^GPh}%(|Z8ECjfotuLurbqD;7Q zN$LEYo3rUvwKmmJdsz$;Te$BF_?~p|gCl=IBrr0ebfFzzG5)%#*tz`GAbkh(&@~&4 z501j@BmMvo90Z>yF#Dl#B)Xf_npB6xomaQ)GxeDpXya}T4F|7eudl)E{fm+8 z(U7^3d#3n3)rR0N*cpU1WiA`PrZoNB(|^UvjrxdXToR*AUz2rT^T2)I|K=S}dY2v4ZnlBWo{< z!L^Z+SQz8N^DMijB_2Zr~CC`}xAIi?jKeA8GqN^zt)L7z? zp1PV+VUCiW2K#fmGrQDTUZ|%H+A6P>JnI;9yeo!={?8E_ya;>XTgrD;itN3J`u;Bw zU!HW%NOd=qs)cjbws*&2kNggQiTk3|JyJ|BHcZ|xv7YP7z<17rM5l$i9T(Jnr4RA0 zPpvE)_rZa);c`Gk41v0*~{aQTQB(s}RAbjmAgLyXWH! z*y0!Op)~PZSNB5ay?UkiVx#5$?auu)%^f$H6UWD7hYz#*K2yBGsX>MDOYE$Beg6ga zzg1rDvvqNE&~{G`4CXH|B-m2>a(%eOJrgh(d)um2DxcHc?~}3T^@Zz0%eny=!dnD; z=ui3}+^3Wu6VSOv*6Q3dfBmlNu(NsKyQ+F&?H1U>zrioLU_WR1Q{gCcvc?|y11;^LS{{ z9GB@wveS7dgsZgmrZ8*oHiY-=L+H6f2<>?d;pY#aVQ(K|Ycq<^C=Sd$SAv<_OP(Mc zE=7Cuc9{r``pCb?|4wMIT@gNLp4L$Jo)@hHb924$mD=sR0RLLku>dX@s%(E6EFAF#GB7GBWRKl&Ye#jo-bL3}pckASOw0p`> zJkuH@XB{Vp<(My`vyV|nG0regM=i7r28;9JEY5UehX;{YEOJc~)~K$q&C|3`t{Z-b zJ{))PWh564AExd!*U~TH!!mDtWMSSjQjZGqAoV7%_=@YqTZ z77Xet&l0P`J8mJi$&coz*c;^YBFYE)9Xe?vzQ*UCzw~3em}QRm8P;vVa1bW*Xr7=w zO7qh`#o-q%*3dGPhcCGqx5&$Vkh$olTGYL*s~T9x=sUUo!i~h29xkUBxJG(6zSaku zzK7omqHSZX!?$IQcBbGL@)oI8r8cWs@Ag-Q i?}Cym`4+7PKU>%Qu^K!7@A-H7Wtu!Y{}}l1e*X { @@ -378,7 +378,7 @@ layui.use(['table', 'slider', 'element', 'form'], function() { }); break; case 'clearPeriodBlock': - myChart.setOption({series: [{ + myChart.setOption({series: [ { markArea: {data: undefined}}]}); } }); @@ -902,7 +902,7 @@ let toGraphicsIndex = (i, period) => { } } - myChart.setOption({series: [{ + myChart.setOption({series: [ { data: window._values, markArea: {data: data}}]}); } diff --git a/src/main/resources/webpage/admin/login/login.html b/src/main/resources/templates/admin/login/login.html similarity index 100% rename from src/main/resources/webpage/admin/login/login.html rename to src/main/resources/templates/admin/login/login.html diff --git a/src/main/resources/templates/admin/manage/reviews/plans/plan-list.html b/src/main/resources/templates/admin/manage/reviews/plans/plan-list.html new file mode 100644 index 0000000..2bae20c --- /dev/null +++ b/src/main/resources/templates/admin/manage/reviews/plans/plan-list.html @@ -0,0 +1,444 @@ + + + + + layui table 组件综合演示 + + + + + + + + + + +
        +
        +
        +
        +
        综合演示
        +
        +
        + + + + +
        +
        +
        +
        +
        + + + + + + \ No newline at end of file diff --git a/src/main/resources/webpage/admin/manage/reviews/set/system/system.html b/src/main/resources/templates/admin/manage/reviews/set/system/system.html similarity index 100% rename from src/main/resources/webpage/admin/manage/reviews/set/system/system.html rename to src/main/resources/templates/admin/manage/reviews/set/system/system.html diff --git a/src/main/resources/webpage/admin/manage/views/app/content/comment.html b/src/main/resources/templates/admin/manage/views/app/content/comment.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/content/comment.html rename to src/main/resources/templates/admin/manage/views/app/content/comment.html diff --git a/src/main/resources/webpage/admin/manage/views/app/content/list.html b/src/main/resources/templates/admin/manage/views/app/content/list.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/content/list.html rename to src/main/resources/templates/admin/manage/views/app/content/list.html diff --git a/src/main/resources/webpage/admin/manage/views/app/content/tags.html b/src/main/resources/templates/admin/manage/views/app/content/tags.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/content/tags.html rename to src/main/resources/templates/admin/manage/views/app/content/tags.html diff --git a/src/main/resources/webpage/admin/manage/views/app/forum/list.html b/src/main/resources/templates/admin/manage/views/app/forum/list.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/forum/list.html rename to src/main/resources/templates/admin/manage/views/app/forum/list.html diff --git a/src/main/resources/webpage/admin/manage/views/app/forum/replys.html b/src/main/resources/templates/admin/manage/views/app/forum/replys.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/forum/replys.html rename to src/main/resources/templates/admin/manage/views/app/forum/replys.html diff --git a/src/main/resources/webpage/admin/manage/views/app/message/index.html b/src/main/resources/templates/admin/manage/views/app/message/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/message/index.html rename to src/main/resources/templates/admin/manage/views/app/message/index.html diff --git a/src/main/resources/webpage/admin/manage/views/app/workorder/list.html b/src/main/resources/templates/admin/manage/views/app/workorder/list.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/app/workorder/list.html rename to src/main/resources/templates/admin/manage/views/app/workorder/list.html diff --git a/src/main/resources/webpage/admin/manage/views/component/anim/index.html b/src/main/resources/templates/admin/manage/views/component/anim/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/anim/index.html rename to src/main/resources/templates/admin/manage/views/component/anim/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/auxiliar/index.html b/src/main/resources/templates/admin/manage/views/component/auxiliar/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/auxiliar/index.html rename to src/main/resources/templates/admin/manage/views/component/auxiliar/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/badge/index.html b/src/main/resources/templates/admin/manage/views/component/badge/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/badge/index.html rename to src/main/resources/templates/admin/manage/views/component/badge/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/button/index.html b/src/main/resources/templates/admin/manage/views/component/button/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/button/index.html rename to src/main/resources/templates/admin/manage/views/component/button/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/carousel/index.html b/src/main/resources/templates/admin/manage/views/component/carousel/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/carousel/index.html rename to src/main/resources/templates/admin/manage/views/component/carousel/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/code/index.html b/src/main/resources/templates/admin/manage/views/component/code/index.html similarity index 95% rename from src/main/resources/webpage/admin/manage/views/component/code/index.html rename to src/main/resources/templates/admin/manage/views/component/code/index.html index f4de2d2..abd67e7 100644 --- a/src/main/resources/webpage/admin/manage/views/component/code/index.html +++ b/src/main/resources/templates/admin/manage/views/component/code/index.html @@ -175,7 +175,7 @@ test event: function(obj) { dropdown.render({ elem: obj.elem, - data: [{title: '自动换行', wordWrap: true}, {title: '不自动换行', wordWrap: false}], + data: [ {title: '自动换行', wordWrap: true}, {title: '不自动换行', wordWrap: false}], show: true, click: function(data, othis) { codeInst.reload({ diff --git a/src/main/resources/webpage/admin/manage/views/component/colorpicker/index.html b/src/main/resources/templates/admin/manage/views/component/colorpicker/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/colorpicker/index.html rename to src/main/resources/templates/admin/manage/views/component/colorpicker/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/dropdown/index.html b/src/main/resources/templates/admin/manage/views/component/dropdown/index.html similarity index 93% rename from src/main/resources/webpage/admin/manage/views/component/dropdown/index.html rename to src/main/resources/templates/admin/manage/views/component/dropdown/index.html index 94db7b8..f602238 100644 --- a/src/main/resources/webpage/admin/manage/views/component/dropdown/index.html +++ b/src/main/resources/templates/admin/manage/views/component/dropdown/index.html @@ -144,7 +144,7 @@ //初演示 dropdown.render({ elem: '.test-dropdown-demo1' - ,data: [{ + ,data: [ { title: 'menu item11' ,id: 100 },{ @@ -162,7 +162,7 @@ //初演示 - 绑定输入框 dropdown.render({ elem: '#test-dropdown-demo2' - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 101 },{ @@ -190,13 +190,13 @@ //初演示 - 绑定文字 dropdown.render({ elem: '#test-dropdown-demo3' - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 100 },{ title: 'menu item 2' ,id: 101 - ,child: [{ //横向子菜单 + ,child: [ { //横向子菜单 title: 'menu item 2-1' ,id: 1011 },{ @@ -212,7 +212,7 @@ title: 'menu group' ,id: 103 ,type: 'group' //纵向菜单组 - ,child: [{ + ,child: [ { title: 'menu item 4-1' ,id: 1031 },{ @@ -236,7 +236,7 @@ //无限层级 dropdown.render({ elem: '#test-dropdown-demo100' - ,data: [{ + ,data: [ { title: 'menu item 1' ,templet: ' {{d.title}} ' ,id: 100 @@ -253,20 +253,20 @@ title: 'menu item 3' ,id: 102 ,type: 'group' - ,child: [{ + ,child: [ { title: 'menu item 3-1' ,id: 103 },{ title: 'menu item 3-2' ,id: 104 - ,child: [{ + ,child: [ { title: 'menu item 3-2-1' ,id: 105 },{ title: 'menu item 3-2-2' ,id: 11 ,type: 'group' - ,child: [{ + ,child: [ { title: 'menu item 3-2-2-1' ,id: 111 },{ @@ -281,13 +281,13 @@ title: 'menu item 3-3' ,id: 111111 ,type: 'group' - ,child: [{ + ,child: [ { title: 'menu item 3-3-1' ,id: 22 },{ title: 'menu item 3-3-2' ,id: 222 - ,child: [{ + ,child: [ { title: 'menu item 3-3-2-1' ,id: 2222 },{ @@ -310,10 +310,10 @@ },{ title: 'menu item 5' ,id: 5 - ,child: [{ + ,child: [ { title: 'menu item 5-1' ,id: 55 - ,child: [{ + ,child: [ { title: 'menu item 5-1-1' ,id: 5555 },{ @@ -335,7 +335,7 @@ ,id: 66 ,type: 'group' ,isSpreadItem: false - ,child: [{ + ,child: [ { title: 'menu item 6-1' ,id: 888 },{ @@ -355,9 +355,9 @@ // dropdown 在表格中的应用 table.render({ elem: '#test-dropdown-table' - ,url: layui.setter.paths.base + '/json/table/demo.js' + ,url: layui.setter.paths.base + 'json/table/demo.js' ,title: '用户数据表' - ,cols: [[ + ,cols: [ [ {type: 'checkbox', fixed: 'left'} ,{field:'id', title:'ID', width:80, fixed: 'left', unresize: true, sort: true} ,{field:'username', title:'用户名', width:120, edit: 'text'} @@ -387,7 +387,7 @@ dropdown.render({ elem: that ,show: true //外部事件触发即显示 - ,data: [{ + ,data: [ { title: 'item 1' ,id: 'aaa' }, { @@ -418,7 +418,7 @@ dropdown.render({ elem: '#test-dropdown-demo4' ,trigger: 'hover' - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 100 },{ @@ -434,7 +434,7 @@ dropdown.render({ elem: '#test-dropdown-demo5' ,trigger: 'mousedown' - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 100 },{ @@ -450,7 +450,7 @@ dropdown.render({ elem: '#test-dropdown-demo6' ,trigger: 'dblclick' - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 100 },{ @@ -469,7 +469,7 @@ ,isAllowSpread: false //禁止菜单组展开收缩 ,style: 'width: 200px' //定义宽度,默认自适应 ,id: 'test777' //定义唯一索引 - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 'test' }, { @@ -481,7 +481,7 @@ },{type:'-'},{ title: 'menu item 3' ,id: '#3' - ,child: [{ + ,child: [ { title: 'menu item 3-1' ,id: '#1' },{ @@ -515,7 +515,7 @@ //重定义样式 dropdown.render({ elem: '#test-dropdown-demo8' - ,data: [{ + ,data: [ { title: 'menu item 1' ,id: 100 },{ diff --git a/src/main/resources/webpage/admin/manage/views/component/flow/index.html b/src/main/resources/templates/admin/manage/views/component/flow/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/flow/index.html rename to src/main/resources/templates/admin/manage/views/component/flow/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/form/element.html b/src/main/resources/templates/admin/manage/views/component/form/element.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/form/element.html rename to src/main/resources/templates/admin/manage/views/component/form/element.html diff --git a/src/main/resources/webpage/admin/manage/views/component/form/group.html b/src/main/resources/templates/admin/manage/views/component/form/group.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/form/group.html rename to src/main/resources/templates/admin/manage/views/component/form/group.html diff --git a/src/main/resources/webpage/admin/manage/views/component/grid/all.html b/src/main/resources/templates/admin/manage/views/component/grid/all.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/grid/all.html rename to src/main/resources/templates/admin/manage/views/component/grid/all.html diff --git a/src/main/resources/webpage/admin/manage/views/component/grid/list.html b/src/main/resources/templates/admin/manage/views/component/grid/list.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/grid/list.html rename to src/main/resources/templates/admin/manage/views/component/grid/list.html diff --git a/src/main/resources/webpage/admin/manage/views/component/grid/mobile-pc.html b/src/main/resources/templates/admin/manage/views/component/grid/mobile-pc.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/grid/mobile-pc.html rename to src/main/resources/templates/admin/manage/views/component/grid/mobile-pc.html diff --git a/src/main/resources/webpage/admin/manage/views/component/grid/mobile.html b/src/main/resources/templates/admin/manage/views/component/grid/mobile.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/grid/mobile.html rename to src/main/resources/templates/admin/manage/views/component/grid/mobile.html diff --git a/src/main/resources/webpage/admin/manage/views/component/grid/speed-dial.html b/src/main/resources/templates/admin/manage/views/component/grid/speed-dial.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/grid/speed-dial.html rename to src/main/resources/templates/admin/manage/views/component/grid/speed-dial.html diff --git a/src/main/resources/webpage/admin/manage/views/component/grid/stack.html b/src/main/resources/templates/admin/manage/views/component/grid/stack.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/grid/stack.html rename to src/main/resources/templates/admin/manage/views/component/grid/stack.html diff --git a/src/main/resources/webpage/admin/manage/views/component/laydate/demo1.html b/src/main/resources/templates/admin/manage/views/component/laydate/demo1.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/laydate/demo1.html rename to src/main/resources/templates/admin/manage/views/component/laydate/demo1.html diff --git a/src/main/resources/webpage/admin/manage/views/component/laydate/demo2.html b/src/main/resources/templates/admin/manage/views/component/laydate/demo2.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/laydate/demo2.html rename to src/main/resources/templates/admin/manage/views/component/laydate/demo2.html diff --git a/src/main/resources/webpage/admin/manage/views/component/laydate/special-demo.html b/src/main/resources/templates/admin/manage/views/component/laydate/special-demo.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/laydate/special-demo.html rename to src/main/resources/templates/admin/manage/views/component/laydate/special-demo.html diff --git a/src/main/resources/webpage/admin/manage/views/component/laydate/theme.html b/src/main/resources/templates/admin/manage/views/component/laydate/theme.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/laydate/theme.html rename to src/main/resources/templates/admin/manage/views/component/laydate/theme.html diff --git a/src/main/resources/webpage/admin/manage/views/component/layer/list.html b/src/main/resources/templates/admin/manage/views/component/layer/list.html similarity index 97% rename from src/main/resources/webpage/admin/manage/views/component/layer/list.html rename to src/main/resources/templates/admin/manage/views/component/layer/list.html index 5a2932b..1c19321 100644 --- a/src/main/resources/webpage/admin/manage/views/component/layer/list.html +++ b/src/main/resources/templates/admin/manage/views/component/layer/list.html @@ -218,7 +218,7 @@ ,test8: function(){ layer.tab({ area: admin.screen() < 2 ? ['100%', '80%'] : ['600px', '300px'], - tab: [{ + tab: [ { title: 'TAB1', content: '
        内容1
        ' }, { diff --git a/src/main/resources/webpage/admin/manage/views/component/layer/special-demo.html b/src/main/resources/templates/admin/manage/views/component/layer/special-demo.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/layer/special-demo.html rename to src/main/resources/templates/admin/manage/views/component/layer/special-demo.html diff --git a/src/main/resources/webpage/admin/manage/views/component/layer/theme.html b/src/main/resources/templates/admin/manage/views/component/layer/theme.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/layer/theme.html rename to src/main/resources/templates/admin/manage/views/component/layer/theme.html diff --git a/src/main/resources/webpage/admin/manage/views/component/laypage/demo1.html b/src/main/resources/templates/admin/manage/views/component/laypage/demo1.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/laypage/demo1.html rename to src/main/resources/templates/admin/manage/views/component/laypage/demo1.html diff --git a/src/main/resources/webpage/admin/manage/views/component/laypage/demo2.html b/src/main/resources/templates/admin/manage/views/component/laypage/demo2.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/laypage/demo2.html rename to src/main/resources/templates/admin/manage/views/component/laypage/demo2.html diff --git a/src/main/resources/webpage/admin/manage/views/component/nav/index.html b/src/main/resources/templates/admin/manage/views/component/nav/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/nav/index.html rename to src/main/resources/templates/admin/manage/views/component/nav/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/panel/index.html b/src/main/resources/templates/admin/manage/views/component/panel/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/panel/index.html rename to src/main/resources/templates/admin/manage/views/component/panel/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/progress/index.html b/src/main/resources/templates/admin/manage/views/component/progress/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/progress/index.html rename to src/main/resources/templates/admin/manage/views/component/progress/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/rate/index.html b/src/main/resources/templates/admin/manage/views/component/rate/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/rate/index.html rename to src/main/resources/templates/admin/manage/views/component/rate/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/slider/index.html b/src/main/resources/templates/admin/manage/views/component/slider/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/slider/index.html rename to src/main/resources/templates/admin/manage/views/component/slider/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/table/auto.html b/src/main/resources/templates/admin/manage/views/component/table/auto.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/auto.html rename to src/main/resources/templates/admin/manage/views/component/table/auto.html index c5d4804..b9aad4d 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/auto.html +++ b/src/main/resources/templates/admin/manage/views/component/table/auto.html @@ -44,7 +44,7 @@ elem: '#test-table-autowidth' ,url: layui.setter.paths.base + 'json/table/user.js' ,cellMinWidth: 80 //全局定义常规单元格的最小宽度,layui 2.2.1 新增 - ,cols: [[ + ,cols: [ [ {field:'id', title: 'ID', sort: true} ,{field:'username', title: '用户名'} //width 支持:数字、百分比和不填写。你还可以通过 minWidth 参数局部定义当前单元格的最小宽度,layui 2.2.1 新增 ,{field:'sex', title: '性别', sort: true} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/cellEdit.html b/src/main/resources/templates/admin/manage/views/component/table/cellEdit.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/cellEdit.html rename to src/main/resources/templates/admin/manage/views/component/table/cellEdit.html index c83e910..c931559 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/cellEdit.html +++ b/src/main/resources/templates/admin/manage/views/component/table/cellEdit.html @@ -43,7 +43,7 @@ table.render({ elem: '#test-table-cellEdit' ,url: layui.setter.paths.base + 'json/table/demo.js' - ,cols: [[ + ,cols: [ [ {type:'checkbox'} ,{field:'id', title:'ID', width:80, sort: true} ,{field:'username', title:'用户名', width:120, sort: true, edit: 'text'} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/cellEvent.html b/src/main/resources/templates/admin/manage/views/component/table/cellEvent.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/cellEvent.html rename to src/main/resources/templates/admin/manage/views/component/table/cellEvent.html index 852ae15..a609f57 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/cellEvent.html +++ b/src/main/resources/templates/admin/manage/views/component/table/cellEvent.html @@ -45,7 +45,7 @@ elem: '#test-table-demoEvent' ,height: 313 ,url: layui.setter.paths.base + 'json/table/user.js' - ,cols: [[ + ,cols: [ [ {field:'id', title: 'ID', width:80} ,{field:'username', title: '用户名', width:80} ,{field:'sign', title: '签名', width:'50%', event: 'setSign', style:'cursor: pointer;'} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/data.html b/src/main/resources/templates/admin/manage/views/component/table/data.html similarity index 95% rename from src/main/resources/webpage/admin/manage/views/component/table/data.html rename to src/main/resources/templates/admin/manage/views/component/table/data.html index 0725f64..7d64236 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/data.html +++ b/src/main/resources/templates/admin/manage/views/component/table/data.html @@ -43,7 +43,7 @@ //展示已知数据 table.render({ elem: '#test-table-data' - ,cols: [[ //标题栏 + ,cols: [ [ //标题栏 {field: 'id', title: 'ID', width: 80, sort: true} ,{field: 'username', title: '用户名', width: 120} ,{field: 'email', title: '邮箱', minWidth: 150} @@ -52,7 +52,7 @@ ,{field: 'city', title: '城市', width: 100} ,{field: 'experience', title: '积分', width: 80, sort: true} ]] - ,data: [{ + ,data: [ { "id": "10001" ,"username": "杜甫" ,"email": "xianxin@baidu.com" diff --git a/src/main/resources/webpage/admin/manage/views/component/table/fixed.html b/src/main/resources/templates/admin/manage/views/component/table/fixed.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/fixed.html rename to src/main/resources/templates/admin/manage/views/component/table/fixed.html index 9d0862d..0a802c9 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/fixed.html +++ b/src/main/resources/templates/admin/manage/views/component/table/fixed.html @@ -46,7 +46,7 @@ ,url: layui.setter.paths.base + 'json/table/user.js' ,width: admin.screen() > 1 ? 892 : '' ,height: 332 - ,cols: [[ + ,cols: [ [ {type:'checkbox', fixed: 'left'} ,{field:'id', width:80, title: 'ID', sort: true, fixed: 'left'} ,{field:'username', width:80, title: '用户名'} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/form.html b/src/main/resources/templates/admin/manage/views/component/table/form.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/form.html rename to src/main/resources/templates/admin/manage/views/component/table/form.html index b274fbc..2f69720 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/form.html +++ b/src/main/resources/templates/admin/manage/views/component/table/form.html @@ -59,7 +59,7 @@ elem: '#test-table-form' ,url: layui.setter.paths.base + 'json/table/user.js' ,cellMinWidth: 80 - ,cols: [[ + ,cols: [ [ {type:'numbers'} ,{type: 'checkbox'} ,{field:'id', title:'ID', width:100, unresize: true, sort: true} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/index.html b/src/main/resources/templates/admin/manage/views/component/table/index.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/index.html rename to src/main/resources/templates/admin/manage/views/component/table/index.html index 90bfa0a..288b1f2 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/index.html +++ b/src/main/resources/templates/admin/manage/views/component/table/index.html @@ -90,7 +90,7 @@ ,cellMinWidth: 80 ,totalRow: true // 开启合计行 ,page: true - ,cols: [[ + ,cols: [ [ {type: 'checkbox', fixed: 'left'} ,{field:'id', fixed: 'left', width:80, title: 'ID', sort: true, totalRowText: '合计:'} ,{field:'username', width:80, title: '用户'} @@ -115,7 +115,7 @@ // 重载测试 dropdown.render({ elem: '#reloadTest' //可绑定在任意元素中,此处以上述按钮为例 - ,data: [{ + ,data: [ { id: 'reload', title: '重载' },{ @@ -140,7 +140,7 @@ //,token: '新的 token2' } /* - ,cols: [[ // 重置表头 + ,cols: [ [ // 重置表头 {type: 'checkbox', fixed: 'left'} ,{field:'id', title:'ID', width:80, fixed: 'left', unresize: true, sort: true, totalRowText: '合计:'} ,{field:'sex', title:'性别', width:80, edit: 'text', sort: true} @@ -193,7 +193,7 @@ // 更多测试 dropdown.render({ elem: '#moreTest' //可绑定在任意元素中,此处以上述按钮为例 - ,data: [{ + ,data: [ { id: 'add', title: '添加' },{ diff --git a/src/main/resources/webpage/admin/manage/views/component/table/initSort.html b/src/main/resources/templates/admin/manage/views/component/table/initSort.html similarity index 95% rename from src/main/resources/webpage/admin/manage/views/component/table/initSort.html rename to src/main/resources/templates/admin/manage/views/component/table/initSort.html index 3dbad6f..18ebbe1 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/initSort.html +++ b/src/main/resources/templates/admin/manage/views/component/table/initSort.html @@ -48,7 +48,7 @@ field: 'wealth' ,type: 'desc' } - ,cols: [[ + ,cols: [ [ {field:'id', title: 'ID', width:80} ,{field:'username', title: '用户名', width:80} ,{field:'score', title: '评分', width:80, sort: true} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/onrow.html b/src/main/resources/templates/admin/manage/views/component/table/onrow.html similarity index 94% rename from src/main/resources/webpage/admin/manage/views/component/table/onrow.html rename to src/main/resources/templates/admin/manage/views/component/table/onrow.html index 7a97c89..1a811a8 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/onrow.html +++ b/src/main/resources/templates/admin/manage/views/component/table/onrow.html @@ -45,8 +45,8 @@ table.render({ elem: '#test-table-onrow' - ,url: layui.setter.paths.base + '/json/table/demo.js' - ,cols: [[ + ,url: layui.setter.paths.base + 'json/table/demo.js' + ,cols: [ [ {field:'id', title:'ID', width:80, fixed: 'left', unresize: true, sort: true} ,{field:'username', title:'用户名', width:120} ,{field:'email', title:'邮箱', width:150, templet: function(res){ diff --git a/src/main/resources/webpage/admin/manage/views/component/table/parseData.html b/src/main/resources/templates/admin/manage/views/component/table/parseData.html similarity index 94% rename from src/main/resources/webpage/admin/manage/views/component/table/parseData.html rename to src/main/resources/templates/admin/manage/views/component/table/parseData.html index d5f932d..9497bd2 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/parseData.html +++ b/src/main/resources/templates/admin/manage/views/component/table/parseData.html @@ -48,12 +48,12 @@ table.render({ elem: '#test-table-parseData' - ,url: layui.setter.paths.base + '/json/table/demo3.js' + ,url: layui.setter.paths.base + 'json/table/demo3.js' ,toolbar: true ,title: '用户数据表' ,totalRow: true ,cellMinWidth: 120 - ,cols: [[ + ,cols: [ [ {field:'id', title:'ID', width:80, sort: true, totalRowText: '合计行'} ,{field:'username', title:'用户名', edit: 'text'} ,{field:'experience', title:'积分', sort: true, totalRow: true} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/radio.html b/src/main/resources/templates/admin/manage/views/component/table/radio.html similarity index 94% rename from src/main/resources/webpage/admin/manage/views/component/table/radio.html rename to src/main/resources/templates/admin/manage/views/component/table/radio.html index ecea7a2..a65ae38 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/radio.html +++ b/src/main/resources/templates/admin/manage/views/component/table/radio.html @@ -50,9 +50,9 @@ table.render({ elem: '#test-table-radio' - ,url: layui.setter.paths.base + '/json/table/user.js' + ,url: layui.setter.paths.base + 'json/table/user.js' ,toolbar: '#test-table-radio-toolbarDemo' - ,cols: [[ + ,cols: [ [ {type:'radio'} ,{field:'id', width:80, title: 'ID', sort: true} ,{field:'username', width:80, title: '用户名'} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/resetPage.html b/src/main/resources/templates/admin/manage/views/component/table/resetPage.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/resetPage.html rename to src/main/resources/templates/admin/manage/views/component/table/resetPage.html index 196d9f2..f3b8633 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/resetPage.html +++ b/src/main/resources/templates/admin/manage/views/component/table/resetPage.html @@ -52,7 +52,7 @@ ,last: false //不显示尾页 } - ,cols: [[ + ,cols: [ [ {field:'id', width:80, title: 'ID', sort: true} ,{field:'username', width:100, title: '用户名'} ,{field:'sex', width:80, title: '性别', sort: true} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/search.html b/src/main/resources/templates/admin/manage/views/component/table/search.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/search.html rename to src/main/resources/templates/admin/manage/views/component/table/search.html index 317d104..f393baf 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/search.html +++ b/src/main/resources/templates/admin/manage/views/component/table/search.html @@ -56,7 +56,7 @@ table.render({ elem: '#test-table-reload' ,url: layui.setter.paths.base + 'json/table/user.js' - ,cols: [[ + ,cols: [ [ {checkbox: true, fixed: true} ,{field:'id', title: 'ID', width:80, sort: true, fixed: true} ,{field:'username', title: '用户名', width:80} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/static.html b/src/main/resources/templates/admin/manage/views/component/table/static.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/table/static.html rename to src/main/resources/templates/admin/manage/views/component/table/static.html diff --git a/src/main/resources/webpage/admin/manage/views/component/table/style.html b/src/main/resources/templates/admin/manage/views/component/table/style.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/component/table/style.html rename to src/main/resources/templates/admin/manage/views/component/table/style.html index e3453b4..b507514 100644 --- a/src/main/resources/webpage/admin/manage/views/component/table/style.html +++ b/src/main/resources/templates/admin/manage/views/component/table/style.html @@ -55,7 +55,7 @@ elem: '#test-table-style' ,url: layui.setter.paths.base + 'json/table/user.js' ,height: 310 - ,cols: [[ + ,cols: [ [ {field:'id', title:'ID', width:80, sort: true} ,{field:'username', title:'用户名', width:80, templet: '#usernameTpl'} ,{field:'sex', title:'性别', width:80, sort: true, templet: '#sexTpl'} diff --git a/src/main/resources/webpage/admin/manage/views/component/table/thead.html b/src/main/resources/templates/admin/manage/views/component/table/thead.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/table/thead.html rename to src/main/resources/templates/admin/manage/views/component/table/thead.html diff --git a/src/main/resources/webpage/admin/manage/views/component/table/tostatic.html b/src/main/resources/templates/admin/manage/views/component/table/tostatic.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/table/tostatic.html rename to src/main/resources/templates/admin/manage/views/component/table/tostatic.html diff --git a/src/main/resources/webpage/admin/manage/views/component/tabs/index.html b/src/main/resources/templates/admin/manage/views/component/tabs/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/tabs/index.html rename to src/main/resources/templates/admin/manage/views/component/tabs/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/timeline/index.html b/src/main/resources/templates/admin/manage/views/component/timeline/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/timeline/index.html rename to src/main/resources/templates/admin/manage/views/component/timeline/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/transfer/index.html b/src/main/resources/templates/admin/manage/views/component/transfer/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/transfer/index.html rename to src/main/resources/templates/admin/manage/views/component/transfer/index.html diff --git a/src/main/resources/webpage/admin/manage/views/component/tree/index.html b/src/main/resources/templates/admin/manage/views/component/tree/index.html similarity index 90% rename from src/main/resources/webpage/admin/manage/views/component/tree/index.html rename to src/main/resources/templates/admin/manage/views/component/tree/index.html index bf54099..5799ffc 100644 --- a/src/main/resources/webpage/admin/manage/views/component/tree/index.html +++ b/src/main/resources/templates/admin/manage/views/component/tree/index.html @@ -89,26 +89,26 @@ ,util = layui.util //模拟数据 - ,data = [{ + ,data = [ { title: '一级1' ,id: 1 ,field: 'name1' ,checked: true ,spread: true - ,children: [{ + ,children: [ { title: '二级1-1 可允许跳转' ,id: 3 ,field: 'name11' ,href: 'https://www.baidu.com/' - ,children: [{ + ,children: [ { title: '三级1-1-3' ,id: 23 ,field: '' - ,children: [{ + ,children: [ { title: '四级1-1-3-1' ,id: 24 ,field: '' - ,children: [{ + ,children: [ { title: '五级1-1-3-1-1' ,id: 30 ,field: '' @@ -122,7 +122,7 @@ title: '三级1-1-1' ,id: 7 ,field: '' - ,children: [{ + ,children: [ { title: '四级1-1-1-1 可允许跳转' ,id: 15 ,field: '' @@ -132,7 +132,7 @@ title: '三级1-1-2' ,id: 8 ,field: '' - ,children: [{ + ,children: [ { title: '四级1-1-2-1' ,id: 32 ,field: '' @@ -142,7 +142,7 @@ title: '二级1-2' ,id: 4 ,spread: true - ,children: [{ + ,children: [ { title: '三级1-2-1' ,id: 9 ,field: '' @@ -156,7 +156,7 @@ title: '二级1-3' ,id: 20 ,field: '' - ,children: [{ + ,children: [ { title: '三级1-3-1' ,id: 21 ,field: '' @@ -171,12 +171,12 @@ ,id: 2 ,field: '' ,spread: true - ,children: [{ + ,children: [ { title: '二级2-1' ,id: 5 ,field: '' ,spread: true - ,children: [{ + ,children: [ { title: '三级2-1-1' ,id: 11 ,field: '' @@ -189,7 +189,7 @@ title: '二级2-2' ,id: 6 ,field: '' - ,children: [{ + ,children: [ { title: '三级2-2-1' ,id: 13 ,field: '' @@ -204,12 +204,12 @@ title: '一级3' ,id: 16 ,field: '' - ,children: [{ + ,children: [ { title: '二级3-1' ,id: 17 ,field: '' ,fixed: true - ,children: [{ + ,children: [ { title: '三级3-1-1' ,id: 18 ,field: '' @@ -222,7 +222,7 @@ title: '二级3-2' ,id: 27 ,field: '' - ,children: [{ + ,children: [ { title: '三级3-2-1' ,id: 28 ,field: '' @@ -235,13 +235,13 @@ }] //模拟数据1 - ,data1 = [{ + ,data1 = [ { title: '江西' ,id: 1 - ,children: [{ + ,children: [ { title: '南昌' ,id: 1000 - ,children: [{ + ,children: [ { title: '青山湖区' ,id: 10001 },{ @@ -258,7 +258,7 @@ },{ title: '广西' ,id: 2 - ,children: [{ + ,children: [ { title: '南宁' ,id: 2000 },{ @@ -268,7 +268,7 @@ },{ title: '陕西' ,id: 3 - ,children: [{ + ,children: [ { title: '西安' ,id: 3000 },{ @@ -278,10 +278,10 @@ }] //模拟数据2 - ,data2 = [{ + ,data2 = [ { title: '早餐' ,id: 1 - ,children: [{ + ,children: [ { title: '油条' ,id: 5 },{ @@ -295,7 +295,7 @@ title: '午餐' ,id: 2 ,checked: true - ,children: [{ + ,children: [ { title: '藜蒿炒腊肉' ,id: 8 },{ @@ -311,7 +311,7 @@ },{ title: '晚餐' ,id: 3 - ,children: [{ + ,children: [ { title: '红烧肉' ,id: 12 ,fixed: true @@ -322,7 +322,7 @@ },{ title: '夜宵' ,id: 4 - ,children: [{ + ,children: [ { title: '小龙虾' ,id: 14 ,checked: true @@ -394,23 +394,23 @@ //手风琴模式 tree.render({ elem: '#test-tree-demo5' - ,data: [{ + ,data: [ { title: '优秀' - ,children: [{ + ,children: [ { title: '80 ~ 90' },{ title: '90 ~ 100' }] },{ title: '良好' - ,children: [{ + ,children: [ { title: '70 ~ 80' },{ title: '60 ~ 70' }] },{ title: '要努力奥' - ,children: [{ + ,children: [ { title: '0 ~ 60' }] }] diff --git a/src/main/resources/webpage/admin/manage/views/component/treeTable/index.html b/src/main/resources/templates/admin/manage/views/component/treeTable/index.html similarity index 97% rename from src/main/resources/webpage/admin/manage/views/component/treeTable/index.html rename to src/main/resources/templates/admin/manage/views/component/treeTable/index.html index 6a32563..d06025e 100644 --- a/src/main/resources/webpage/admin/manage/views/component/treeTable/index.html +++ b/src/main/resources/templates/admin/manage/views/component/treeTable/index.html @@ -69,7 +69,7 @@ }, height: 'full-100', toolbar: '#TPL-treeTable-demo', - cols: [[ + cols: [ [ {type: 'checkbox', fixed: 'left'}, {field: 'id', title: 'ID', width: 80, sort: true, fixed: 'left'}, {field: 'name', title: '用户名', width: 180, fixed: 'left'}, diff --git a/src/main/resources/webpage/admin/manage/views/component/upload/demo1.html b/src/main/resources/templates/admin/manage/views/component/upload/demo1.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/upload/demo1.html rename to src/main/resources/templates/admin/manage/views/component/upload/demo1.html diff --git a/src/main/resources/webpage/admin/manage/views/component/upload/demo2.html b/src/main/resources/templates/admin/manage/views/component/upload/demo2.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/upload/demo2.html rename to src/main/resources/templates/admin/manage/views/component/upload/demo2.html diff --git a/src/main/resources/webpage/admin/manage/views/component/util/index.html b/src/main/resources/templates/admin/manage/views/component/util/index.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/component/util/index.html rename to src/main/resources/templates/admin/manage/views/component/util/index.html diff --git a/src/main/resources/webpage/admin/manage/views/index.html b/src/main/resources/templates/admin/manage/views/demo-index.html similarity index 97% rename from src/main/resources/webpage/admin/manage/views/index.html rename to src/main/resources/templates/admin/manage/views/demo-index.html index ea69274..8f43975 100644 --- a/src/main/resources/webpage/admin/manage/views/index.html +++ b/src/main/resources/templates/admin/manage/views/demo-index.html @@ -2,6 +2,7 @@ + Tushare 数据服务管理后台 diff --git a/src/main/resources/webpage/admin/manage/views/home/console.html b/src/main/resources/templates/admin/manage/views/home/console.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/home/console.html rename to src/main/resources/templates/admin/manage/views/home/console.html diff --git a/src/main/resources/webpage/admin/manage/views/home/homepage1.html b/src/main/resources/templates/admin/manage/views/home/homepage1.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/home/homepage1.html rename to src/main/resources/templates/admin/manage/views/home/homepage1.html diff --git a/src/main/resources/webpage/admin/manage/views/home/homepage2.html b/src/main/resources/templates/admin/manage/views/home/homepage2.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/home/homepage2.html rename to src/main/resources/templates/admin/manage/views/home/homepage2.html diff --git a/src/main/resources/templates/admin/manage/views/index.html b/src/main/resources/templates/admin/manage/views/index.html new file mode 100644 index 0000000..8f1ecfe --- /dev/null +++ b/src/main/resources/templates/admin/manage/views/index.html @@ -0,0 +1,226 @@ + + + + + + Tushare 数据服务管理后台 + + + + + + + + + + + + + + diff --git a/src/main/resources/webpage/admin/manage/views/senior/echarts/bar.html b/src/main/resources/templates/admin/manage/views/senior/echarts/bar.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/senior/echarts/bar.html rename to src/main/resources/templates/admin/manage/views/senior/echarts/bar.html diff --git a/src/main/resources/webpage/admin/manage/views/senior/echarts/line.html b/src/main/resources/templates/admin/manage/views/senior/echarts/line.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/senior/echarts/line.html rename to src/main/resources/templates/admin/manage/views/senior/echarts/line.html diff --git a/src/main/resources/webpage/admin/manage/views/senior/echarts/map.html b/src/main/resources/templates/admin/manage/views/senior/echarts/map.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/senior/echarts/map.html rename to src/main/resources/templates/admin/manage/views/senior/echarts/map.html diff --git a/src/main/resources/webpage/admin/manage/views/senior/im/index.html b/src/main/resources/templates/admin/manage/views/senior/im/index.html similarity index 96% rename from src/main/resources/webpage/admin/manage/views/senior/im/index.html rename to src/main/resources/templates/admin/manage/views/senior/im/index.html index af664a9..94d93e2 100644 --- a/src/main/resources/webpage/admin/manage/views/senior/im/index.html +++ b/src/main/resources/templates/admin/manage/views/senior/im/index.html @@ -99,7 +99,7 @@ ,isVideo: true //扩展工具栏 - ,tool: [{ + ,tool: [ { alias: 'code' ,title: '代码' ,icon: '' diff --git a/src/main/resources/webpage/admin/manage/views/set/system/email.html b/src/main/resources/templates/admin/manage/views/set/system/email.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/set/system/email.html rename to src/main/resources/templates/admin/manage/views/set/system/email.html diff --git a/src/main/resources/webpage/admin/manage/views/set/system/website.html b/src/main/resources/templates/admin/manage/views/set/system/website.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/set/system/website.html rename to src/main/resources/templates/admin/manage/views/set/system/website.html diff --git a/src/main/resources/webpage/admin/manage/views/set/user/info.html b/src/main/resources/templates/admin/manage/views/set/user/info.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/set/user/info.html rename to src/main/resources/templates/admin/manage/views/set/user/info.html diff --git a/src/main/resources/webpage/admin/manage/views/set/user/password.html b/src/main/resources/templates/admin/manage/views/set/user/password.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/set/user/password.html rename to src/main/resources/templates/admin/manage/views/set/user/password.html diff --git a/src/main/resources/webpage/admin/manage/views/system/about.html b/src/main/resources/templates/admin/manage/views/system/about.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/system/about.html rename to src/main/resources/templates/admin/manage/views/system/about.html diff --git a/src/main/resources/webpage/admin/manage/views/system/theme.html b/src/main/resources/templates/admin/manage/views/system/theme.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/system/theme.html rename to src/main/resources/templates/admin/manage/views/system/theme.html diff --git a/src/main/resources/webpage/admin/manage/views/template/addresslist.html b/src/main/resources/templates/admin/manage/views/template/addresslist.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/addresslist.html rename to src/main/resources/templates/admin/manage/views/template/addresslist.html diff --git a/src/main/resources/webpage/admin/manage/views/template/caller.html b/src/main/resources/templates/admin/manage/views/template/caller.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/caller.html rename to src/main/resources/templates/admin/manage/views/template/caller.html diff --git a/src/main/resources/webpage/admin/manage/views/template/goodslist.html b/src/main/resources/templates/admin/manage/views/template/goodslist.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/goodslist.html rename to src/main/resources/templates/admin/manage/views/template/goodslist.html diff --git a/src/main/resources/webpage/admin/manage/views/template/msgboard.html b/src/main/resources/templates/admin/manage/views/template/msgboard.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/msgboard.html rename to src/main/resources/templates/admin/manage/views/template/msgboard.html diff --git a/src/main/resources/webpage/admin/manage/views/template/personalpage.html b/src/main/resources/templates/admin/manage/views/template/personalpage.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/personalpage.html rename to src/main/resources/templates/admin/manage/views/template/personalpage.html diff --git a/src/main/resources/webpage/admin/manage/views/template/search.html b/src/main/resources/templates/admin/manage/views/template/search.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/search.html rename to src/main/resources/templates/admin/manage/views/template/search.html diff --git a/src/main/resources/webpage/admin/manage/views/template/tips/404.html b/src/main/resources/templates/admin/manage/views/template/tips/404.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/tips/404.html rename to src/main/resources/templates/admin/manage/views/template/tips/404.html diff --git a/src/main/resources/webpage/admin/manage/views/template/tips/error.html b/src/main/resources/templates/admin/manage/views/template/tips/error.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/template/tips/error.html rename to src/main/resources/templates/admin/manage/views/template/tips/error.html diff --git a/src/main/resources/webpage/admin/manage/views/user/administrators/list.html b/src/main/resources/templates/admin/manage/views/user/administrators/list.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/user/administrators/list.html rename to src/main/resources/templates/admin/manage/views/user/administrators/list.html diff --git a/src/main/resources/webpage/admin/manage/views/user/administrators/role.html b/src/main/resources/templates/admin/manage/views/user/administrators/role.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/user/administrators/role.html rename to src/main/resources/templates/admin/manage/views/user/administrators/role.html diff --git a/src/main/resources/webpage/admin/manage/views/user/forget.html b/src/main/resources/templates/admin/manage/views/user/forget.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/user/forget.html rename to src/main/resources/templates/admin/manage/views/user/forget.html diff --git a/src/main/resources/webpage/admin/manage/views/user/login.html b/src/main/resources/templates/admin/manage/views/user/login.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/user/login.html rename to src/main/resources/templates/admin/manage/views/user/login.html diff --git a/src/main/resources/webpage/admin/manage/views/user/reg.html b/src/main/resources/templates/admin/manage/views/user/reg.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/user/reg.html rename to src/main/resources/templates/admin/manage/views/user/reg.html diff --git a/src/main/resources/webpage/admin/manage/views/user/user/list.html b/src/main/resources/templates/admin/manage/views/user/user/list.html similarity index 100% rename from src/main/resources/webpage/admin/manage/views/user/user/list.html rename to src/main/resources/templates/admin/manage/views/user/user/list.html diff --git a/src/main/resources/webpage/docs/docs.html b/src/main/resources/templates/docs/docs.html similarity index 100% rename from src/main/resources/webpage/docs/docs.html rename to src/main/resources/templates/docs/docs.html diff --git a/src/test/java/link/at17/mid/tushare/FireDragonOutSeaTest.java b/src/test/java/link/at17/mid/tushare/FireDragonOutSeaTest.java deleted file mode 100644 index 3a23de2..0000000 --- a/src/test/java/link/at17/mid/tushare/FireDragonOutSeaTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package link.at17.mid.tushare; - -import java.text.ParseException; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import link.at17.mid.tushare.dao.StockCalendarDao; -import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; -import link.at17.mid.tushare.data.models.IndexValue; -import link.at17.mid.tushare.data.models.StockInfo; -import link.at17.mid.tushare.data.models.StockValue; -import link.at17.mid.tushare.data.util.Indicators; -import link.at17.mid.tushare.data.util.PointsUtil; -import link.at17.mid.tushare.data.util.PriceUtil; -import link.at17.mid.tushare.enums.CrossType; -import link.at17.mid.tushare.enums.ListStatus; -import lombok.extern.slf4j.Slf4j; - -/** - * 狼峰火龙出海战法( - * @author Barry - * - */ -@SpringBootTest -@Slf4j -class FireDragonOutSeaTest { - - @Autowired - TushareCrawler tushareCrawler; - - @Autowired - StockCalendarDao stockCalendarDao; - - @Autowired - StockDailyDao stockDailyDao; - - @Autowired - StockInfoDao stockInfoDao; - - @Test - void test() throws ParseException { - List stockInfos = stockInfoDao.getStockListByListStatus(ListStatus.LIST); - //stockInfos = new ArrayList<>(); - //stockInfos.add(stockInfoDao.selectOne(new QueryWrapper().eq("ts_code", "601330.SH"))); - logInfoWithTabs("代码", "名称", "火龙日期"); - stockInfos.parallelStream().forEach(stockInfo -> { - if (stockInfo.getName().contains("ST") || stockInfo.getName().contains("退")) { - return; - } - List dailyK; - try { - dailyK = stockDailyDao.getQfqDailyBeforeTushare(stockInfo.getTsCode(), null, 209L); - } catch (Exception e1) { - // TODO Auto-generated catch block - return; - } - if (dailyK.size() == 0) { - return; - } - List ma20 = Indicators.getMA(dailyK, 20); - List ma60 = Indicators.getMA(dailyK, 60); - - List> ma2060Crosses; - try { - ma2060Crosses = PointsUtil.getCross(ma20, ma60); - } - catch (Exception e) { - return; - } - - - - if (ma2060Crosses.size() > 1) { - IndexValue prevCross = ma2060Crosses.get(ma2060Crosses.size() - 2); - IndexValue currCross = ma2060Crosses.get(ma2060Crosses.size() - 1); - //IndexValue trixCross = trixCrosses.get(trixCrosses.size() - 1); - if (currCross.getValue() == CrossType.Bullish && currCross.getIndex() - prevCross.getIndex() > 40) { - // 首先出现叉的 Index - Integer firstAppearIndex = currCross.getIndex(); - List upLimitIndexes = PriceUtil.getLimitIndexes(dailyK, PriceUtil.getLimitByStockInfo(stockInfo)); - boolean upLimitNearly = false; - int nearlyRange = 4; - for (int i = Math.max(0, firstAppearIndex - nearlyRange); - i < Math.min(dailyK.size(), firstAppearIndex + nearlyRange + 1); i++) { - if (upLimitIndexes.contains(i)) { - upLimitNearly = true; - break; - } - } - if (upLimitNearly) { - logInfoWithTabs( - stockInfo.getTsCode(), - stockInfo.getName(), - dailyK.get(firstAppearIndex).getDateStr("yyyy-MM-dd") - ); - } - } - } - - - }); - } - - - - public static void logInfoWithTabs(Object ...o) { - String format = ""; - for (int i = 0; i < o.length; i++) { - format += "{}\t"; - } - format = format.trim(); - log.info(format, o); - } -} diff --git a/src/test/java/link/at17/mid/tushare/LonBullishCrossTest.java b/src/test/java/link/at17/mid/tushare/LonBullishCrossTest.java deleted file mode 100644 index 141396a..0000000 --- a/src/test/java/link/at17/mid/tushare/LonBullishCrossTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package link.at17.mid.tushare; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; - -import link.at17.mid.tushare.dao.StockCalendarDao; -import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; -import link.at17.mid.tushare.data.models.IndexValue; -import link.at17.mid.tushare.data.models.StockInfo; -import link.at17.mid.tushare.data.models.StockValue; -import link.at17.mid.tushare.data.util.Indicators; -import link.at17.mid.tushare.data.util.PointsUtil; -import link.at17.mid.tushare.enums.CrossType; -import link.at17.mid.tushare.enums.ListStatus; -import link.at17.mid.tushare.system.util.LocalDateTimeUtils; -import lombok.extern.slf4j.Slf4j; - -/** - * LON 指标金叉买卖测试 - * @author Barry - * - */ -@SpringBootTest -@Slf4j -class LonBullishCrossTest { - - @Autowired - TushareCrawler tushareCrawler; - - @Autowired - StockCalendarDao stockCalendarDao; - - @Autowired - StockDailyDao stockDailyDao; - - @Autowired - StockInfoDao stockInfoDao; - - @Test - void test() throws ParseException { - List stockInfos = stockInfoDao.getStockListByListStatus(ListStatus.LIST); - stockInfos = new ArrayList<>(); - stockInfos.add(stockInfoDao.selectOne(new QueryWrapper().eq("ts_code", "601330.SH"))); - - - List profits = new ArrayList<>(); - stockInfos.parallelStream().forEach(stockInfo -> { - if (stockInfo.getName().contains("ST")) { - return; - } - List dailyK = stockDailyDao.getQfqDailyBeforeTushare(stockInfo.getTsCode(), null, 209L); - if (dailyK.size() == 0) { - return; - } - - List lon = Indicators.getLON(dailyK, 10, 20); - List lonma10 = Indicators.getMA(lon, v->v, 10); - - List> lonCrosses; - try { - lonCrosses = PointsUtil.getCross(lon, lonma10); - } - catch (Exception e) { - return; - } - - - List ma6 = Indicators.getMA(dailyK, 6); - List ma12 = Indicators.getMA(dailyK, 12); - List ma20 = Indicators.getMA(dailyK, 20); - List ma60 = Indicators.getMA(dailyK, 60); - - - if (lonCrosses.size() > 1) { - IndexValue lonCrossPrev = lonCrosses.get(lonCrosses.size() - 2); - IndexValue lonCross = lonCrosses.get(lonCrosses.size() - 1); - //IndexValue trixCross = trixCrosses.get(trixCrosses.size() - 1); - if (lonCross.getValue() == CrossType.Bullish && lonCross.getIndex() - lonCrossPrev.getIndex() >= 60) { - // 首先出现叉的 Index - Integer firstAppearIndex = lonCross.getIndex(); - if (ma20 != null && - ma20.get(firstAppearIndex - 1) != null && - ma20.get(firstAppearIndex) != null && - ma20.get(firstAppearIndex - 1) > ma20.get(firstAppearIndex)) { - // 前面是下跌趋势,直接pass - return; - } - - StockValue buyValue = dailyK.get(lonCross.getIndex()); - StockValue sellValue = dailyK.get(dailyK.size() - 1); - Double profit = (sellValue.getClose() - buyValue.getClose()) / buyValue.getClose() * 100; - log.info("{} {} - {} 买入 {}, {} 现价 {}, 总涨幅 {}%", - stockInfo.getTsCode(), - stockInfo.getName(), - LocalDateTimeUtils.format(buyValue.getDate(), "yyyy-MM-dd"), buyValue.getClose(), - LocalDateTimeUtils.format(sellValue.getDate(), "yyyy-MM-dd"), sellValue.getClose(), - profit); - profits.add(profit); - } - } - - - }); - System.out.println("总收益率 " + profits.stream().reduce(Double::sum).get() / profits.size()); - } - -} diff --git a/src/test/java/link/at17/mid/tushare/MaBullishArrangeTest.java b/src/test/java/link/at17/mid/tushare/MaBullishArrangeTest.java deleted file mode 100644 index 74377fd..0000000 --- a/src/test/java/link/at17/mid/tushare/MaBullishArrangeTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package link.at17.mid.tushare; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import link.at17.mid.tushare.dao.StockCalendarDao; -import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; -import link.at17.mid.tushare.data.models.IndexValue; -import link.at17.mid.tushare.data.models.StockInfo; -import link.at17.mid.tushare.data.models.StockValue; -import link.at17.mid.tushare.data.util.Indicators; -import link.at17.mid.tushare.data.util.PointsUtil; -import link.at17.mid.tushare.enums.CrossType; -import link.at17.mid.tushare.enums.ListStatus; -import link.at17.mid.tushare.system.util.LocalDateTimeUtils; -import lombok.extern.slf4j.Slf4j; - -/** - * 均线多头排列测试 - * @author Barry - * - */ -@SpringBootTest -@Slf4j -class MaBullishArrangeTest { - - @Autowired - TushareCrawler tushareCrawler; - - @Autowired - StockCalendarDao stockCalendarDao; - - @Autowired - StockDailyDao stockDailyDao; - - @Autowired - StockInfoDao stockInfoDao; - - @Test - void test() throws ParseException { - List stockInfos = stockInfoDao.getStockListByListStatus(ListStatus.LIST); - //stockInfos = new ArrayList<>(); - //stockInfos.add(stockInfoDao.selectOne(new QueryWrapper().eq("ts_code", "601330.SH"))); - - - List profits = new ArrayList<>(); - stockInfos.parallelStream().forEach(stockInfo -> { - if (stockInfo.getName().contains("ST")) { - return; - } - List dailyK; - try { - dailyK = stockDailyDao.getQfqDailyBeforeTushare(stockInfo.getTsCode(), null, 205L); - } catch (Exception e1) { - // TODO Auto-generated catch block - return; - } - if (dailyK.size() == 0) { - return; - } - - List lon = Indicators.getLON(dailyK, 10, 20); - List lonma10 = Indicators.getMA(lon, v->v, 10); - - List> lonCrosses; - try { - lonCrosses = PointsUtil.getCross(lon, lonma10); - } - catch (Exception e) { - return; - } - - List volMA5 = Indicators.getMA(dailyK, v -> v.getVol(), 5); - List volMA10 = Indicators.getMA(dailyK, v -> v.getVol(), 10); - - List> volCrosses; - try { - volCrosses = PointsUtil.getCross(volMA5, volMA10); - } - catch (Exception e) { - return; - } - - - List ma6 = Indicators.getMA(dailyK, 6); - List ma12 = Indicators.getMA(dailyK, 12); - List ma20 = Indicators.getMA(dailyK, 20); - List ma60 = Indicators.getMA(dailyK, 60); - - List bullishArrange; - try { - bullishArrange = PointsUtil.getBullishArrange(2, 4, ma6, ma12, ma20, ma60); - } - catch (Exception e) { - return; - } - - // 测试均线多头策略的收益 - - for (int i = 1; i < dailyK.size(); i++) { - if (bullishArrange.contains(i) && !bullishArrange.contains(i - 1)) { - StockValue current = dailyK.get(i); - StockValue previous = dailyK.get(i - 1); - String date = LocalDateTimeUtils.format(dailyK.get(i).getDate(), "yyyy-MM-dd"); - List future = dailyK.subList(i, Math.min(i + 10, dailyK.size())); - StockValue max = future.stream().max((x, y) -> x.getClose().compareTo(y.getClose())).orElse(current); - StockValue min = future.stream().max((x, y) -> -x.getClose().compareTo(y.getClose())).orElse(current); - //if (date.equals("2021-10-08")) - // 代码 名称 日期 当日涨跌幅 窗口期最大涨幅 最大涨幅时间 窗口期最大回撤 最大回撤时间 - log.info("{}\t{}\t{}\t{}%\t{}%\t{}\t{}%\t{}", - stockInfo.getTsCode(), - stockInfo.getName(), - date, - (current.getClose() - previous.getClose()) / previous.getClose() * 100, - (max.getClose() - current.getClose()) / current.getClose() * 100, - LocalDateTimeUtils.format(max.getDate(), "yyyy-MM-dd"), - (min.getClose() - current.getClose()) / current.getClose() * 100, - LocalDateTimeUtils.format(min.getDate(), "yyyy-MM-dd") - ); - } - } - - /* - bullishArrange.stream().forEach(index -> { - log.info("{} {} - {} 出现多头排列", - stockInfo.getTsCode(), - stockInfo.getName(), - DateUtils.format(dailyK.get(index).getDate(), "yyyy-MM-dd")); - }); - if (true) return;*/ - }); - System.out.println("总收益率 " + profits.stream().reduce(Double::sum).orElse(0d) / profits.size()); - } - -} diff --git a/src/test/java/link/at17/mid/tushare/MinuteCostDistribution.java b/src/test/java/link/at17/mid/tushare/MinuteCostDistribution.java index ed27c18..16fe483 100644 --- a/src/test/java/link/at17/mid/tushare/MinuteCostDistribution.java +++ b/src/test/java/link/at17/mid/tushare/MinuteCostDistribution.java @@ -7,13 +7,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; import link.at17.mid.tushare.dao.StockMinuteDao; import link.at17.mid.tushare.data.models.CostDistribution; import link.at17.mid.tushare.data.models.StockInfo; import link.at17.mid.tushare.data.models.StockValueEx; +import link.at17.mid.tushare.data.service.StockInfoService; import link.at17.mid.tushare.data.util.Indicators; import link.at17.mid.tushare.enums.ListStatus; import link.at17.mid.tushare.enums.StockSpan; @@ -30,9 +29,6 @@ import lombok.extern.slf4j.Slf4j; @SpringBootTest @Slf4j class MinuteCostDistribution { - - @Autowired - StockCalendarDao stockCalendarDao; @Autowired StockDailyDao stockDailyDao; @@ -41,11 +37,11 @@ class MinuteCostDistribution { StockMinuteDao stockMinuteDao; @Autowired - StockInfoDao stockInfoDao; + StockInfoService stockInfoService; @Test void test() throws ParseException { - List stockInfos = stockInfoDao.getStockListByListStatus(ListStatus.LIST); + List stockInfos = stockInfoService.listByListStatus(ListStatus.LIST); // stockInfos = new ArrayList<>(); // stockInfos.add(stockInfoDao.selectOne(new QueryWrapper().eq("ts_code", "000812.SZ"))); @@ -59,7 +55,7 @@ class MinuteCostDistribution { if (stockName.contains("ST") || stockName.startsWith("N") || stockName.startsWith("C") || stockCode.endsWith("BJ")) { return; } - List minuteK = stockMinuteDao.getExQfqMinuteBeforeTushare(stockInfo.getTsCode(), StockSpan.Minute, null, 100L * 241); + List minuteK = stockMinuteDao.getExQfqMinuteBefore(stockInfo.getTsCode(), StockSpan.Minute, null, 100L * 241); List satisfiedDates = new ArrayList<>(); for (int i = count; i < minuteK.size(); i++) { StockValueEx current = minuteK.get(i); diff --git a/src/test/java/link/at17/mid/tushare/VolIncreaseBk.java b/src/test/java/link/at17/mid/tushare/VolIncreaseBk.java index c61fdd3..3d3b8a2 100644 --- a/src/test/java/link/at17/mid/tushare/VolIncreaseBk.java +++ b/src/test/java/link/at17/mid/tushare/VolIncreaseBk.java @@ -7,11 +7,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.dao.StockThsDailyDao; -import link.at17.mid.tushare.dao.StockThsListDao; +import link.at17.mid.tushare.dao.ThsDailyDao; +import link.at17.mid.tushare.dao.ThsListDao; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; import link.at17.mid.tushare.data.models.StockValue; import link.at17.mid.tushare.data.models.ThsStockInfo; @@ -31,24 +29,18 @@ class VolIncreaseBk { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; @Autowired - StockInfoDao stockInfoDao; + ThsListDao thsListDao; @Autowired - StockThsListDao stockThsListDao; - - @Autowired - StockThsDailyDao stockThsDailyDao; + ThsDailyDao thsDailyDao; @Test void test() throws ParseException { - List stockInfos = stockThsListDao.listByExchange(ThsStockMarket.A); + List stockInfos = thsListDao.listByExchange(ThsStockMarket.A); //stockInfos = new ArrayList<>(); //stockInfos.add(stockInfoDao.selectOne(new QueryWrapper().eq("ts_code", "601330.SH"))); @@ -56,7 +48,7 @@ class VolIncreaseBk { List profits = new ArrayList<>(); int count = 5; stockInfos.parallelStream().forEach(stockInfo -> { - List dailyK = stockThsDailyDao.getDailyBeforeTushare(stockInfo.getTsCode(), null, 200L); + List dailyK = thsDailyDao.getDailyBeforeTushare(stockInfo.getTsCode(), null, 200L); if (dailyK.size() == 0) { return; } diff --git a/src/test/java/link/at17/mid/tushare/data/UpdateData.java b/src/test/java/link/at17/mid/tushare/data/UpdateData.java index fce183c..3057f80 100644 --- a/src/test/java/link/at17/mid/tushare/data/UpdateData.java +++ b/src/test/java/link/at17/mid/tushare/data/UpdateData.java @@ -6,13 +6,17 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; import link.at17.mid.tushare.data.crawler.QueryWay; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; -import link.at17.mid.tushare.data.models.StockCalendar; -import link.at17.mid.tushare.enums.StockHolderType; +import link.at17.mid.tushare.data.service.StockAdjustService; +import link.at17.mid.tushare.data.service.StockCalendarService; +import link.at17.mid.tushare.data.service.StockDailyBasicService; +import link.at17.mid.tushare.data.service.StockDailyService; +import link.at17.mid.tushare.data.service.StockInfoService; +import link.at17.mid.tushare.data.service.StockLimitService; +import link.at17.mid.tushare.data.service.ThsDailyService; +import link.at17.mid.tushare.data.service.ThsMemberService; import link.at17.mid.tushare.enums.StockMarket; import lombok.extern.slf4j.Slf4j; @@ -23,14 +27,32 @@ class UpdateData { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; @Autowired - StockInfoDao stockInfoDao; + StockInfoService stockInfoService; + + @Autowired + StockCalendarService stockCalendarService; + + @Autowired + StockLimitService stockLimitService; + + @Autowired + StockDailyService stockDailyService; + + @Autowired + StockAdjustService stockAdjustService; + + @Autowired + StockDailyBasicService stockDailyBasicService; + + @Autowired + ThsMemberService thsMemberService; + + @Autowired + ThsDailyService thsDailyService; @Test void contextLoads() { @@ -44,9 +66,9 @@ class UpdateData { LocalDateTime startDate2 = LocalDateTime.of(2021, 8, 14, 0, 0);//LocalDateTimeUtils.parseDate("20210814000000", "yyyyMMddHHmmss"); // Date endDate = DateUtils.parseDate("20210820192030", "yyyyMMddHHmmss"); System.out.println(startDate.toLocalDate()); - System.out.println(stockCalendarDao.getOpenDateOffset(StockMarket.SZ, startDate3, 1)); - System.out.println(stockCalendarDao.getOpenDateOffset(StockMarket.SZ, startDate, 1)); - System.out.println(stockCalendarDao.getOpenDateOffset(StockMarket.SZ, startDate2, 1)); + System.out.println(stockCalendarService.getOpenDateOffset(StockMarket.SZ, startDate3, 1)); + System.out.println(stockCalendarService.getOpenDateOffset(StockMarket.SZ, startDate, 1)); + System.out.println(stockCalendarService.getOpenDateOffset(StockMarket.SZ, startDate2, 1)); } @@ -54,11 +76,6 @@ class UpdateData { @Test void testStockListUpdate() { - StockCalendar szLatestCal = stockCalendarDao.getLatest(StockMarket.SZ); - StockCalendar shLatestCal = stockCalendarDao.getLatest(StockMarket.SH); - if (szLatestCal == null || shLatestCal == null) { - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); - } // tushareCrawler.updateStockList(); // tushareCrawler.updateStockDaily(QueryWay.ByDateUpdate); @@ -71,16 +88,16 @@ class UpdateData { // tushareCrawler.updateStockMinValue(QueryWay.ByStock, StockSpan.Minute); // 更新交易日历(if needed) - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); + stockCalendarService.updateData(); - tushareCrawler.updateStockList(); - tushareCrawler.updateStockDaily(QueryWay.ByDateUpdate); - tushareCrawler.updateDailyBasic(QueryWay.ByDateUpdate); - tushareCrawler.updateStockAdjustTushare(QueryWay.ByDateUpdate); - tushareCrawler.updateStockLimit(QueryWay.ByDateUpdate); + stockInfoService.updateData(); + stockDailyService.updateData(QueryWay.ByDateUpdate); + stockDailyBasicService.updateData(QueryWay.ByDateUpdate); + stockAdjustService.updateData(QueryWay.ByDateUpdate); + stockLimitService.updateData(QueryWay.ByDateUpdate); - tushareCrawler.updateThsDaily(QueryWay.ByDateUpdate); - tushareCrawler.updateThsMember(); + thsDailyService.updateData(QueryWay.ByDateUpdate); + thsMemberService.updateData(); // tushareCrawler.updateStockHolder(StockHolderType.TOP10Float); // tushareCrawler.updateStockMinValue(QueryWay.ByStock, StockSpan.SixtyMinute); // tushareCrawler.updateStockMinValue(QueryWay.ByStock, StockSpan.Minute); @@ -93,26 +110,6 @@ class UpdateData { // StockSpan.Daily, stockList, 630L, QueryWay.ByStock); } - //@Test - void testStockListInitialize() { - - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); - - tushareCrawler.updateThsList(); - tushareCrawler.updateThsDaily(QueryWay.ByDateAll); - - tushareCrawler.updateStockList(); - tushareCrawler.updateStockDaily(QueryWay.ByDateAll); - tushareCrawler.updateDailyBasic(QueryWay.ByDateAll); - tushareCrawler.updateStockAdjustTushare(QueryWay.ByDateAll); - - tushareCrawler.updateThsMember(); - tushareCrawler.updateStockLimit(QueryWay.ByDateAll); - - tushareCrawler.updateStockHolder(StockHolderType.TOP10); - tushareCrawler.updateStockHolder(StockHolderType.TOP10Float); - } - @Test void testUpdateKaipanlaInfo() { } diff --git a/src/test/java/link/at17/mid/tushare/data/UpdateDataCrossCheck.java b/src/test/java/link/at17/mid/tushare/data/UpdateDataCrossCheck.java index eef1219..13187ca 100644 --- a/src/test/java/link/at17/mid/tushare/data/UpdateDataCrossCheck.java +++ b/src/test/java/link/at17/mid/tushare/data/UpdateDataCrossCheck.java @@ -4,13 +4,15 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; import link.at17.mid.tushare.data.crawler.QueryWay; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; -import link.at17.mid.tushare.data.models.StockCalendar; -import link.at17.mid.tushare.enums.StockMarket; +import link.at17.mid.tushare.data.service.StockAdjustService; +import link.at17.mid.tushare.data.service.StockCalendarService; +import link.at17.mid.tushare.data.service.StockDailyBasicService; +import link.at17.mid.tushare.data.service.StockDailyService; +import link.at17.mid.tushare.data.service.StockInfoService; +import link.at17.mid.tushare.data.service.ThsDailyService; import lombok.extern.slf4j.Slf4j; @SpringBootTest @@ -20,45 +22,39 @@ class UpdateDataCrossCheck { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; @Autowired - StockInfoDao stockInfoDao; + StockInfoService stockInfoService; + + @Autowired + StockCalendarService stockCalendarService; + + @Autowired + StockDailyService stockDailyService; + + @Autowired + StockAdjustService stockAdjustService; + + @Autowired + StockDailyBasicService stockDailyBasicService; + + @Autowired + ThsDailyService thsDailyService; @Test void testStockListUpdate() { + + stockCalendarService.updateData(); + stockInfoService.updateData(); + stockDailyService.updateData(QueryWay.ByStockCrossCheck); + stockDailyBasicService.updateData(QueryWay.ByStockCrossCheck); + stockAdjustService.updateData(QueryWay.ByStockCrossCheck); - StockCalendar szLatestCal = stockCalendarDao.getLatest(StockMarket.SZ); - StockCalendar shLatestCal = stockCalendarDao.getLatest(StockMarket.SH); - if (szLatestCal == null || shLatestCal == null) { - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); - } - - //tushareCrawler.updateStockList(); - //tushareCrawler.updateStockDaily(QueryWay.ByDateUpdate); - //tushareCrawler.updateDailyBasic(QueryWay.ByDateUpdate); - //tushareCrawler.updateStockAdjustTushare(QueryWay.ByDateUpdate); - - //tushareCrawler.updateThsList(); - //tushareCrawler.updateThsDaily(QueryWay.ByDateUpdate); - //tushareCrawler.updateStockMinValue(QueryWay.ByStock, StockSpan.SixtyMinute); - //tushareCrawler.updateStockMinValue(QueryWay.ByStock, StockSpan.Minute); - - // 更新交易日历(if needed) - tushareCrawler.updateStockCalendar(StockMarket.SH, StockMarket.SZ); - - tushareCrawler.updateStockList(); - tushareCrawler.updateStockDaily(QueryWay.ByStockCrossCheck); - tushareCrawler.updateDailyBasic(QueryWay.ByStockCrossCheck); - tushareCrawler.updateStockAdjustTushare(QueryWay.ByStockCrossCheck); - - tushareCrawler.updateThsDaily(QueryWay.ByStockCrossCheck); + thsDailyService.updateData(QueryWay.ByStockCrossCheck); //tushareCrawler.updateStockMinValue(QueryWay.ByStockCrossCheck, StockSpan.SixtyMinute); //tushareCrawler.updateStockMinValue(QueryWay.ByStockCrossCheck, StockSpan.Minute); //tushareCrawler.updateThsMember(); diff --git a/src/test/java/link/at17/mid/tushare/data/UpdateMinuteTest.java b/src/test/java/link/at17/mid/tushare/data/UpdateMinuteTest.java index b2c2b9d..c642427 100644 --- a/src/test/java/link/at17/mid/tushare/data/UpdateMinuteTest.java +++ b/src/test/java/link/at17/mid/tushare/data/UpdateMinuteTest.java @@ -7,13 +7,12 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.dao.StockMinuteDao; import link.at17.mid.tushare.data.crawler.QueryWay; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; import link.at17.mid.tushare.data.models.StockInfo; +import link.at17.mid.tushare.data.service.StockInfoService; +import link.at17.mid.tushare.data.service.StockMinuteService; import link.at17.mid.tushare.enums.ListStatus; import link.at17.mid.tushare.enums.StockSpan; import lombok.extern.slf4j.Slf4j; @@ -25,27 +24,24 @@ class UpdateMinuteTest { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; @Autowired - StockMinuteDao stockMinuteDao; + StockMinuteService stockMinuteService; @Autowired - StockInfoDao stockInfoDao; + StockInfoService stockInfoService; //@Test void testStockMinuteUpdate() { - tushareCrawler.updateStockMinValue(QueryWay.ByStock, StockSpan.Minute60); + stockMinuteService.updateData(QueryWay.ByStock, StockSpan.Minute60); } //@Test void testGetQfqStockMinute() { try { - stockMinuteDao.getQfqMinuteBeforeTushare("002323.SZ", StockSpan.Minute60, null, 500l).stream().forEach(System.out::println); + stockMinuteService.getQfqMinuteBefore("002323.SZ", StockSpan.Minute60, null, 500l).stream().forEach(System.out::println); } catch (Exception e) { log.error("{}", e); @@ -54,11 +50,11 @@ class UpdateMinuteTest { @Test void testGetAllMissingDates() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - List stockInfos = stockInfoDao.getStockListByListStatus(ListStatus.LIST); + List stockInfos = stockInfoService.listByListStatus(ListStatus.LIST); stockInfos.forEach(stockInfo -> { if (stockInfo.isMainBoard()) { - List allMissingDates = stockMinuteDao.getAllMissingDates(StockSpan.Minute60, stockInfo); + List allMissingDates = stockMinuteService.getAllMissingDates(StockSpan.Minute60, stockInfo); allMissingDates.forEach(date -> { log.info("{} {}\t{}", stockInfo.getTsCode(), stockInfo.getName(), formatter.format(date)); diff --git a/src/test/java/link/at17/mid/tushare/data/UpdateStockAdjustTest.java b/src/test/java/link/at17/mid/tushare/data/UpdateStockAdjustTest.java index f203df7..5d03b6a 100644 --- a/src/test/java/link/at17/mid/tushare/data/UpdateStockAdjustTest.java +++ b/src/test/java/link/at17/mid/tushare/data/UpdateStockAdjustTest.java @@ -8,12 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import link.at17.mid.tushare.dao.StockAdjustDao; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; import link.at17.mid.tushare.dao.StockMinuteDao; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; import link.at17.mid.tushare.data.models.StockInfo; +import link.at17.mid.tushare.data.service.StockInfoService; import link.at17.mid.tushare.enums.ListStatus; import link.at17.mid.tushare.enums.StockSpan; import lombok.extern.slf4j.Slf4j; @@ -25,9 +24,6 @@ class UpdateStockAdjustTest { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; @@ -38,12 +34,12 @@ class UpdateStockAdjustTest { StockAdjustDao stockAdjustDao; @Autowired - StockInfoDao stockInfoDao; + StockInfoService stockInfoService; @Test void testGetAllMissingDates() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - List stockInfos = stockInfoDao.getStockListByListStatus(ListStatus.LIST); + List stockInfos = stockInfoService.listByListStatus(ListStatus.LIST); stockInfos.forEach(stockInfo -> { if (stockInfo.isMainBoard()) { diff --git a/src/test/java/link/at17/mid/tushare/data/UpdateStockLimitTest.java b/src/test/java/link/at17/mid/tushare/data/UpdateStockLimitTest.java index 6e28c90..5301d33 100644 --- a/src/test/java/link/at17/mid/tushare/data/UpdateStockLimitTest.java +++ b/src/test/java/link/at17/mid/tushare/data/UpdateStockLimitTest.java @@ -4,11 +4,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockLimitDao; import link.at17.mid.tushare.data.crawler.QueryWay; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; +import link.at17.mid.tushare.data.service.StockLimitService; import lombok.extern.slf4j.Slf4j; @SpringBootTest @@ -18,19 +17,16 @@ class UpdateStockLimitTest { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; @Autowired - StockLimitDao stockLimitDao; + StockLimitService stockLimitService; @Test void testStockListUpdate() { - tushareCrawler.updateStockLimit(QueryWay.ByDateAll); + stockLimitService.updateData(QueryWay.ByDateAll); } } diff --git a/src/test/java/link/at17/mid/tushare/playground/StockInfoServiceTest.java b/src/test/java/link/at17/mid/tushare/playground/StockInfoServiceTest.java new file mode 100644 index 0000000..3f7a1c8 --- /dev/null +++ b/src/test/java/link/at17/mid/tushare/playground/StockInfoServiceTest.java @@ -0,0 +1,39 @@ +package link.at17.mid.tushare.playground; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.github.yulichang.query.MPJLambdaQueryWrapper; +import link.at17.mid.tushare.data.models.StockInfo; +import link.at17.mid.tushare.data.service.StockInfoService; +import lombok.extern.slf4j.Slf4j; + +@SpringBootTest +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class StockInfoServiceTest { + + + @Autowired + StockInfoService stockInfoService; + + @Test + @Order(5) + void testListWithWrapper() { + + MPJLambdaQueryWrapper ew = new MPJLambdaQueryWrapper<>(); + ew.setAlias("i"); + ew.likeLeft(StockInfo::getTsCode, "SZ"); + List stockInfoList = stockInfoService.list(ew); + for (StockInfo stockInfo : stockInfoList) { + log.info("{}", stockInfo); + } + } + +} diff --git a/src/test/java/link/at17/mid/tushare/test/CostDistributionTest.java b/src/test/java/link/at17/mid/tushare/test/CostDistributionTest.java index 74a43ee..48ac708 100644 --- a/src/test/java/link/at17/mid/tushare/test/CostDistributionTest.java +++ b/src/test/java/link/at17/mid/tushare/test/CostDistributionTest.java @@ -7,7 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; import link.at17.mid.tushare.data.models.CostDistribution; import link.at17.mid.tushare.data.models.StockValueEx; import link.at17.mid.tushare.data.util.Indicators; @@ -20,16 +19,13 @@ class CostDistributionTest { @Autowired StockDailyDao stockDailyDao; - @Autowired - StockInfoDao stockInfoDao; - @Test void contextLoads() { } @Test void testCalcCost() { - List values = stockDailyDao.getExQfqDailyBeforeTushare("000812.SZ", null, 300L); + List values = stockDailyDao.getExQfqDailyBefore("000812.SZ", null, 300L); CostDistribution cd = Indicators.getCostDistribution(values, 299, 150, 120); log.info("{}", cd); } diff --git a/src/test/java/link/at17/mid/tushare/test/IndicatorTest.java b/src/test/java/link/at17/mid/tushare/test/IndicatorTest.java index 0a4942a..b0e35d5 100644 --- a/src/test/java/link/at17/mid/tushare/test/IndicatorTest.java +++ b/src/test/java/link/at17/mid/tushare/test/IndicatorTest.java @@ -7,8 +7,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; -import link.at17.mid.tushare.data.models.CostDistribution; import link.at17.mid.tushare.data.models.StockValueEx; import link.at17.mid.tushare.data.util.Indicators; import link.at17.mid.tushare.system.util.LocalDateTimeUtils; @@ -21,16 +19,13 @@ class IndicatorTest { @Autowired StockDailyDao stockDailyDao; - @Autowired - StockInfoDao stockInfoDao; - @Test void contextLoads() { } @Test void testIndicator() { - List values = stockDailyDao.getExQfqDailyBeforeTushare("001256.SZ", null, null); + List values = stockDailyDao.getExQfqDailyBefore("001256.SZ", null, null); List vpci = Indicators.getVPCI(values, 14, 5); List vwma = Indicators.getVWMA(values, 14); for (int i = 0; i < values.size(); i++) { diff --git a/src/test/java/link/at17/mid/tushare/test/StockCalendarTest.java b/src/test/java/link/at17/mid/tushare/test/StockCalendarTest.java index a51bfb6..6ffba7e 100644 --- a/src/test/java/link/at17/mid/tushare/test/StockCalendarTest.java +++ b/src/test/java/link/at17/mid/tushare/test/StockCalendarTest.java @@ -12,11 +12,10 @@ import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import link.at17.mid.tushare.dao.StockCalendarDao; import link.at17.mid.tushare.dao.StockDailyDao; -import link.at17.mid.tushare.dao.StockInfoDao; import link.at17.mid.tushare.data.crawler.tushare.TushareCrawler; import link.at17.mid.tushare.data.models.StockCalendar; +import link.at17.mid.tushare.data.service.StockCalendarService; import link.at17.mid.tushare.enums.StockMarket; import link.at17.mid.tushare.system.util.LocalDateTimeUtils; import lombok.extern.slf4j.Slf4j; @@ -29,14 +28,11 @@ class StockCalendarTest { @Autowired TushareCrawler tushareCrawler; - @Autowired - StockCalendarDao stockCalendarDao; - @Autowired StockDailyDao stockDailyDao; - + @Autowired - StockInfoDao stockInfoDao; + StockCalendarService stockCalendarService; /** * 测试开始 @@ -53,7 +49,7 @@ class StockCalendarTest { @Test @Order(2) void testUpdateStockCalendar() { - boolean result = tushareCrawler.updateStockCalendar(); + boolean result = stockCalendarService.updateData(); Assert.assertTrue("updateStockCalendar failed", result); } @@ -64,24 +60,24 @@ class StockCalendarTest { @Order(3) void testQueryLatest() { // 测试不指定市场时的查询,应有查询结果且日期为本年度最后一天 - StockCalendar unassignedLatestCal = stockCalendarDao.getLatest(null); + StockCalendar unassignedLatestCal = stockCalendarService.getLatest(); Assert.assertNotNull(unassignedLatestCal); Assert.assertTrue(LocalDateTimeUtils.isLastDayOfThisYear(unassignedLatestCal.getDate())); // 测试深交所查询,应有查询结果,市场对应,且日期为本年度最后一天 - StockCalendar szLatestCal = stockCalendarDao.getLatest(StockMarket.SZ); + StockCalendar szLatestCal = stockCalendarService.getLatest(StockMarket.SZ); Assert.assertNotNull(szLatestCal); Assert.assertTrue(szLatestCal.getExchange().equals(StockMarket.SZ)); Assert.assertTrue(LocalDateTimeUtils.isLastDayOfThisYear(szLatestCal.getDate())); // 测试上交所查询,应有查询结果,市场对应,且日期为本年度最后一天 - StockCalendar shLatestCal = stockCalendarDao.getLatest(StockMarket.SH); + StockCalendar shLatestCal = stockCalendarService.getLatest(StockMarket.SH); Assert.assertNotNull(shLatestCal); Assert.assertTrue(shLatestCal.getExchange().equals(StockMarket.SH)); Assert.assertTrue(LocalDateTimeUtils.isLastDayOfThisYear(shLatestCal.getDate())); // 测试非深交、上交所查询,应无查询结果 - StockCalendar notSzOrShLatestCal = stockCalendarDao.getLatest(StockMarket.BJ); + StockCalendar notSzOrShLatestCal = stockCalendarService.getLatest(StockMarket.BJ); Assert.assertNull(notSzOrShLatestCal); } @@ -104,13 +100,13 @@ class StockCalendarTest { LocalDateTime d3 = LocalDateTime.of(2021, 8, 14, 0, 0); LocalDateTime p1d123 = LocalDateTime.of(2021, 8, 16, 0, 0); - LocalDateTime d1p1 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d1, 1); // 20210816 - LocalDateTime d1o0 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d1, 0); // 20210813 - LocalDateTime d2p1 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d2, 1); // 20210816 - LocalDateTime d2o0 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d2, 0); // 20210813 - LocalDateTime d3p1 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d3, 1); // 20210816 - LocalDateTime d3n1 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d3, -1); // 20210813 - LocalDateTime d3o0 = stockCalendarDao.getOpenDateOffset(StockMarket.SZ, d3, 0); // null + LocalDateTime d1p1 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d1, 1); // 20210816 + LocalDateTime d1o0 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d1, 0); // 20210813 + LocalDateTime d2p1 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d2, 1); // 20210816 + LocalDateTime d2o0 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d2, 0); // 20210813 + LocalDateTime d3p1 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d3, 1); // 20210816 + LocalDateTime d3n1 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d3, -1); // 20210813 + LocalDateTime d3o0 = stockCalendarService.getOpenDateOffset(StockMarket.SZ, d3, 0); // null log.info("d1: {}, offset 1: {}", d1, d1p1); log.info("d1: {}, offset 0: {}", d1, d1o0); @@ -136,33 +132,33 @@ class StockCalendarTest { long between; LocalDateTime dt1 = LocalDateTime.of(2021, 8, 16, 1, 20, 30); LocalDateTime dt2 = LocalDateTime.of(2021, 8, 20, 19, 20, 30); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, dt1, dt2); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, dt1, dt2); log.info("between {} and {}: {}", dt1, dt2, between); Assert.assertTrue(between == 5); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, dt2, dt1); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, dt2, dt1); log.info("between {} and {}: {}", dt2, dt1, between); Assert.assertTrue(between == 5); LocalDate d1 = LocalDate.of(2021, 8, 16); LocalDate d2 = LocalDate.of(2021, 8, 20); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, d1, d2); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, d1, d2); log.info("between {} and {}: {}", d1, d2, between); Assert.assertTrue(between == 5); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, d2, d1); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, d2, d1); log.info("between {} and {}: {}", d2, d1, between); Assert.assertTrue(between == 5); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, d1, d1); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, d1, d1); log.info("between {} and {}: {}", d1, d1, between); Assert.assertTrue(between == 1); LocalDate d3 = LocalDate.of(2021, 8, 15); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, d3, d1); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, d3, d1); log.info("between {} and {}: {}", d3, d1, between); Assert.assertTrue(between == 1); - between = stockCalendarDao.countOpenDaysBetween(StockMarket.SH, d3, d3); + between = stockCalendarService.countOpenDaysBetween(StockMarket.SH, d3, d3); log.info("between {} and {}: {}", d3, d3, between); Assert.assertTrue(between == 0); @@ -175,7 +171,7 @@ class StockCalendarTest { void testGetAllOpenDatesBetween() { LocalDateTime dt1 = LocalDateTime.of(2021, 8, 16, 1, 20, 30); LocalDateTime dt2 = LocalDateTime.of(2021, 8, 20, 19, 20, 30); - List betweens = stockCalendarDao.getAllOpenDatesBetween(StockMarket.SH, dt1, dt2); + List betweens = stockCalendarService.getAllOpenDatesBetween(StockMarket.SH, dt1, dt2); List assumed = List.of( LocalDateTime.of(2021, 8, 16, 0, 0, 0),