diff --git a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill index 4f57452..f137ae0 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/lib/providers/asset_provider.dart b/flutter_monisuo/lib/providers/asset_provider.dart index c53833e..43c7d59 100644 --- a/flutter_monisuo/lib/providers/asset_provider.dart +++ b/flutter_monisuo/lib/providers/asset_provider.dart @@ -238,7 +238,9 @@ class AssetProvider extends ChangeNotifier { final response = await _fundService.cancelOrder(orderNo); if (response.success) { await loadFundOrders(); - await loadFundAccount(); + await loadFundAccount(force: true); + _eventBus.fire(AppEventType.assetChanged); + _eventBus.fire(AppEventType.orderChanged); } return response; } catch (e) { diff --git a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart index d2aab77..51402c2 100644 --- a/flutter_monisuo/lib/ui/pages/trade/trade_page.dart +++ b/flutter_monisuo/lib/ui/pages/trade/trade_page.dart @@ -3,6 +3,7 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:provider/provider.dart'; import '../../../core/theme/app_spacing.dart'; import '../../../core/theme/app_theme_extension.dart'; +import '../../../core/event/app_event_bus.dart'; import '../../../data/models/coin.dart'; import '../../../providers/market_provider.dart'; import '../../../providers/asset_provider.dart'; @@ -265,6 +266,8 @@ class _TradePageState extends State _amountController.clear(); // 刷新資產數據 context.read().refreshAll(force: true); + // 通知其他頁面刷新 + context.read().fire(AppEventType.assetChanged); _showResultDialog(true, '${isBuy ? '買入' : '賣出'}成功', '$quantity $coinCode @ $price USDT'); } else { diff --git a/monisuo-admin/src/pages/monisuo/finance-orders.vue b/monisuo-admin/src/pages/monisuo/finance-orders.vue index 6f50427..abae680 100644 --- a/monisuo-admin/src/pages/monisuo/finance-orders.vue +++ b/monisuo-admin/src/pages/monisuo/finance-orders.vue @@ -5,7 +5,7 @@ import { toast } from 'vue-sonner' import type { OrderFund } from '@/services/api/monisuo-admin.api' import { BasicPage } from '@/components/global-layout' -import { useApproveOrderMutation, useGetPendingOrdersQuery } from '@/services/api/monisuo-admin.api' +import { useApproveOrderMutation, useGetPendingOrdersQuery, useGetUserStatsQuery } from '@/services/api/monisuo-admin.api' const pageNum = ref(1) const pageSize = ref(10) @@ -28,6 +28,25 @@ const approveStatus = ref(2) const rejectReason = ref('') const adminRemark = ref('') +// 查询订单关联用户的统计数据 +const orderUserId = computed(() => currentOrder.value?.userId ?? 0) +const { data: userStatsData, isLoading: userStatsLoading } = useGetUserStatsQuery(orderUserId) +const userStats = computed(() => userStatsData.value?.data) + +function formatVal(val: number | string | undefined | null): string { + if (val == null) return '0.00' + const n = typeof val === 'string' ? Number.parseFloat(val) : val + if (Number.isNaN(n)) return '0.00' + return n.toFixed(2) +} + +function daysAgo(time: string | undefined | null): number { + if (!time) return 0 + const d = new Date(time.replace('T', ' ')) + if (Number.isNaN(d.getTime())) return 0 + return Math.floor((Date.now() - d.getTime()) / (1000 * 60 * 60 * 24)) +} + function viewOrderDetail(order: OrderFund) { currentOrder.value = order showDetailDialog.value = true @@ -55,12 +74,19 @@ async function handleApprove() { adminRemark: adminRemark.value || undefined, }) - toast.success(`订单已{action}`) + // 立即更新本地订单状态,防止旧状态仍显示操作按钮 + if (approveStatus.value === 2) { + currentOrder.value.status = 2 // 财务通过→已出款 + } else { + currentOrder.value.status = 3 // 驳回 + } + + toast.success(`订单已${action}`) showApproveDialog.value = false refetchPending() } catch (e: any) { - toast.error(e.message || e.response?.data?.msg || `{action}失败`) + toast.error(e.message || e.response?.data?.msg || `${action}失败`) } } @@ -274,7 +300,125 @@ function copyToClipboard(text: string) {
{{ currentOrder.orderNo }}
用户
-
{{ currentOrder.username }}
+
+ {{ currentOrder.username }} + + ID: {{ currentOrder.userId }} + +
+ + +
+
+ +
+
+
+ 用户概况 +
+
+
+
+ 资金余额 +
+
+ {{ formatVal(userStats.fundAccount?.balance) }} +
+
+
+
+ 累计充值 +
+
+ {{ formatVal(userStats.fundAccount?.totalDeposit) }} +
+
+
+
+ 累计提现 +
+
+ {{ formatVal(userStats.fundAccount?.totalWithdraw) }} +
+
+
+
+
+
+ 冻结金额 +
+
+ {{ formatVal(userStats.fundAccount?.frozen) }} +
+
+
+
+ 推广人数 +
+
+ {{ userStats.referralStats?.directCount || 0 }} +
+
+
+
+ 累计福利 +
+
+ {{ formatVal(userStats.bonusStats?.totalBonusClaimed) }} +
+
+
+
+ 注册时间: {{ userStats.user?.createTime || '-' }} +
+ + +
+
提现金额
{{ formatAmount(currentOrder.amount) }}
@@ -346,7 +490,45 @@ function copyToClipboard(text: string) {
用户
-
{{ currentOrder.username }}
+
+ {{ currentOrder.username }} + (ID: {{ currentOrder.userId }}) +
+
+ +
+ +
+
+
+
+
+ 资金余额 +
+
+ {{ formatVal(userStats.fundAccount?.balance) }} +
+
+
+
+ 累计充值 +
+
+ {{ formatVal(userStats.fundAccount?.totalDeposit) }} +
+
+
+
+ 累计提现 +
+
+ {{ formatVal(userStats.fundAccount?.totalWithdraw) }} +
+
+
+
+ 冻结: {{ formatVal(userStats.fundAccount?.frozen) }} | 推广: {{ userStats.referralStats?.directCount || 0 }}人 | 注册: {{ userStats.user?.createTime?.substring(0, 10) || '-' }} +
提现金额
diff --git a/monisuo-admin/src/pages/monisuo/orders.vue b/monisuo-admin/src/pages/monisuo/orders.vue index dbacbf3..340408b 100644 --- a/monisuo-admin/src/pages/monisuo/orders.vue +++ b/monisuo-admin/src/pages/monisuo/orders.vue @@ -6,7 +6,7 @@ import type { OrderFund } from '@/services/api/monisuo-admin.api' import { BasicPage } from '@/components/global-layout' import { useAuthStore } from '@/stores/auth' -import { useApproveOrderMutation, useGetAllOrdersQuery, useGetPendingOrdersQuery } from '@/services/api/monisuo-admin.api' +import { useApproveOrderMutation, useGetAllOrdersQuery, useGetPendingOrdersQuery, useGetUserStatsQuery } from '@/services/api/monisuo-admin.api' // 分离分页状态 const pendingPage = ref(1) @@ -61,6 +61,11 @@ const approveStatus = ref(2) const rejectReason = ref('') const adminRemark = ref('') +// 查询订单关联用户的统计数据 +const orderUserId = computed(() => currentOrder.value?.userId ?? 0) +const { data: userStatsData, isLoading: userStatsLoading } = useGetUserStatsQuery(orderUserId) +const userStats = computed(() => userStatsData.value?.data) + function viewOrderDetail(order: OrderFund) { currentOrder.value = order showDetailDialog.value = true @@ -88,13 +93,27 @@ async function handleApprove() { adminRemark: adminRemark.value || undefined, }) - toast.success(`订单已{action}`) + // 立即更新本地订单状态,防止旧状态仍显示操作按钮 + const role = adminRole.value + if (approveStatus.value === 2) { + if (currentOrder.value.type === 1) { + currentOrder.value.status = 3 // 充值完成 + } else if (role === 2 || (role === 1 && currentOrder.value.status === 1)) { + currentOrder.value.status = 5 // 提现管理员通过→待财务审核 + } else { + currentOrder.value.status = 2 // 提现财务通过→已出款 + } + } else { + currentOrder.value.status = currentOrder.value.type === 1 ? 4 : 3 // 驳回 + } + + toast.success(`订单已${action}`) showApproveDialog.value = false refetchPending() if (allLoaded.value) refetchAll() } catch (e: any) { - toast.error(e.message || e.response?.data?.msg || `{action}失败`) + toast.error(e.message || e.response?.data?.msg || `${action}失败`) } } @@ -129,6 +148,20 @@ function formatAmount(amount: number): string { return amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } +function formatVal(val: number | string | undefined | null): string { + if (val == null) return '0.00' + const n = typeof val === 'string' ? Number.parseFloat(val) : val + if (Number.isNaN(n)) return '0.00' + return n.toFixed(2) +} + +function daysAgo(time: string | undefined | null): number { + if (!time) return 0 + const d = new Date(time.replace('T', ' ')) + if (Number.isNaN(d.getTime())) return 0 + return Math.floor((Date.now() - d.getTime()) / (1000 * 60 * 60 * 24)) +} + // 根据订单类型和状态获取状态样式 function getStatusVariant(order: OrderFund): 'default' | 'secondary' | 'destructive' | 'outline' { const { type, status } = order @@ -673,8 +706,126 @@ function copyToClipboard(text: string) {
{{ currentOrder.username }} + + ID: {{ currentOrder.userId }} +
+ + +
类型
@@ -863,8 +1014,46 @@ function copyToClipboard(text: string) {
{{ currentOrder.username }} + (ID: {{ currentOrder.userId }})
+ +
类型 diff --git a/monisuo-admin/src/services/api/monisuo-admin.api.ts b/monisuo-admin/src/services/api/monisuo-admin.api.ts index 223c426..00a39cb 100644 --- a/monisuo-admin/src/services/api/monisuo-admin.api.ts +++ b/monisuo-admin/src/services/api/monisuo-admin.api.ts @@ -340,6 +340,8 @@ export function useApproveOrderMutation() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['useGetPendingOrdersQuery'] }) queryClient.invalidateQueries({ queryKey: ['useGetAllOrdersQuery'] }) + queryClient.invalidateQueries({ queryKey: ['useGetFinanceOverviewQuery'] }) + queryClient.invalidateQueries({ queryKey: ['useGetUserStatsQuery'] }) }, }) } diff --git a/src/main/java/com/it/rattan/SpcCloudApplication.java b/src/main/java/com/it/rattan/SpcCloudApplication.java index c906266..f7eedff 100644 --- a/src/main/java/com/it/rattan/SpcCloudApplication.java +++ b/src/main/java/com/it/rattan/SpcCloudApplication.java @@ -9,6 +9,8 @@ import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.context.annotation.ComponentScan; import org.springframework.transaction.annotation.EnableTransactionManagement; +import java.util.TimeZone; + @SpringBootApplication @ServletComponentScan(basePackages ={"com.it.rattan"}) @@ -19,6 +21,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; public class SpcCloudApplication { public static void main(String[] args) { + // 统一设置JVM默认时区为北京时间,确保无论服务器部署在哪里时间都一致 + TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); SpringApplication.run(SpcCloudApplication.class, args); } diff --git a/src/main/java/com/it/rattan/monisuo/config/JacksonConfig.java b/src/main/java/com/it/rattan/monisuo/config/JacksonConfig.java index ebba9a8..a14e708 100644 --- a/src/main/java/com/it/rattan/monisuo/config/JacksonConfig.java +++ b/src/main/java/com/it/rattan/monisuo/config/JacksonConfig.java @@ -7,40 +7,60 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + /** * Jackson 配置类 - * 彻底解决 StackOverflowError 问题 + * 统一时间格式为 yyyy-MM-dd HH:mm:ss,时区为 Asia/Shanghai */ @Configuration public class JacksonConfig { + private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; + private static final String DATE_PATTERN = "yyyy-MM-dd"; + @Bean @Primary public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); - + // 设置可见性 mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); - + // 禁用空对象序列化失败 mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); - + // 忽略未知属性 mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - + // 不序列化 null 值 mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - - // 注册 Java 8 时间模块 - mapper.registerModule(new JavaTimeModule()); - + + // 注册 Java 8 时间模块,自定义日期时间格式 + JavaTimeModule module = new JavaTimeModule(); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN); + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + module.addSerializer(java.time.LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter)); + module.addDeserializer(java.time.LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter)); + module.addSerializer(java.time.LocalDate.class, new LocalDateSerializer(dateFormatter)); + module.addDeserializer(java.time.LocalDate.class, new LocalDateDeserializer(dateFormatter)); + mapper.registerModule(module); + // 禁用日期作为时间戳 mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - + + // 统一时区为北京时间 + mapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + return mapper; } } diff --git a/src/main/java/com/it/rattan/monisuo/controller/AdminController.java b/src/main/java/com/it/rattan/monisuo/controller/AdminController.java index 385b7b2..2fa0a02 100644 --- a/src/main/java/com/it/rattan/monisuo/controller/AdminController.java +++ b/src/main/java/com/it/rattan/monisuo/controller/AdminController.java @@ -445,6 +445,7 @@ public class AdminController { coin.setStatus(status); coin.setUpdateTime(LocalDateTime.now()); coinService.updateById(coin); + coinService.clearCache(coin.getCode()); return Result.success(status == 1 ? "已上架" : "已下架", null); } diff --git a/src/main/java/com/it/rattan/monisuo/service/FundService.java b/src/main/java/com/it/rattan/monisuo/service/FundService.java index 6bccf84..0b5f773 100644 --- a/src/main/java/com/it/rattan/monisuo/service/FundService.java +++ b/src/main/java/com/it/rattan/monisuo/service/FundService.java @@ -540,7 +540,7 @@ public class FundService { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); if (adminRole != null && adminRole == 3) { - // 财务只看提现订单 + // 财务:看所有提现订单 wrapper.eq(OrderFund::getType, 2); } else if (type != null && type > 0) { wrapper.eq(OrderFund::getType, type); @@ -550,14 +550,9 @@ public class FundService { wrapper.eq(OrderFund::getStatus, status); } - if (adminId != null) { - if (adminRole != null && adminRole == 3) { - // 财务:看自己审批过的 - wrapper.eq(OrderFund::getFinanceAdminId, adminId); - } else { - // 管理员:看自己审批过的 - wrapper.eq(OrderFund::getApproveAdminId, adminId); - } + if (adminId != null && adminRole != null && adminRole == 2) { + // 普通管理员:看自己审批过的 + wrapper.eq(OrderFund::getApproveAdminId, adminId); } wrapper.orderByDesc(OrderFund::getCreateTime); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index f80ae5a..5a82c80 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -21,6 +21,8 @@ spring: connection-timeout: 30000 connection-test-query: SELECT 1 jackson: + time-zone: Asia/Shanghai + date-format: yyyy-MM-dd HH:mm:ss serialization: write-dates-as-timestamps: false fail-on-empty-beans: false diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml index b607889..c3f8643 100644 --- a/src/main/resources/application-prd.yml +++ b/src/main/resources/application-prd.yml @@ -31,6 +31,15 @@ spring: maxPoolPreparedStatementPerConnectionSize: 20 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 + jackson: + time-zone: Asia/Shanghai + date-format: yyyy-MM-dd HH:mm:ss + serialization: + write-dates-as-timestamps: false + fail-on-empty-beans: false + deserialization: + fail-on-unknown-properties: false + default-property-inclusion: non_null #mybatis-plus