111
This commit is contained in:
Binary file not shown.
@@ -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"}]}
|
||||
{"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"}]}
|
||||
@@ -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":[]}
|
||||
{"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":[]}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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<AssetService>(create: (_) => AssetService(dioClient)),
|
||||
Provider<FundService>(create: (_) => FundService(dioClient)),
|
||||
Provider<BonusService>(create: (_) => BonusService(dioClient)),
|
||||
Provider<KlineService>(create: (_) => KlineService(dioClient)),
|
||||
Provider<KlineWebSocketService>(create: (_) => KlineWebSocketService()),
|
||||
// State Management
|
||||
ChangeNotifierProvider<AuthProvider>(
|
||||
create: (ctx) {
|
||||
@@ -120,6 +125,12 @@ class MyApp extends StatelessWidget {
|
||||
ctx.read<AppEventBus>(),
|
||||
),
|
||||
),
|
||||
ChangeNotifierProvider<KlineProvider>(
|
||||
create: (ctx) => KlineProvider(
|
||||
ctx.read<KlineService>(),
|
||||
ctx.read<KlineWebSocketService>(),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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] },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
13
monisuo-admin/src/types/route-map.d.ts
vendored
13
monisuo-admin/src/types/route-map.d.ts
vendored
@@ -96,6 +96,13 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/monisuo/kline-config': RouteRecordInfo<
|
||||
'/monisuo/kline-config',
|
||||
'/monisuo/kline-config',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| 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'
|
||||
|
||||
19
pom.xml
19
pom.xml
@@ -95,6 +95,25 @@
|
||||
<version>2.9.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- WebSocket STOMP (K线实时推送) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
<version>2.2.4.RELEASE</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis (K线数据持久化 + 价格缓存) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<version>2.2.4.RELEASE</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
<version>2.8.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,25 @@ public class CoinService extends ServiceImpl<CoinMapper, Coin> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存(币种数据变更时调用)
|
||||
*/
|
||||
|
||||
@@ -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("交易价格已变化,请刷新后重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user