diff --git a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill index 6acbbf8..fcc7ced 100644 Binary files a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill and b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill differ diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache index 891131a..89a8686 100644 --- a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/.filecache @@ -1 +1 @@ -{"version":2,"files":[{"path":"D:\\flutter\\bin\\cache\\dart-sdk\\version","hash":"800169ad7335b889bf428af171476466"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"d6b4a7aa67aeb750be9e5aec884f1f73"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml","hash":"03c567345af5a72ca098cfa0a67b3423"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json","hash":"932fae7a247d7a3fd85340e755adb05b"},{"path":"D:\\flutter\\packages\\flutter_tools\\lib\\src\\build_system\\targets\\native_assets.dart","hash":"f78c405bcece3968277b212042da9ed6"},{"path":"d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"d6b4a7aa67aeb750be9e5aec884f1f73"}]} \ No newline at end of file +{"version":2,"files":[{"path":"D:\\flutter\\bin\\cache\\dart-sdk\\version","hash":"800169ad7335b889bf428af171476466"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"210a143189f8879d1701d1cbd9f101c4"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml","hash":"e1161312ba8c4e95e1db1322589118d8"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json","hash":"afae0876d3b33abce85dc7253e57b534"},{"path":"D:\\flutter\\packages\\flutter_tools\\lib\\src\\build_system\\targets\\native_assets.dart","hash":"f78c405bcece3968277b212042da9ed6"},{"path":"d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"210a143189f8879d1701d1cbd9f101c4"}]} \ No newline at end of file diff --git a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json index 4464929..f9ce023 100644 --- a/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json +++ b/flutter_monisuo/build/9a5d09bec60a9bd952a3f584c1b9bd3b/dart_build_result.json @@ -1 +1 @@ -{"build_start":"2026-04-06T01:07:05.140833","build_end":"2026-04-06T01:07:05.218007","dependencies":["file:///D:/flutter/bin/cache/dart-sdk/version","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/pubspec.yaml","file:///d:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json"],"code_assets":[],"data_assets":[]} \ No newline at end of file +{"build_start":"2026-04-06T13:57:48.988866","build_end":"2026-04-06T13:57:52.932920","dependencies":["file:///D:/flutter/bin/cache/dart-sdk/version","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/pubspec.yaml","file:///d:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json"],"code_assets":[],"data_assets":[]} \ No newline at end of file diff --git a/flutter_monisuo/lib/core/constants/api_endpoints.dart b/flutter_monisuo/lib/core/constants/api_endpoints.dart index bac0c4e..3152c52 100644 --- a/flutter_monisuo/lib/core/constants/api_endpoints.dart +++ b/flutter_monisuo/lib/core/constants/api_endpoints.dart @@ -108,4 +108,20 @@ class ApiEndpoints { /// 每日盈亏 static const String dailyProfit = '/api/asset/daily-profit'; + + // ==================== K线模块 ==================== + + /// K线历史数据 + static const String klineHistory = '/api/kline/history'; + + /// 当前K线 + static const String klineCurrent = '/api/kline/current'; + + /// 支持的K线周期 + static const String klineIntervals = '/api/kline/intervals'; + + /// K线 WebSocket 地址 + static const String klineWs = '${isProduction ? 'ws' : 'ws'}://' + '${isProduction ? '8.155.172.147:5010' : 'localhost:5010'}' + '/ws/kline'; } diff --git a/flutter_monisuo/lib/main.dart b/flutter_monisuo/lib/main.dart index 797ee21..c7b7d72 100644 --- a/flutter_monisuo/lib/main.dart +++ b/flutter_monisuo/lib/main.dart @@ -18,9 +18,12 @@ import 'data/services/trade_service.dart'; import 'data/services/asset_service.dart'; import 'data/services/fund_service.dart'; import 'data/services/bonus_service.dart'; +import 'data/services/kline_service.dart'; +import 'data/services/kline_websocket_service.dart'; import 'providers/auth_provider.dart'; import 'providers/market_provider.dart'; import 'providers/asset_provider.dart'; +import 'providers/kline_provider.dart'; import 'providers/theme_provider.dart'; import 'ui/pages/auth/login_page.dart'; import 'ui/pages/main/main_page.dart'; @@ -101,6 +104,8 @@ class MyApp extends StatelessWidget { Provider(create: (_) => AssetService(dioClient)), Provider(create: (_) => FundService(dioClient)), Provider(create: (_) => BonusService(dioClient)), + Provider(create: (_) => KlineService(dioClient)), + Provider(create: (_) => KlineWebSocketService()), // State Management ChangeNotifierProvider( create: (ctx) { @@ -120,6 +125,12 @@ class MyApp extends StatelessWidget { ctx.read(), ), ), + ChangeNotifierProvider( + create: (ctx) => KlineProvider( + ctx.read(), + ctx.read(), + ), + ), ]; } diff --git a/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart b/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart index 7da5938..074f7e9 100644 --- a/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart +++ b/flutter_monisuo/lib/ui/pages/trade/components/coin_selector.dart @@ -4,6 +4,7 @@ import '../../../../core/theme/app_spacing.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/app_theme_extension.dart'; import '../../../../data/models/coin.dart'; +import '../../kline/kline_page.dart'; import 'coin_avatar.dart'; /// 币种选择器组件 @@ -60,6 +61,22 @@ class CoinSelector extends StatelessWidget { ), ], ), + // K线图标(仅选中币种后显示) + if (selectedCoin != null) + GestureDetector( + onTap: () => _navigateToKline(context), + child: Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: context.appColors.surfaceCard, + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all(color: context.appColors.ghostBorder), + ), + child: Icon(LucideIcons.chartNoAxesColumn, + size: 20, color: context.colors.primary), + ), + ), + const SizedBox(width: AppSpacing.sm), // 下拉箭头 Icon(LucideIcons.chevronDown, size: 16, color: context.colors.onSurfaceVariant), @@ -69,6 +86,14 @@ class CoinSelector extends StatelessWidget { ); } + void _navigateToKline(BuildContext context) { + if (selectedCoin == null) return; + Navigator.push( + context, + MaterialPageRoute(builder: (_) => KlinePage(coin: selectedCoin!)), + ); + } + void _showCoinPicker(BuildContext context) { showModalBottomSheet( context: context, diff --git a/flutter_monisuo/pubspec.lock b/flutter_monisuo/pubspec.lock index e829fcd..ed25077 100644 --- a/flutter_monisuo/pubspec.lock +++ b/flutter_monisuo/pubspec.lock @@ -373,6 +373,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + k_chart: + dependency: "direct main" + description: + name: k_chart + sha256: "059163563285cc001dc0257880f598774c63791155a7e0f3a37064dda89c9168" + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: @@ -698,6 +706,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.12.1" + stomp_dart_client: + dependency: "direct main" + description: + name: stomp_dart_client + sha256: "9ca00600a212f1e08fda614cf6815437829b1d08d8911ff5c798f130a2fa2d59" + url: "https://pub.dev" + source: hosted + version: "2.1.3" stream_channel: dependency: transitive description: @@ -818,6 +834,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" xdg_directories: dependency: transitive description: diff --git a/flutter_monisuo/pubspec.yaml b/flutter_monisuo/pubspec.yaml index 7b131c5..3bfffd9 100644 --- a/flutter_monisuo/pubspec.yaml +++ b/flutter_monisuo/pubspec.yaml @@ -40,6 +40,13 @@ dependencies: # 字体 google_fonts: ^6.2.1 + # K线图表 + k_chart: ^0.7.1 + + # WebSocket (STOMP) + stomp_dart_client: ^2.0.0 + web_socket_channel: ^3.0.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/monisuo-admin/src/composables/use-sidebar.ts b/monisuo-admin/src/composables/use-sidebar.ts index 14d2fc1..d1a849b 100644 --- a/monisuo-admin/src/composables/use-sidebar.ts +++ b/monisuo-admin/src/composables/use-sidebar.ts @@ -1,4 +1,4 @@ -import { CircleDollarSign, Coins, DollarSign, Palette, Receipt, Settings, ShieldCheck, TrendingUp, Users } from 'lucide-vue-next' +import { CandlestickChart, CircleDollarSign, Coins, DollarSign, Palette, Receipt, Settings, ShieldCheck, TrendingUp, Users } from 'lucide-vue-next' import type { NavGroup } from '@/components/app-sidebar/types' import { useAuthStore } from '@/stores/auth' @@ -22,6 +22,7 @@ export function useSidebar() { { title: '订单审批', url: '/monisuo/orders', icon: Receipt, roles: [1, 2] }, { title: '财务审批', url: '/monisuo/finance-orders', icon: CircleDollarSign, roles: [1, 3] }, { title: '业务分析', url: '/monisuo/analytics', icon: TrendingUp, roles: [1] }, + { title: 'K线配置', url: '/monisuo/kline-config', icon: CandlestickChart, roles: [1] }, { title: '管理员管理', url: '/monisuo/admins', icon: ShieldCheck, roles: [1] }, ], }, diff --git a/monisuo-admin/src/router/guard/auth-guard.ts b/monisuo-admin/src/router/guard/auth-guard.ts index 1a6534c..ebef2ce 100644 --- a/monisuo-admin/src/router/guard/auth-guard.ts +++ b/monisuo-admin/src/router/guard/auth-guard.ts @@ -8,7 +8,7 @@ import { useAuthStore } from '@/stores/auth' const WHITE_LIST = ['/auth/sign-in', '/auth/sign-up', '/auth/forgot-password'] // 仅超级管理员可访问的路由前缀 -const SUPER_ADMIN_ONLY = ['/monisuo/dashboard', '/monisuo/users', '/monisuo/coins', '/monisuo/analytics', '/monisuo/admins'] +const SUPER_ADMIN_ONLY = ['/monisuo/dashboard', '/monisuo/users', '/monisuo/coins', '/monisuo/analytics', '/monisuo/admins', '/monisuo/kline-config'] export function setupAuthGuard(router: Router) { router.beforeEach((to) => { diff --git a/monisuo-admin/src/types/route-map.d.ts b/monisuo-admin/src/types/route-map.d.ts index bbe56ac..b2cbab5 100644 --- a/monisuo-admin/src/types/route-map.d.ts +++ b/monisuo-admin/src/types/route-map.d.ts @@ -96,6 +96,13 @@ declare module 'vue-router/auto-routes' { Record, | never >, + '/monisuo/kline-config': RouteRecordInfo< + '/monisuo/kline-config', + '/monisuo/kline-config', + Record, + Record, + | never + >, '/monisuo/orders': RouteRecordInfo< '/monisuo/orders', '/monisuo/orders', @@ -212,6 +219,12 @@ declare module 'vue-router/auto-routes' { views: | never } + 'src/pages/monisuo/kline-config.vue': { + routes: + | '/monisuo/kline-config' + views: + | never + } 'src/pages/monisuo/orders.vue': { routes: | '/monisuo/orders' diff --git a/pom.xml b/pom.xml index 2ec2001..deda583 100644 --- a/pom.xml +++ b/pom.xml @@ -95,6 +95,25 @@ 2.9.2 + + + org.springframework.boot + spring-boot-starter-websocket + 2.2.4.RELEASE + + + + + org.springframework.boot + spring-boot-starter-data-redis + 2.2.4.RELEASE + + + org.apache.commons + commons-pool2 + 2.8.0 + + org.projectlombok diff --git a/src/main/java/com/it/rattan/SpcCloudApplication.java b/src/main/java/com/it/rattan/SpcCloudApplication.java index c906266..ce29bb7 100644 --- a/src/main/java/com/it/rattan/SpcCloudApplication.java +++ b/src/main/java/com/it/rattan/SpcCloudApplication.java @@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -14,6 +15,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; @ServletComponentScan(basePackages ={"com.it.rattan"}) @ComponentScan(basePackages ={"com.it.rattan"}) @EnableTransactionManagement +@EnableScheduling /*@EnableAsync @EnableAspectJAutoProxy*/ public class SpcCloudApplication { diff --git a/src/main/java/com/it/rattan/monisuo/entity/Coin.java b/src/main/java/com/it/rattan/monisuo/entity/Coin.java index 45f803c..344ca8d 100644 --- a/src/main/java/com/it/rattan/monisuo/entity/Coin.java +++ b/src/main/java/com/it/rattan/monisuo/entity/Coin.java @@ -90,6 +90,24 @@ public class Coin implements Serializable { /** 排序权重(越大越靠前) */ private Integer sort; + /** 交易开始时间 HH:mm */ + private String tradeStartTime; + + /** 交易结束时间 HH:mm */ + private String tradeEndTime; + + /** 每日最大涨跌幅(%) */ + private BigDecimal maxChangePercent; + + /** K线模拟最低价 */ + private BigDecimal priceMin; + + /** K线模拟最高价 */ + private BigDecimal priceMax; + + /** 1=启用K线模拟 */ + private Integer simulationEnabled; + /** 创建时间 */ @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; diff --git a/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java b/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java index dde69d2..23d0aa2 100644 --- a/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java +++ b/src/main/java/com/it/rattan/monisuo/filter/TokenFilter.java @@ -25,6 +25,9 @@ public class TokenFilter implements Filter { "/api/user/register", "/api/user/login", "/api/wallet/default", + "/api/wallet/networks", + "/api/kline/", + "/ws/", "/admin/login", "/uploads/", "/swagger-resources", diff --git a/src/main/java/com/it/rattan/monisuo/service/AssetService.java b/src/main/java/com/it/rattan/monisuo/service/AssetService.java index b3c527d..781d8ab 100644 --- a/src/main/java/com/it/rattan/monisuo/service/AssetService.java +++ b/src/main/java/com/it/rattan/monisuo/service/AssetService.java @@ -92,9 +92,10 @@ public class AssetService { result.put("tradeBalance", tradeBalance); BigDecimal totalAsset = fundBalance.add(tradeBalance); result.put("totalAsset", totalAsset); - // 总盈亏 = 总资产 - 累计充值(用户净投入为累计充值,总资产超出部分即为盈利) + // 总盈亏 = 总资产 + 累计提现 - 累计充值(净投入 = 充值 - 提现,总资产超出净投入即为盈利) BigDecimal totalDeposit = fund.getTotalDeposit() != null ? fund.getTotalDeposit() : BigDecimal.ZERO; - result.put("totalProfit", totalAsset.subtract(totalDeposit)); + BigDecimal totalWithdraw = fund.getTotalWithdraw() != null ? fund.getTotalWithdraw() : BigDecimal.ZERO; + result.put("totalProfit", totalAsset.add(totalWithdraw).subtract(totalDeposit)); return result; } diff --git a/src/main/java/com/it/rattan/monisuo/service/CoinService.java b/src/main/java/com/it/rattan/monisuo/service/CoinService.java index 38d7539..3934981 100644 --- a/src/main/java/com/it/rattan/monisuo/service/CoinService.java +++ b/src/main/java/com/it/rattan/monisuo/service/CoinService.java @@ -132,6 +132,25 @@ public class CoinService extends ServiceImpl { return list(wrapper); } + /** + * 更新缓存中的币种价格(K线模拟专用,不写DB) + */ + public void updateCachedPrice(String code, BigDecimal price) { + String key = code.toUpperCase(); + Coin cached = coinCodeCache.get(key); + if (cached != null) { + cached.setPrice(price); + } + if (cachedActiveCoins != null) { + for (Coin coin : cachedActiveCoins) { + if (coin.getCode().equalsIgnoreCase(key)) { + coin.setPrice(price); + break; + } + } + } + } + /** * 清除所有缓存(币种数据变更时调用) */ diff --git a/src/main/java/com/it/rattan/monisuo/service/TradeService.java b/src/main/java/com/it/rattan/monisuo/service/TradeService.java index 7efec83..d73d2eb 100644 --- a/src/main/java/com/it/rattan/monisuo/service/TradeService.java +++ b/src/main/java/com/it/rattan/monisuo/service/TradeService.java @@ -14,6 +14,8 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.*; /** @@ -48,6 +50,9 @@ public class TradeService { throw new RuntimeException("该币种已下架"); } + // 模拟币种交易校验 + validateSimulationTrade(coin, price); + // 计算金额 BigDecimal amount = price.multiply(quantity).setScale(8, RoundingMode.DOWN); @@ -128,6 +133,9 @@ public class TradeService { throw new RuntimeException("该币种已下架"); } + // 模拟币种交易校验 + validateSimulationTrade(coin, price); + // 检查持仓 AccountTrade coinAccount = assetService.getOrCreateTradeAccount(userId, coinCode); if (coinAccount.getQuantity().compareTo(quantity) < 0) { @@ -214,4 +222,43 @@ public class TradeService { .eq(OrderTrade::getOrderNo, orderNo); return orderTradeMapper.selectOne(wrapper); } + + /** + * 模拟币种交易校验:交易时段 + 价格一致性 + */ + private void validateSimulationTrade(Coin coin, BigDecimal tradePrice) { + if (coin.getSimulationEnabled() == null || coin.getSimulationEnabled() != 1) { + return; + } + + // 校验交易时段 + if (coin.getTradeStartTime() != null && coin.getTradeEndTime() != null) { + try { + LocalTime now = LocalTime.now(); + DateTimeFormatter fmt = DateTimeFormatter.ofPattern("HH:mm"); + LocalTime start = LocalTime.parse(coin.getTradeStartTime(), fmt); + LocalTime end = LocalTime.parse(coin.getTradeEndTime(), fmt); + boolean inRange; + if (end.isAfter(start)) { + inRange = !now.isBefore(start) && !now.isAfter(end); + } else { + inRange = !now.isBefore(start) || !now.isAfter(end); + } + if (!inRange) { + throw new RuntimeException("当前不在交易时段内(" + coin.getTradeStartTime() + " - " + coin.getTradeEndTime() + ")"); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception ignored) {} + } + + // 校验价格一致性(允许 0.1% 滑点) + if (coin.getPrice() != null && coin.getPrice().compareTo(BigDecimal.ZERO) > 0) { + BigDecimal diff = tradePrice.subtract(coin.getPrice()).abs(); + BigDecimal threshold = coin.getPrice().multiply(new BigDecimal("0.001")); + if (diff.compareTo(threshold) > 0) { + throw new RuntimeException("交易价格已变化,请刷新后重试"); + } + } + } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f80ae5a..b8156cc 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -7,6 +7,17 @@ spring: enabled: true max-file-size: 5MB max-request-size: 10MB + redis: + host: 8.155.172.147 + port: 6379 + password: sion+Rui!$ + database: 1 + timeout: 3000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 2 datasource: username: monisuo password: JPJ8wYicSGC8aRnk