Răsfoiți Sursa

Merge remote-tracking branch 'origin/dev' into dev

# Conflicts:
#	service/src/main/java/com/kym/service/admin/impl/StationServiceImpl.java
skyline 2 ani în urmă
părinte
comite
53b32232d2
83 a modificat fișierele cu 4657 adăugiri și 2030 ștergeri
  1. 2 1
      admin-web/.env.development
  2. 2 0
      admin-web/package.json
  3. 11 5
      admin-web/src/App.vue
  4. 63 24
      admin-web/src/components/form/ExtSelect.vue
  5. 0 1
      admin-web/src/layout/navMenu/vertical.vue
  6. 2 2
      admin-web/src/router/frontEnd.ts
  7. 75 16
      admin-web/src/router/route.ts
  8. 1 1
      admin-web/src/stores/themeConfig.ts
  9. 343 292
      admin-web/src/theme/app.scss
  10. 16 3
      admin-web/src/views/admin/account/index.vue
  11. 1 0
      admin-web/src/views/admin/activity/dialog.vue
  12. 314 0
      admin-web/src/views/admin/dict/index.vue
  13. 1 1
      admin-web/src/views/admin/index.vue
  14. 269 0
      admin-web/src/views/admin/investor/dialog.vue
  15. 276 0
      admin-web/src/views/admin/investor/index.vue
  16. 4 0
      admin-web/src/views/admin/invoice/index.vue
  17. 703 0
      admin-web/src/views/admin/kanban/index.vue
  18. 1 0
      admin-web/src/views/admin/ordering/index.vue
  19. 10 0
      admin-web/src/views/admin/refund/index.vue
  20. 1 0
      admin-web/src/views/admin/station/endpoint/index.vue
  21. 9 1
      admin-web/src/views/admin/station/list/index.vue
  22. 247 0
      admin-web/src/views/admin/station/stat/dialog.vue
  23. 252 6
      admin-web/src/views/admin/station/stat/index.vue
  24. 334 0
      admin-web/src/views/admin/station/statment/dialog.vue
  25. 271 0
      admin-web/src/views/admin/station/statment/index.vue
  26. 10 3
      admin-web/src/views/admin/user/dialog.vue
  27. 1 1
      admin-web/src/views/admin/user/index.vue
  28. 53 46
      admin-web/vite.config.ts
  29. 1 2
      admin/src/main/java/com/kym/admin/controller/AdminUserController.java
  30. 9 0
      admin/src/main/java/com/kym/admin/controller/DataDictController.java
  31. 71 0
      admin/src/main/java/com/kym/admin/controller/InvestorInfoController.java
  32. 49 16
      admin/src/main/java/com/kym/admin/controller/StatController.java
  33. 61 0
      admin/src/main/java/com/kym/admin/controller/StatementsController.java
  34. 11 0
      admin/src/main/java/com/kym/admin/controller/StationController.java
  35. 45 4
      admin/src/main/java/com/kym/admin/jobs/StationStatJob.java
  36. 0 1551
      admin/src/main/resources/template/statement.html
  37. 2 0
      common/src/main/java/com/kym/common/annotation/DynamicCache.java
  38. 37 1
      common/src/main/java/com/kym/common/aspect/DynamicCacheAspect.java
  39. 17 2
      common/src/main/java/com/kym/common/utils/CommUtil.java
  40. 1 0
      database/3-rollback.sql
  41. 3 0
      database/3.sql
  42. 92 0
      entity/src/main/java/com/kym/entity/admin/InvestorInfo.java
  43. 149 0
      entity/src/main/java/com/kym/entity/admin/Statements.java
  44. 15 2
      entity/src/main/java/com/kym/entity/admin/StationStatDay.java
  45. 32 2
      entity/src/main/java/com/kym/entity/admin/StationStatMonth.java
  46. 29 0
      entity/src/main/java/com/kym/entity/admin/queryParams/StatementsQueryParam.java
  47. 50 0
      entity/src/main/java/com/kym/entity/admin/vo/ConnectorVo.java
  48. 1 0
      entity/src/main/java/com/kym/entity/admin/vo/CustomUserVo.java
  49. 182 0
      entity/src/main/java/com/kym/entity/admin/vo/StatementsVo.java
  50. 1 0
      entity/src/main/java/com/kym/entity/miniapp/ChargeOrder.java
  51. 2 0
      entity/src/main/java/com/kym/entity/miniapp/DataDict.java
  52. 16 0
      mapper/src/main/java/com/kym/mapper/admin/InvestorInfoMapper.java
  53. 16 0
      mapper/src/main/java/com/kym/mapper/admin/StatementsMapper.java
  54. 31 0
      mapper/src/main/resources/mappers/admin/InvestorInfoMapper.xml
  55. 42 0
      mapper/src/main/resources/mappers/admin/StatementsMapper.xml
  56. 4 1
      mapper/src/main/resources/mappers/admin/StationStatDayMapper.xml
  57. 6 1
      mapper/src/main/resources/mappers/admin/StationStatMonthMapper.xml
  58. 1 1
      miniapp/src/main/java/com/kym/miniapp/aspect/AppLogAspect.java
  59. 11 0
      miniapp/src/main/java/com/kym/miniapp/controller/DataDictController.java
  60. 22 11
      miniapp/src/main/java/com/kym/miniapp/jobs/StartChargeDelayJob.java
  61. 3 3
      miniapp/src/main/java/com/kym/miniapp/jobs/StopChargeDelayJob.java
  62. 1 1
      service/src/main/java/com/kym/service/admin/AdminUserService.java
  63. 20 0
      service/src/main/java/com/kym/service/admin/InvestorInfoService.java
  64. 24 0
      service/src/main/java/com/kym/service/admin/StatementsService.java
  65. 3 0
      service/src/main/java/com/kym/service/admin/StationService.java
  66. 5 0
      service/src/main/java/com/kym/service/admin/StationStatMonthService.java
  67. 0 5
      service/src/main/java/com/kym/service/admin/impl/ActivityServiceImpl.java
  68. 2 1
      service/src/main/java/com/kym/service/admin/impl/AdminUserServiceImpl.java
  69. 36 0
      service/src/main/java/com/kym/service/admin/impl/InvestorInfoServiceImpl.java
  70. 1 0
      service/src/main/java/com/kym/service/admin/impl/RoleServiceImpl.java
  71. 137 0
      service/src/main/java/com/kym/service/admin/impl/StatementsServiceImpl.java
  72. 44 4
      service/src/main/java/com/kym/service/admin/impl/StationServiceImpl.java
  73. 26 1
      service/src/main/java/com/kym/service/admin/impl/StationStatMonthServiceImpl.java
  74. 6 6
      service/src/main/java/com/kym/service/cache/KymCache.java
  75. 1 1
      service/src/main/java/com/kym/service/enplus/impl/EnNotifyServiceImpl.java
  76. 2 0
      service/src/main/java/com/kym/service/enplus/impl/EnPlusServiceImpl.java
  77. 4 0
      service/src/main/java/com/kym/service/miniapp/DataDictService.java
  78. 5 2
      service/src/main/java/com/kym/service/miniapp/impl/ChargeOrderServiceImpl.java
  79. 14 2
      service/src/main/java/com/kym/service/miniapp/impl/ChargeServiceImpl.java
  80. 29 3
      service/src/main/java/com/kym/service/miniapp/impl/DataDictServiceImpl.java
  81. 9 2
      service/src/main/java/com/kym/service/miniapp/impl/InvoiceServiceImpl.java
  82. 2 0
      service/src/main/java/com/kym/service/miniapp/impl/UserServiceImpl.java
  83. 2 2
      service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

+ 2 - 1
admin-web/.env.development

@@ -2,5 +2,6 @@
 ENV = development
 
 # 本地环境接口地址
-VITE_API_URL = http://localhost:8080/admin/
+VITE_API_URL = http://localhost:8080/admin
+#VITE_API_URL = https://www.kuaiyuman.cn/admin/
 VITE_FILE_URL = http://static.kuaiyuman.cn/

+ 2 - 0
admin-web/package.json

@@ -17,9 +17,11 @@
 		"echarts": "^5.4.1",
 		"echarts-gl": "^2.0.9",
 		"element-plus": "^2.3.9",
+		"html2canvas": "^1.4.1",
 		"js-cookie": "^3.0.1",
 		"js-table2excel": "^1.0.3",
 		"jsencrypt": "^3.3.2",
+		"jspdf": "^2.5.1",
 		"lodash": "^4.17.21",
 		"mitt": "^3.0.0",
 		"nprogress": "^0.2.0",

+ 11 - 5
admin-web/src/App.vue

@@ -22,7 +22,10 @@ import mittBus from '/@/utils/mitt';
 import setIntroduction from '/@/utils/setIconfont';
 import {$get,$body} from "/@/utils/request";
 import u from "/@/utils/u";
+import {initFrontEndControlRoutes} from "/@/router/frontEnd";
+import {useUserInfo} from "/@/stores/userInfo";
 
+const storesUserInfo = useUserInfo();
 // 引入组件
 const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
 const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
@@ -100,18 +103,21 @@ onMounted(() => {
 	});
 });
 
-const refreshEnv = ()=>{
+const   refreshEnv = async ()=>{
   $body("/dataDict/list", {pageSize:1024}).then((res: any) => {
     let {list}  = res;
     var dictGroup = u.groupByKey(list,"code");
     Session.set("dicts", dictGroup);
   })
 
-  $get("/admin-user/profile").then((obj: any) => {
+  $get("/admin-user/profile").then(async (obj: any) => {
     if (obj) {
-      let {user, permissionList} = obj;
-      let userInfo = {...user, permList: permissionList}
-      Session.set('userInfo',userInfo)
+      let user = obj[0];
+      let userInfo = {...user, permList: user.permissions}
+      Session.set('userInfo', userInfo)
+
+      await storesUserInfo.setUserInfos(userInfo);
+      await initFrontEndControlRoutes();
     }
 
   }).catch(err => {

+ 63 - 24
admin-web/src/components/form/ExtSelect.vue

@@ -16,13 +16,13 @@
     <template #prefix>
       <SvgIcon :name="prefix"/>
     </template>
-<!--    <el-option :key="item.id" v-for="item in state.list" :value="item.id" :label="item.name||item.title"> {{ item.name || item.title }}</el-option>-->
+    <!--    <el-option :key="item.id" v-for="item in state.list" :value="item.id" :label="item.name||item.title"> {{ item.name || item.title }}</el-option>-->
   </el-select-v2>
 </template>
 <script lang="ts" setup name="ExtSelect">
 import {onMounted, ref, nextTick, watch, reactive} from 'vue';
 import u from "/@/utils/u";
-import {$body, $post,$get} from "/@/utils/request";
+import {$body, $post, $get} from "/@/utils/request";
 
 const emit = defineEmits(['update:modelValue', 'on-change']);
 
@@ -62,29 +62,47 @@ const props = defineProps({
   dataList: {
     type: Array<any>
   },
-  urlMethod:{
-    type:String,
-    default:'post'
+  urlMethod: {
+    type: String,
+    default: 'post'
   },
-  labelKey:{
-    type:String,
-    default:'name'
+  labelKey: {
+    type: String,
+    default: 'name'
+  },
+  valueKey: {
+    type: String,
+    default: 'id'
   },
- valueKey:{
+  dataKey:{
     type:String,
-    default:'id'
+    default:'list'
   }
 })
 
 const state = reactive({
-  list: [] ,
+  list: [] as Array<any>,
   modelVal: null
 })
 
-watch(()=>props.modelValue,(val)=>{
-  console.log("watch>>>",val)
+watch(() => props.modelValue, (val) => {
+  console.log("watch>>>", val)
   state.modelVal = val;
+})
+
+
+watch(() => props.dataList, (val) => {
+  console.log("watch>>>", val)
+  nextTick(() => {
+    if (!u.isEmptyOrNull(props.dataList)) {
+      // state.list = props.dataList;
+      state.list = props.dataList?.map((k: any) => {
+        return {value: k[props.valueKey], label: k[props.labelKey]}
+      })
+    }
   })
+},{deep:true})
+
 
 onMounted(() => {
   loadData();
@@ -92,7 +110,10 @@ onMounted(() => {
 
 const loadData = () => {
   if (!u.isEmptyOrNull(props.dataList)) {
-    state.list = props.dataList;
+    // state.list = props.dataList;
+    state.list = props.dataList?.map((k: any) => {
+      return {value: k[props.valueKey], label: k[props.labelKey]}
+    })
   } else {
     if (!props.url) {
       return;
@@ -104,27 +125,45 @@ const loadData = () => {
     if (props.query) {
       query = Object.assign({}, query, props.query);
     }
-    if(props.urlMethod?.toLowerCase()==='post'){
+    if (props.urlMethod?.toLowerCase() === 'post') {
       $body(`${props.url}`, query).then((list: any) => {
         // let {list,count}  = res;
-        state.list =  list.map((k:any)=>{
-          return {value:k[props.valueKey],label:k[props.labelKey]}
+        let dataList = [];
+        if(props.dataKey){
+          dataList = list[props.dataKey]
+        }else{
+          dataList = list
+        }
+        state.list = dataList.map((k: any) => {
+          return {value: k[props.valueKey], label: k[props.labelKey]}
         })
         console.log(state.list)
         // state.value.list = res?.list
       });
-    }else{
+    } else {
       $get(`${props.url}`, query).then((list: any) => {
         // let {list,count}  = res;
-        if(list.list){
-          state.list =  list.list.map((k:any)=>{
-            return {value:k[props.valueKey],label:k[props.labelKey]}
-          })
+        let dataList = [];
+        if(props.dataKey){
+          dataList = list[props.dataKey]
         }else{
-          state.list =  list.map((k:any)=>{
-            return {value:k[props.valueKey],label:k[props.labelKey]}
+          dataList = list
+        }
+        state.list = dataList.map((k: any) => {
+          return {value: k[props.valueKey], label: k[props.labelKey]}
+        })
+
+/*
+        if (list.list) {
+          state.list = list.list.map((k: any) => {
+            return {value: k[props.valueKey], label: k[props.labelKey]}
+          })
+        } else {
+          state.list = list.map((k: any) => {
+            return {value: k[props.valueKey], label: k[props.labelKey]}
           })
         }
+*/
 
         console.log(state.list)
         // state.value.list = res?.list

+ 0 - 1
admin-web/src/layout/navMenu/vertical.vue

@@ -4,7 +4,6 @@
 		:default-active="state.defaultActive"
 		background-color="transparent"
 		:collapse="state.isCollapse"
-		:unique-opened="getThemeConfig.isUniqueOpened"
 		:collapse-transition="false"
 	>
 		<template v-for="val in menuLists[0].children">

+ 2 - 2
admin-web/src/router/frontEnd.ts

@@ -132,8 +132,8 @@ export function setFilterMenuAndCacheTagsViewRoutes() {
 	//管理后台路由设置
 	// debugger
 	let routes = [...staticRoutes]
-	console.log("设置后台菜单>>>",adminRoutes)
-	let menuList = setFilterHasPermsMenu(adminRoutes, userInfos.value.permissions);
+	// console.log("设置后台菜单>>>",adminRoutes,userInfos.value.permList)
+	let menuList = setFilterHasPermsMenu(adminRoutes, userInfos.value.permList);
 	console.log("设置后台菜单>>>",menuList)
 	storesRoutesList.setRoutesList(menuList);
 	setCacheTagsViewRoutes();

+ 75 - 16
admin-web/src/router/route.ts

@@ -102,7 +102,21 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                     // perm:'admin'
                 }
             },
-
+            {
+                path: '/kanban',
+                name: 'adminKanban',
+                component: () => import('/@/views/admin/kanban/index.vue'),
+                meta: {
+                    title: '数据看板',
+                    isLink: '',
+                    isHide: false,
+                    isKeepAlive: true,
+                    isAffix: false,
+                    isIframe: false,
+                    icon: 'ele-PictureRounded',
+                    perm:"kanban.list",
+                }
+            },
             {
                 path: '/station',
                 name: 'adminStation',
@@ -149,23 +163,53 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                             icon: 'ele-User',
                         },
                     },
-                    /*         {
-                               path: '/station/stat',
-                               name: 'adminStationStat',
-                               component: () => import('/@/views/admin/station/stat/index.vue'),
-                               meta: {
-                                   title: '营收概览',
-                                   isLink: '',
-                                   isHide: false,
-                                   isKeepAlive: true,
-                                   isAffix: false,
-                                   isIframe: false,
-
-                                   icon: 'ele-Compass',
-                               },
-                           },*/
+                    {
+                        path: '/station/statMonth',
+                        name: 'adminStationStatMonth',
+                        component: () => import('/@/views/admin/station/stat/index.vue'),
+                        meta: {
+                            title: '站点月表',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm:"stationStatMonth.list",
+                            icon: 'ele-Histogram',
+                        },
+                    },
+                    {
+                        path: '/station/statement',
+                        name: 'adminStatement',
+                        component: () => import('/@/views/admin/station/statment/index.vue'),
+                        meta: {
+                            title: '对账单',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm:"statement.list",
+                            icon: 'ele-CreditCard',
+                        },
+                    },
                 ]
             },
+            {
+                path: '/investor',
+                name: 'adminInvestor',
+                component: () => import('/@/views/admin/investor/index.vue'),
+                meta: {
+                    title: '投资者/物业',
+                    isLink: '',
+                    isHide: false,
+                    isKeepAlive: true,
+                    isAffix: false,
+                    isIframe: false,
+                    icon: 'ele-Avatar',
+                    perm:"investor.list",
+                }
+            },
             {
                 path: '/banner',
                 name: 'adminBanner',
@@ -317,6 +361,21 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                             icon: 'ele-Compass',
                         },
                     },
+                    {
+                        path: '/org/dict',
+                        name: 'orgDict',
+                        component: () => import('/@/views/admin/dict/index.vue'),
+                        meta: {
+                            title: '数据字典',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm:"dict.list",
+                            icon: 'ele-Collection',
+                        },
+                    },
                 ]
             },
    /*         {

+ 1 - 1
admin-web/src/stores/themeConfig.ts

@@ -30,7 +30,7 @@ export const useThemeConfig = defineStore('themeConfig', {
             // 默认顶栏导航背景颜色
             topBar: '#FFFFFF',
             // 默认顶栏导航字体颜色
-            topBarColor: '#606266',
+            topBarColor: '#DDEBF7',
             // 是否开启顶栏背景颜色渐变
             isTopBarColorGradual: false,
 

+ 343 - 292
admin-web/src/theme/app.scss

@@ -2,426 +2,477 @@
 ------------------------------- */
 
 * {
-	margin: 0;
-	padding: 0;
-	box-sizing: border-box;
-	outline: none !important;
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  outline: none !important;
 }
 
 :root {
-	--next-color-white: #ffffff;
-	--next-bg-main-color: #f8f8f8;
-	--next-bg-color: #f5f5ff;
-	--next-border-color-light: #f1f2f3;
-	--next-color-primary-lighter: #ecf5ff;
-	--next-color-success-lighter: #f0f9eb;
-	--next-color-warning-lighter: #fdf6ec;
-	--next-color-danger-lighter: #fef0f0;
-	--next-color-dark-hover: #0000001a;
-	--next-color-menu-hover: rgba(0, 0, 0, 0.2);
-	--next-color-user-hover: rgba(0, 0, 0, 0.04);
-	--next-color-seting-main: #e9eef3;
-	--next-color-seting-aside: #d3dce6;
-	--next-color-seting-header: #b3c0d1;
+  --next-color-white: #ffffff;
+  --next-bg-main-color: #f8f8f8;
+  --next-bg-color: #f5f5ff;
+  --next-border-color-light: #f1f2f3;
+  --next-color-primary-lighter: #ecf5ff;
+  --next-color-success-lighter: #f0f9eb;
+  --next-color-warning-lighter: #fdf6ec;
+  --next-color-danger-lighter: #fef0f0;
+  --next-color-dark-hover: #0000001a;
+  --next-color-menu-hover: rgba(0, 0, 0, 0.2);
+  --next-color-user-hover: rgba(0, 0, 0, 0.04);
+  --next-color-seting-main: #e9eef3;
+  --next-color-seting-aside: #d3dce6;
+  --next-color-seting-header: #b3c0d1;
 }
 
 html,
 body,
 #app {
-	margin: 0;
-	padding: 0;
-	width: 100%;
-	height: 100%;
-	font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
-	font-weight: 400;
-	-webkit-font-smoothing: antialiased;
-	-webkit-tap-highlight-color: transparent;
-	background-color: var(--next-bg-main-color);
-	font-size: 14px;
-	overflow: hidden;
-	position: relative;
+  margin: 0;
+  padding: 0;
+  width: 100%;
+  height: 100%;
+  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
+  font-weight: 400;
+  -webkit-font-smoothing: antialiased;
+  -webkit-tap-highlight-color: transparent;
+  background-color: var(--next-bg-main-color);
+  font-size: 14px;
+  overflow: hidden;
+  position: relative;
 }
 
 /* 主布局样式
 ------------------------------- */
 .layout-container {
-	width: 100%;
-	height: 100%;
-	.layout-pd {
-		padding: 15px !important;
-	}
-	.layout-flex {
-		display: flex;
-		flex-direction: column;
-		flex: 1;
-	}
-	.layout-aside {
-		background: var(--next-bg-menuBar);
-		box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
-		height: inherit;
-		position: relative;
-		z-index: 1;
-		display: flex;
-		flex-direction: column;
-		overflow-x: hidden !important;
-		.el-scrollbar__view {
-			overflow: hidden;
-		}
-	}
-	.layout-header {
-		padding: 0 !important;
-		height: auto !important;
-	}
-	.layout-main {
-		padding: 0 !important;
-		overflow: hidden;
-		width: 100%;
-		background-color: var(--next-bg-main-color);
-		display: flex;
-		flex-direction: column;
-		// 内层 el-scrollbar样式,用于界面高度自适应(main.vue)
-		.layout-main-scroll {
-			@extend .layout-flex;
-			.layout-parent {
-				@extend .layout-flex;
-				position: relative;
-			}
-		}
-	}
-	// 用于界面高度自适应
-	.layout-padding {
-		@extend .layout-pd;
-		position: absolute;
-		left: 0;
-		top: 0;
-		height: 100%;
-		width: 100%;
-		overflow: hidden;
-		@extend .layout-flex;
-		&-auto {
-			height: inherit;
-			@extend .layout-flex;
-		}
-		&-view {
-			background: var(--el-color-white);
-			width: 100%;
-			height: 100%;
-			border-radius: 4px;
-			border: 1px solid var(--el-border-color-light, #ebeef5);
-			overflow: hidden;
-		}
-	}
-	// 用于界面高度自适应,主视图区 main 的内边距,用于 iframe
-	.layout-padding-unset {
-		padding: 0 !important;
-		&-view {
-			border-radius: 0 !important;
-			border: none !important;
-		}
-	}
-	// 用于设置 iframe loading 时的高度(loading 垂直居中显示)
-	.layout-iframe {
-		.el-loading-parent--relative {
-			height: 100%;
-		}
-	}
-	.el-scrollbar {
-		width: 100%;
-	}
-	.layout-el-aside-br-color {
-		border-right: 1px solid var(--el-border-color-light, #ebeef5);
-	}
-	// pc端左侧导航样式
-	.layout-aside-pc-220 {
-		width: 220px !important;
-		transition: width 0.3s ease;
-	}
-	.layout-aside-pc-64 {
-		width: 64px !important;
-		transition: width 0.3s ease;
-	}
-	.layout-aside-pc-1 {
-		width: 1px !important;
-		transition: width 0.3s ease;
-	}
-	// 手机端左侧导航样式
-	.layout-aside-mobile {
-		position: fixed;
-		top: 0;
-		left: -220px;
-		width: 220px;
-		z-index: 9999999;
-	}
-	.layout-aside-mobile-close {
-		left: -220px;
-		transition: all 0.3s cubic-bezier(0.39, 0.58, 0.57, 1);
-	}
-	.layout-aside-mobile-open {
-		left: 0;
-		transition: all 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
-	}
-	.layout-aside-mobile-mode {
-		position: fixed;
-		top: 0;
-		right: 0;
-		bottom: 0;
-		left: 0;
-		height: 100%;
-		background-color: rgba(0, 0, 0, 0.5);
-		z-index: 9999998;
-		animation: error-img 0.3s;
-	}
-	.layout-mian-height-50 {
-		height: calc(100vh - 50px);
-	}
-	.layout-columns-warp {
-		flex: 1;
-		display: flex;
-		overflow: hidden;
-	}
-	.layout-hide {
-		display: none;
-	}
+  width: 100%;
+  height: 100%;
+
+  .layout-pd {
+    padding: 15px !important;
+  }
+
+  .layout-flex {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+  }
+
+  .layout-aside {
+    background: var(--next-bg-menuBar);
+    box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
+    height: inherit;
+    position: relative;
+    z-index: 1;
+    display: flex;
+    flex-direction: column;
+    overflow-x: hidden !important;
+
+    .el-scrollbar__view {
+      overflow: hidden;
+    }
+  }
+
+  .layout-header {
+    padding: 0 !important;
+    height: auto !important;
+  }
+
+  .layout-main {
+    padding: 0 !important;
+    overflow: hidden;
+    width: 100%;
+    background-color: var(--next-bg-main-color);
+    display: flex;
+    flex-direction: column;
+    // 内层 el-scrollbar样式,用于界面高度自适应(main.vue)
+    .layout-main-scroll {
+      @extend .layout-flex;
+
+      .layout-parent {
+        @extend .layout-flex;
+        position: relative;
+      }
+    }
+  }
+
+  // 用于界面高度自适应
+  .layout-padding {
+    @extend .layout-pd;
+    position: absolute;
+    left: 0;
+    top: 0;
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+    @extend .layout-flex;
+
+    &-auto {
+      height: inherit;
+      @extend .layout-flex;
+    }
+
+    &-view {
+      background: var(--el-color-white);
+      width: 100%;
+      height: 100%;
+      border-radius: 4px;
+      border: 1px solid var(--el-border-color-light, #ebeef5);
+      overflow: hidden;
+    }
+  }
+
+  // 用于界面高度自适应,主视图区 main 的内边距,用于 iframe
+  .layout-padding-unset {
+    padding: 0 !important;
+
+    &-view {
+      border-radius: 0 !important;
+      border: none !important;
+    }
+  }
+
+  // 用于设置 iframe loading 时的高度(loading 垂直居中显示)
+  .layout-iframe {
+    .el-loading-parent--relative {
+      height: 100%;
+    }
+  }
+
+  .el-scrollbar {
+    width: 100%;
+  }
+
+  .layout-el-aside-br-color {
+    border-right: 1px solid var(--el-border-color-light, #ebeef5);
+  }
+
+  // pc端左侧导航样式
+  .layout-aside-pc-220 {
+    width: 220px !important;
+    transition: width 0.3s ease;
+  }
+
+  .layout-aside-pc-64 {
+    width: 64px !important;
+    transition: width 0.3s ease;
+  }
+
+  .layout-aside-pc-1 {
+    width: 1px !important;
+    transition: width 0.3s ease;
+  }
+
+  // 手机端左侧导航样式
+  .layout-aside-mobile {
+    position: fixed;
+    top: 0;
+    left: -220px;
+    width: 220px;
+    z-index: 9999999;
+  }
+
+  .layout-aside-mobile-close {
+    left: -220px;
+    transition: all 0.3s cubic-bezier(0.39, 0.58, 0.57, 1);
+  }
+
+  .layout-aside-mobile-open {
+    left: 0;
+    transition: all 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
+  }
+
+  .layout-aside-mobile-mode {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    height: 100%;
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 9999998;
+    animation: error-img 0.3s;
+  }
+
+  .layout-mian-height-50 {
+    height: calc(100vh - 50px);
+  }
+
+  .layout-columns-warp {
+    flex: 1;
+    display: flex;
+    overflow: hidden;
+  }
+
+  .layout-hide {
+    display: none;
+  }
 }
 
 /* element plus 全局样式
 ------------------------------- */
 .layout-breadcrumb-seting {
-	.el-divider {
-		background-color: rgb(230, 230, 230);
-	}
+  .el-divider {
+    background-color: rgb(230, 230, 230);
+  }
 }
 
 /* nprogress 进度条跟随主题颜色
 ------------------------------- */
 #nprogress {
-	.bar {
-		background: var(--el-color-primary) !important;
-		z-index: 9999999 !important;
-	}
+  .bar {
+    background: var(--el-color-primary) !important;
+    z-index: 9999999 !important;
+  }
 }
 
 /* flex 弹性布局
 ------------------------------- */
 .flex {
-	display: flex;
+  display: flex;
 }
+
 .flex-auto {
-	flex: 1;
-	overflow: hidden;
+  flex: 1;
+  overflow: hidden;
 }
+
 .flex-center {
-	@extend .flex;
-	flex-direction: column;
-	width: 100%;
-	overflow: hidden;
+  @extend .flex;
+  flex-direction: column;
+  width: 100%;
+  overflow: hidden;
 }
+
 .flex-margin {
-	margin: auto;
+  margin: auto;
 }
+
 .flex-warp {
-	display: flex;
-	flex-wrap: wrap;
-	align-content: flex-start;
-	margin: 0 -5px;
-	.flex-warp-item {
-		padding: 5px;
-		.flex-warp-item-box {
-			width: 100%;
-			height: 100%;
-		}
-	}
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  margin: 0 -5px;
+
+  .flex-warp-item {
+    padding: 5px;
+
+    .flex-warp-item-box {
+      width: 100%;
+      height: 100%;
+    }
+  }
 }
 
-.flex-justify-center{
-	justify-content: center;
+.flex-justify-center {
+  justify-content: center;
 }
 
-.flex-justify-around{
-	justify-content: space-around;
+.flex-justify-around {
+  justify-content: space-around;
 }
-.flex-justify-between{
-	justify-content: space-between;
+
+.flex-justify-between {
+  justify-content: space-between;
 }
-.flex-align-items-center{
-	align-items: center;
-	align-content: center;
+
+.flex-align-items-center {
+  align-items: center;
+  align-content: center;
 }
 
 /* cursor 鼠标形状
 ------------------------------- */
 // 默认
 .cursor-default {
-	cursor: default !important;
+  cursor: default !important;
 }
+
 // 帮助
 .cursor-help {
-	cursor: help !important;
+  cursor: help !important;
 }
+
 // 手指
 .cursor-pointer {
-	cursor: pointer !important;
+  cursor: pointer !important;
 }
+
 // 移动
 .cursor-move {
-	cursor: move !important;
+  cursor: move !important;
 }
 
+// 禁用
+.cursor-disabled {
+  cursor: not-allowed !important;
+}
+
+
 /* 宽高 100%
 ------------------------------- */
 .w100 {
-	width: 100% !important;
+  width: 100% !important;
 }
+
 .h100 {
-	height: 100% !important;
+  height: 100% !important;
 }
+
 .vh100 {
-	height: 100vh !important;
+  height: 100vh !important;
 }
+
 .max100vh {
-	max-height: 100vh !important;
+  max-height: 100vh !important;
 }
+
 .min100vh {
-	min-height: 100vh !important;
+  min-height: 100vh !important;
 }
 
 .wd100 {
-	width: 100px !important;
+  width: 100px !important;
 }
 
 .wd150 {
-	width: 150px !important;
+  width: 150px !important;
 }
 
 .wd200 {
-	width: 200px !important;
+  width: 200px !important;
 }
 
 
 /* 颜色值
 ------------------------------- */
 .color-primary {
-	color: var(--el-color-primary);
+  color: var(--el-color-primary);
 }
+
 .color-success {
-	color: var(--el-color-success);
+  color: var(--el-color-success);
 }
+
 .color-warning {
-	color: var(--el-color-warning);
+  color: var(--el-color-warning);
 }
+
 .color-danger {
-	color: var(--el-color-danger);
+  color: var(--el-color-danger);
 }
+
 .color-info {
-	color: var(--el-color-info);
+  color: var(--el-color-info);
 }
 
 //固定宽度
 @for $i from 0 through 30 {
-	.wd#{$i*5} {
-		width: #{$i*5}px !important;
-	}
+  .wd#{$i*5} {
+    width: #{$i*5}px !important;
+  }
 }
 
 /* 字体大小全局样式
 ------------------------------- */
 @for $i from 10 through 32 {
-	.font#{$i} {
-		font-size: #{$i}px !important;
-	}
+  .font#{$i} {
+    font-size: #{$i}px !important;
+  }
 }
 
 /* 外边距、内边距全局样式
 ------------------------------- */
 @for $i from 1 through 35 {
-	.mt#{$i} {
-		margin-top: #{$i}px !important;
-	}
-	.mr#{$i} {
-		margin-right: #{$i}px !important;
-	}
-	.mb#{$i} {
-		margin-bottom: #{$i}px !important;
-	}
-	.ml#{$i} {
-		margin-left: #{$i}px !important;
-	}
-	.pt#{$i} {
-		padding-top: #{$i}px !important;
-	}
-	.pr#{$i} {
-		padding-right: #{$i}px !important;
-	}
-	.pb#{$i} {
-		padding-bottom: #{$i}px !important;
-	}
-	.pl#{$i} {
-		padding-left: #{$i}px !important;
-	}
-	.pv#{$i}{
-		padding: #{$i}px auto !important;
-	}
+  .mt#{$i} {
+    margin-top: #{$i}px !important;
+  }
+  .mr#{$i} {
+    margin-right: #{$i}px !important;
+  }
+  .mb#{$i} {
+    margin-bottom: #{$i}px !important;
+  }
+  .ml#{$i} {
+    margin-left: #{$i}px !important;
+  }
+  .pt#{$i} {
+    padding-top: #{$i}px !important;
+  }
+  .pr#{$i} {
+    padding-right: #{$i}px !important;
+  }
+  .pb#{$i} {
+    padding-bottom: #{$i}px !important;
+  }
+  .pl#{$i} {
+    padding-left: #{$i}px !important;
+  }
+  .pv#{$i} {
+    padding: #{$i}px auto !important;
+  }
 
-	.ph#{$i}{
-		padding: auto #{$i}px  !important;
-	}
+  .ph#{$i} {
+    padding: auto #{$i}px !important;
+  }
 
-	.pd#{$i}{
-		padding: #{$i}px  !important;
-	}
+  .pd#{$i} {
+    padding: #{$i}px !important;
+  }
 
-	.mv#{$i}{
-		margin: #{$i}px auto !important;
-	}
+  .mv#{$i} {
+    margin: #{$i}px auto !important;
+  }
 
-	.mh#{$i}{
-		margin: auto #{$i}px  !important;
-	}
+  .mh#{$i} {
+    margin: auto #{$i}px !important;
+  }
 }
 
 
-.hc{
-	visibility: hidden !important;
-	transition: all 0.5s;
+.hc {
+  visibility: hidden !important;
+  transition: all 0.5s;
 }
 
-.hp{
-	&:hover{
-		.hc{
-			visibility: visible !important;
-		}
-	}
+.hp {
+  &:hover {
+    .hc {
+      visibility: visible !important;
+    }
+  }
 }
 
-.text-align-left{
-	text-align: left;
+.text-align-left {
+  text-align: left;
 }
 
-.text-align-center{
-	text-align: center;
+.text-align-center {
+  text-align: center;
 }
-.text-align-right{
-	text-align: right;
+
+.text-align-right {
+  text-align: right;
 }
 
 
-.float-right{
-	float: right;
+.float-right {
+  float: right;
 }
 
-.float-left{
-	float: left;
+.float-left {
+  float: left;
 }
 
-.code{
-	width: 100%;
-	padding: 5px;
-	margin: 5px;
-	border-radius: 5px;
-	background-color: #f5f7fa;
-	color:#000;
+.code {
+  width: 100%;
+  padding: 5px;
+  margin: 5px;
+  border-radius: 5px;
+  background-color: #f5f7fa;
+  color: #000;
 }
 
-.scroll_y{
-	overflow-y: scroll;
+.scroll_y {
+  overflow-y: scroll;
 }

+ 16 - 3
admin-web/src/views/admin/account/index.vue

@@ -88,7 +88,7 @@
             :show-overflow-tooltip="!field.fixed&&field.width>150"
         >
           <template #default="{row}">
-            <template v-if="['rechargeAmount','totalMoney','refundAmount','balance','payAmount','discountAmount','refundDiscountAmount'].includes(field.prop)">
+            <template v-if="['rechargeAmount','totalMoney','refundAmount','balance','frozenAmount','payAmount','discountAmount','refundDiscountAmount'].includes(field.prop)">
               {{ u.fmt.fmtMoney(row[field.prop]) }}
             </template>
             <template v-else-if="'status'===field.prop">
@@ -104,6 +104,11 @@
                 <span>  <el-button link type="primary" @click="handleGotoCharge(row)">{{row[field.prop]}} <SvgIcon name="ele-Link" class="hc"></SvgIcon></el-button></span>
               </div>
             </template>
+            <template v-else-if="'refundTimes'===field.prop">
+              <div class="flex w100 flex-justify-around hp">
+                <span>  <el-button link type="primary" @click="handleGotoRefund(row)">{{row[field.prop]}} <SvgIcon name="ele-Link" class="hc"></SvgIcon></el-button></span>
+              </div>
+            </template>
             <template v-else>
               <div>{{ row[field.prop] }}</div>
             </template>
@@ -155,6 +160,7 @@ const state = reactive({
       // {label: '用户名',width: 150,  prop: 'userName', resizable: true, fixed: 'left'},
       {label: '手机号', width: 120, prop: 'mobilePhone', resizable: true, fixed: 'left'},
       {label: '余额', width: 80, prop: 'balance', resizable: true, fixed: 'left'},
+      {label: '冻结余额', width: 90, prop: 'frozenAmount', resizable: true, fixed: 'left'},
       {label: '状态', width: 80, prop: 'status', align: 'center'},
       {label: '注册时间', width: 160, prop: 'registerTime', resizable: true},
       {label: '充值次数', width: 90, prop: 'rechargeTimes', resizable: true},
@@ -225,11 +231,18 @@ const loadData = (refresh: boolean = false) => {
 };
 
 const handleGotoRecharge=(row:any)=>{
-  router.push(`/finance?mobilePhone=${row.mobilePhone}`)
+  let url = router.resolve(`/finance?mobilePhone=${row.mobilePhone}`);
+  window.open(url.href, '_blank');
 }
 
 const handleGotoCharge=(row:any)=>{
-  router.push(`/ordering?mobilePhone=${row.mobilePhone}`)
+  let url = router.resolve(`/ordering?mobilePhone=${row.mobilePhone}`);
+  window.open(url.href, '_blank');
+}
+
+const handleGotoRefund=(row:any)=>{
+  let url = router.resolve(`/refund?mobilePhone=${row.mobilePhone}`);
+  window.open(url.href, '_blank');
 }
 
 // 打开修改用户弹窗

+ 1 - 0
admin-web/src/views/admin/activity/dialog.vue

@@ -136,6 +136,7 @@
               url-method="get"
               label-key="stationName"
               value-key="stationId"
+              data-key=""
               clearable
               class="w100 mt5">
           </ext-select>

+ 314 - 0
admin-web/src/views/admin/dict/index.vue

@@ -0,0 +1,314 @@
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+        <!--        <el-input
+                    v-model="state.formQuery.name"
+                    placeholder="字典名称"
+                    clearable
+                    @blur="loadData(true)"
+                    class="wd150 mr10">
+                </el-input>-->
+
+        <el-input
+            v-model="state.formQuery.code"
+            placeholder="字典编码"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+
+        <el-button class="ml10" plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+
+        <el-button class="ml10" plain size="default" type="primary" @click="handleAddDict" v-auth="'dict.add'">
+          <SvgIcon name="ele-FolderAdd"/>
+          创建
+        </el-button>
+      </el-form>
+
+      <div class="flex">
+        <el-table
+            style="width: 400px;"
+            width="400"
+            border
+            stripe="stripe"
+            :height="state.tableData.height"
+            highlight-current-row
+            current-row-key="id"
+            row-key="id"
+            :data="state.tableData.data"
+            @row-click="handleDictRowClick"
+            v-loading="state.tableData.loading">
+          <template #empty>
+            <el-empty></el-empty>
+          </template>
+          <el-table-column
+              v-for="field in state.columns"
+              :key="field.prop"
+              :label="field.label"
+              :column-key="field.prop"
+              :width="field.width"
+              :min-width="field.minWidth"
+              :fixed="field.fixed"
+              :sortable="field.sortable"
+              :show-overflow-tooltip="!field.fixed&&field.width>150"
+          >
+            <template #default="{row}">
+              <template v-if="field.prop==='expand'">
+                <p style="padding-left: 2em;" v-html="row[field.prop]"></p>
+              </template>
+              <template v-else>
+                <div>{{ row[field.prop] }}</div>
+              </template>
+
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <div class="dict-content__box flex-auto ml10 pr5 pl5" v-if="state.visible">
+          <el-input
+              :readonly="state.dictForm.id"
+              v-model="state.dictForm.code"
+              placeholder="字典编码"
+              clearable
+              size="default"
+              class="w100 mr10">
+          </el-input>
+
+          <el-input
+              v-model="state.dictForm.remark"
+              placeholder="备注"
+              clearable
+              type="textarea"
+              size="default"
+              :rows="2"
+              class="w100  mt10">
+          </el-input>
+
+          <el-row :gutter="10" class="mt20">
+            <el-col :span="8">字典项名称</el-col>
+            <el-col :span="8">字典项码值</el-col>
+            <el-col :span="4">排序权重</el-col>
+          </el-row>
+          <div v-for="(item,idx) in state.dictForm.list" :key="idx" class="mt10">
+            <el-row :gutter="10">
+              <el-col :span="8">
+                <el-input size="default" v-model="item.name" placeholder="字典名称"></el-input>
+              </el-col>
+              <el-col :span="8">
+                <el-input size="default" :readonly="item.id" v-model="item.value" placeholder="字典码值"></el-input>
+              </el-col>
+              <el-col :span="8">
+                <el-input-number :min="1"  size="default" controls-position="right" v-model="item.weight" placeholder="排序权重"></el-input-number>
+                <SvgIcon name="ele-RemoveFilled" color="var(--el-color-danger)" class="ml3 cursor-pointer" @click="handleDeleteDictItem(idx)"></SvgIcon>
+              </el-col>
+            </el-row>
+          </div>
+
+          <el-button  v-auth="'dict.add'" size="small" class="mt10 mr5" type="primary" @click="handleAddDictItem">新增</el-button>
+          <el-button v-auth="'dict.modify'" size="small" class="mt10" type="success" v-if="state.saveVisible" @click="handleSaveDict">保存</el-button>
+
+        </div>
+      </div>
+
+
+      <el-affix position="bottom" :offset="49">
+        <ext-page class="page-pager" v-model:value="state.pageQuery" @change="loadData(false)"/>
+      </el-affix>
+
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="Dict">
+import {defineAsyncComponent, reactive, onMounted, ref, getCurrentInstance, nextTick} from 'vue';
+import ExtPage from "/@/components/form/ExtPage.vue"
+import {$body} from "/@/utils/request";
+import u from "/@/utils/u";
+import {Msg} from "/@/utils/message";
+// 引入组件
+
+const {proxy}: any = getCurrentInstance();
+import {ElButton} from 'element-plus'
+
+// 定义变量内容
+const queryRef = ref();
+const dicDialogRef = ref();
+
+const state = reactive({
+  tableData: {
+    data: [] as Array<any>,
+    total: 0,
+    loading: false,
+    height: 500
+  },
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 20,
+    total: 0
+  },
+  formQuery: {},
+  columns: [
+    {label: '编码', prop: 'code', query: true, type: 'text', resizable: true, width: 250, fixed: 'left'},
+    {label: '备注', prop: 'remark', query: true, type: 'text', resizable: true, width: 160},
+    {
+      label: '操作时间', prop: 'updateTime', width: 180, query: true, type: 'datetime', resizable: true,
+    }
+  ],
+  dictForm: {
+    visible:false,
+    id: 0,
+    code: '',
+    remark: '',
+    list: [] as Array<any>
+  },
+  dictLoading: false,
+  visible:false,
+  saveVisible:false
+});
+
+
+// 初始化表格数据
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  $body('dataDict/listV2', {query:state.formQuery, ...state.pageQuery}).then((res: any) => {
+    let {list, total} = res;
+    state.tableData.data = list;
+    state.pageQuery.total = total;
+    state.tableData.loading = false;
+  })
+};
+/*// 打开新增字典弹窗
+const onOpenAddDic = (type: string) => {
+  console.log(type)
+  dicDialogRef.value.openDialog(type);
+};
+// 打开修改字典弹窗
+const onOpenEditDic = (type: string, row: RowDicType) => {
+  console.log(row)
+  dicDialogRef.value.openDialog(type, row);
+};*/
+// 删除字典
+const onRowDel = (row: RowDicType) => {
+  console.log(row)
+  Msg.confirm(`此操作将永久删除字典名称:“${row.type}”,是否继续?`).then(() => {
+    loadData();
+    Msg.message("删除成功")
+  })
+}
+
+// 页面加载时
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 320
+  })
+});
+
+const handleDictRowClick = (row: any) => {
+  state.dictLoading = true;
+  Msg.showLoading()
+  $body(`dataDict/list`, {query: {code: row.code}}).then(res => {
+    state.visible = true;
+    Msg.hideLoading()
+    state.dictLoading = false;
+    let {list} = res;
+    if (u.isEmptyOrNull(list)) {
+      state.dictForm = {
+        code: '',
+        remark: '',
+        list: []
+      }
+      return;
+    }
+
+    u.sort(list, 'weight');
+    state.dictForm = {
+      id: list[0].id,
+      code: list[0].code,
+      remark: list[0].remark,
+      list: list
+    }
+
+  })
+}
+
+const handleDeleteDictItem = (idx) => {
+  state.saveVisible = true;
+  state.dictForm.list.splice(idx,1)
+}
+
+const handleAddDict = () => {
+  state.saveVisible = true;
+  state.visible = true;
+  state.dictForm = {
+    code: '字典码值',
+    remark: '备注',
+    list: [
+      {name: '', value: '', weight: 0}
+    ]
+  }
+}
+
+const handleAddDictItem = () => {
+  state.saveVisible = true;
+  state.dictForm.list.push({
+    code: '',
+    name: '',
+    value: '',
+    remark: '',
+    weight: 0
+  })
+}
+
+const handleSaveDict = () => {
+  let params = state.dictForm.list.map(k => {
+    let {id, name, value, weight} = k;
+    return {
+      id, name, value, weight,
+      code: state.dictForm.code,
+      remark: state.dictForm.remark
+    }
+  })
+  $body(`dataDict/saveOrUpdate`, params).then(() => {
+    Msg.message("保存成功")
+    state.saveVisible = false;
+  })
+}
+</script>
+
+<style scoped lang="scss">
+.system-container {
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+</style>

+ 1 - 1
admin-web/src/views/admin/index.vue

@@ -217,7 +217,7 @@ const initLineChart = (dataList: Array<any>) => {
     },
     grid: {top: 70, right: 0, bottom: 30, left: 50},
     tooltip: {trigger: 'axis'},
-    legend: {data: ['充电量', /*'总金额','电费',*/'服务费',], right: 0},
+    legend: {data: ['充电量', /*'总金额','电费',*/'实付服务费',], right: 0},
     xAxis: {
       data: xAxis,
       // data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],

+ 269 - 0
admin-web/src/views/admin/investor/dialog.vue

@@ -0,0 +1,269 @@
+<style scoped lang="scss">
+
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-drawer
+        :title="state.dialog.title"
+        v-model="state.dialog.isShowDialog"
+        size="600"
+        class="pd10"
+        append-to-body
+        destroy-on-close
+        :close-on-click-modal="false"
+    >
+      <el-form
+          inline
+          :model="state.ruleForm"
+          :rules="state.rules"
+          label-position="top"
+          ref="formRef"
+          size="default"
+          label-width="100px"
+          class="mt5">
+        <el-form-item label="账户名" prop="accountName">
+          <el-input
+              v-model="state.ruleForm.accountName"
+              placeholder="账户名"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="客户名称" prop="adminUserName">
+          <el-input
+              v-model="state.ruleForm.adminUserName"
+              placeholder="客户名称"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="关联客户" prop="adminUserId">
+          <ext-select
+              v-model="state.ruleForm.adminUserId"
+              placeholder="关联用户"
+              url="admin-user/list"
+              url-method="get"
+              label-key="nickname"
+              clearable
+              class="wd200 ">
+          </ext-select>
+
+<!--          <el-input
+              v-model="state.ruleForm.adminUserId"
+              placeholder="客户用户id"
+              clearable
+              class="wd200">
+          </el-input>-->
+        </el-form-item>
+
+        <el-form-item label="管理站点" prop="stationId">
+          <ext-select
+              v-model="state.ruleForm.stationId"
+              placeholder="关联站点"
+              url="station/listStation"
+              url-method="get"
+              label-key="stationName"
+              value-key="stationId"
+              data-key=""
+              clearable
+              class="wd200 ">
+          </ext-select>
+          <!--          <el-input
+                        v-model="state.ruleForm.stationId"
+                        placeholder="站点id"
+                        clearable
+                        class="wd200">
+                    </el-input>-->
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <ext-d-select
+              v-model="state.ruleForm.status"
+              placeholder="状态"
+              type="Investor.status"
+              clearable
+              class="wd200 "/>
+        </el-form-item>
+        <el-form-item label="电话号码" prop="telephone">
+          <el-input
+              v-model="state.ruleForm.telephone"
+              placeholder="电话号码"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="银行卡号" prop="bankCardNo">
+          <el-input
+              v-model="state.ruleForm.bankCardNo"
+              placeholder="银行卡号"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="开户行名称" prop="bankName" >
+          <el-input
+              v-model="state.ruleForm.bankName"
+              placeholder="开户行名称"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="税号" prop="taxNo">
+          <el-input
+              v-model="state.ruleForm.taxNo"
+              placeholder="税号"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="增值税率" prop="vatRate">
+          <el-input
+              v-model="state.ruleForm.vatRate"
+              placeholder="增值税率 0.06表示6%"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="电损承担比例 (0.30表示30%)" prop="elecLossProportion">
+          <el-input
+              v-model="state.ruleForm.elecLossProportion"
+              placeholder="电损承担比例 0.30表示30%"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="分成比例 (0.45表示45%)" prop="splittingProportion">
+          <el-input
+              v-model="state.ruleForm.splittingProportion"
+              placeholder="分成比例 0.45表示45%"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+
+<!--        <el-form-item label="站点名称" prop="stationName">
+          <el-input
+              v-model="state.ruleForm.stationName"
+              placeholder="站点名称"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>-->
+
+
+
+        <el-form-item label="备注" prop="remark" class="w100">
+          <el-input
+              v-model="state.ruleForm.remark"
+              placeholder="备注"
+              clearable
+              :rows="3"
+              type="textarea"
+              class="w100">
+          </el-input>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onCancel" size="default">取 消</el-button>
+          <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">{{ state.dialog.submitTxt }}</el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts" name="InvestorInfoDialog">
+import {defineAsyncComponent, reactive, onMounted, ref} from 'vue';
+import {Msg} from "/@/utils/message";
+import {$body, $get} from "/@/utils/request";
+import u from '/@/utils/u'
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+import ExtDSelect from "/@/components/form/ExtDSelect.vue";
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+//定义初始变量,重置使用
+const initState = () => ({
+  ruleForm: {
+    id: 0
+  },
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  rules: {
+    telephone:[u.validator.mobile]
+  },
+})
+
+// 定义变量内容
+const state = reactive(initState());
+
+
+// 打开弹窗
+const open = (action: string = 'add', row: any) => {
+  state.dialog.title = u.dialog.actions[action].title + "『投资者/物业』"
+  state.dialog.submitTxt = u.dialog.actions[action].btn + "『投资者/物业』"
+  state.dialog.isShowDialog = true;
+  if (action !== 'add') {
+    loadData(row.id);
+  }
+};
+// 关闭弹窗
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState())
+};
+// 取消
+const onCancel = () => {
+  onClose();
+};
+// 提交
+const onSubmit = () => {
+  formRef.value.validate((valid, fields) => {
+    // console.log('basic checkForm!', valid,fields)
+    if (valid) {
+      state.btnLoading = true;
+      const url = state.ruleForm.id ? "investorInfo/update" : "investorInfo/create"
+      $body(url, state.ruleForm).then(() => {
+        state.btnLoading = false;
+        Msg.message('操作成功');
+        console.log('submit!')
+        onClose();
+        emit('refresh');
+      })
+    } else {
+      state.btnLoading = false;
+      Msg.message('表单校验失败', 'error');
+    }
+  })
+
+};
+
+const handleFormChange = (formData: any) => {
+  console.log(formData)
+}
+
+// 初始化表格数据
+const loadData = (id: any) => {
+  $get(`investorInfo/${id}`).then((res: any) => {
+    state.ruleForm = res;
+  })
+}
+
+// 暴露变量
+defineExpose({
+  open
+});
+
+
+</script>

+ 276 - 0
admin-web/src/views/admin/investor/index.vue

@@ -0,0 +1,276 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+</style>
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+
+
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+        <el-input
+            v-model="state.formQuery.username"
+            placeholder="客户姓名"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+        <el-input
+            v-model="state.formQuery.stationId"
+            placeholder="站点id"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+        <ext-d-select
+            v-model="state.formQuery.status"
+            placeholder="状态"
+            type="Investor.status"
+            clearable
+            @on-change="loadData(true)"
+            class="wd150 mr10"/>
+
+        <!--        <el-input
+                    v-model="state.formQuery.status"
+                    placeholder="状态:0-无效,1-有效"
+                    clearable
+                    @blur="loadData(true)"
+                    class="wd150 mr10">
+                </el-input>-->
+        <el-input
+            v-model="state.formQuery.mobilePhone"
+            placeholder="电话号码"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+
+
+        <el-button class="ml10" plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+
+        <el-button  v-auth="'investor.add'"   size="default" plain  type="success" class="ml10" @click="onRowClick('add',null)">
+          <SvgIcon name="ele-FolderAdd"/>
+          新增
+        </el-button>
+      </el-form>
+
+      <el-table
+          border
+          stripe="stripe"
+          :height="state.tableData.height"
+          highlight-current-row
+          current-row-key="id"
+          row-key="id"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading"
+          @selection-change="handleTableSelectionChange"
+          @sort-change="handleTableSortChange">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column
+            v-for="field in state.tableData.columns"
+            :key="field.prop"
+            :label="field.label"
+            :column-key="field.prop"
+            :width="field.width"
+            :min-width="field.minWidth"
+            :fixed="field.fixed"
+            :sortable="field.sortable"
+            :show-overflow-tooltip="!field.fixed&&field.width>150"
+        >
+          <template #default="{row}">
+            <template v-if="field.prop==='stationId'">
+              <div class="text-align-center">
+                {{ row.stationId }}
+                <hr>
+                {{ row.stationName }}
+              </div>
+
+            </template>
+            <template v-else-if="field.prop==='status'">
+              <ext-d-label type="Investor.status" v-model="row.status"></ext-d-label>
+            </template>
+            <template v-else-if="field.prop==='action'">
+              <el-button  v-auth="'investor.modify'"  size="small" plain  type="warning" @click="onRowClick('edit',row)">编辑</el-button>
+              <el-button  v-auth="'investor.list'"  size="small" plain  type="primary" @click="onRowClick('view',row)">查看</el-button>
+            </template>
+            <template v-else>
+              <div>{{ row[field.prop] }}</div>
+            </template>
+
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <ext-page class="page-pager" v-model:value="state.pageQuery" @change="loadData(false)"/>
+    </el-card>
+  </div>
+  <InvestorInfoDialog ref="investorInfoDialogRef" @refresh="loadData(true)"/>
+</template>
+
+<script setup lang="ts" name="InvestorInfoList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onBeforeUnmount} from 'vue';
+import {$body, $get} from "/@/utils/request";
+import {Msg} from "/@/utils/message";
+
+
+import ExtPage from '/@/components/form/ExtPage.vue'
+
+import mittBus from '/@/utils/mitt';
+import ExtDSelect from "/@/components/form/ExtDSelect.vue";
+import ExtDLabel from "/@/components/form/ExtDLabel.vue";
+
+const InvestorInfoDialog = defineAsyncComponent(() => import("/@/views/admin/investor/dialog.vue"));
+
+//定义引用
+const queryRef = ref();
+const investorInfoDialogRef = ref();
+
+//定义变量
+const state = reactive({
+  formQuery: {},
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 10,
+    total: 0
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array<any>,
+    loading: false,
+    columns: [
+      {label: '客户姓名', prop: 'adminUserName', resizable: true},
+      {label: '电话号码', prop: 'telephone', resizable: true},
+      {label: '站点', prop: 'stationId', resizable: true},
+      {label: '分成比例', prop: 'splittingProportion', resizable: true, width: 90},
+      {label: '电损承担比例', prop: 'elecLossProportion', resizable: true, width: 120},
+      {label: '增值税率', prop: 'vatRate', resizable: true, width: 90},
+      {label: '账户名', prop: 'accountName', resizable: true},
+      {label: '银行卡号', prop: 'bankCardNo', resizable: true, width: 180},
+      {label: '开户行名称', prop: 'bankName', resizable: true, width: 180},
+      {label: '税号', prop: 'taxNo', resizable: true, width: 180},
+      // {label: '备注', prop: 'remark', resizable: true},
+      // {label: '状态', prop: 'status', sortable: 'custom', align: 'center'},
+      // {label: '创建时间', prop: 'createTime', sortable: 'custom', resizable: true, width: 180},
+      // {label: '更新时间', prop: 'updateTime', sortable: 'custom', resizable: true, width: 180},
+      {
+        label: '操作', prop: 'action', width: 180, align: 'center', fixed: 'right',
+      }
+    ],
+  },
+})
+
+
+// 监听双向绑定 modelValue 的变化
+// watch(
+//         () => state.pageIndex,
+//         () => {
+//
+//         }
+// );
+
+//生命周期钩子
+onBeforeMount(() => {
+})
+
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 320
+  })
+
+  mittBus.on("investorInfo.refresh", () => {
+    loadData();
+  })
+});
+
+onBeforeUnmount(() => {
+  mittBus.off("investorInfo.refresh")
+})
+
+
+//region 方法区
+// 初始化表格数据
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  $get(`/investorInfo/list`, {...state.formQuery, ...state.pageQuery}).then((res: any) => {
+    let {list, total} = res;
+    state.tableData.data = list;
+    state.pageQuery.total = total;
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+
+// 打开修改投资者弹窗
+const onRowClick = (type: string, row: any) => {
+  investorInfoDialogRef.value.open(type, row);
+};
+
+// 删除投资者
+const onRowDel = (row: any) => {
+  Msg.confirm(`此操作将永久删除:『${row.name}』,是否继续?`).then(() => {
+    $get(`/investorInfo/delete/${row.id}`).then(() => {
+      Msg.message("删除成功", 'success')
+    }).catch(() => {
+      Msg.message("删除失败", 'error')
+    })
+  });
+};
+
+const handleTableSelectionChange = (selection: any) => {
+  console.log("handleTableSelectionChange>>", selection)
+  // emit("on-check-change", selection)
+}
+
+const handleTableSortChange = (column, prop, order) => {
+  console.log("handleTableSortChange>>", column, prop, order)
+  // emit("on-sort-change", column)
+}
+
+
+//endregion
+
+
+// 暴露变量
+// defineExpose({
+//     loadData,
+// });
+</script>

+ 4 - 0
admin-web/src/views/admin/invoice/index.vue

@@ -164,6 +164,9 @@
             <template v-else-if="field.prop==='serviceMoney'">
               {{ u.fmt.fmtMoney(row[field.prop]) }}
             </template>
+            <template v-else-if="field.prop==='serviceMoneyDiscount'">
+              {{ u.fmt.fmtMoney(row[field.prop]) }}
+            </template>
             <template v-else-if="field.prop==='action'">
               <el-button v-if="row.status===0" v-auth="'invoice.modify'" size="small" plain type="warning" @click="handleInvice(row)">开票</el-button>
               <el-button v-if="row.status===0" v-auth="'invoice.modify'" size="small" plain type="danger" @click="handleCancelInvoice(row)">取消</el-button>
@@ -231,6 +234,7 @@ const state = reactive({
       {label: '发票金额', prop: 'invoiceAmount', resizable: true, width: 90},
       {label: '订单总额', prop: 'totalMoney', resizable: true, width: 90},
       {label: '总服务费', prop: 'serviceMoney', resizable: true, width: 90},
+      {label: '总服务费', prop: 'elecMoney', resizable: true, width: 90},
       {label: '服务费优惠', prop: 'serviceMoneyDiscount', resizable: true, width: 105},
       {label: '开票人', prop: 'biller', resizable: true, width: 80},
       {label: '公司税号', prop: 'taxId', resizable: true, width: 110},

+ 703 - 0
admin-web/src/views/admin/kanban/index.vue

@@ -0,0 +1,703 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+</style>
+<template>
+  <div class="system-container1 layout-padding">
+
+    <el-form
+        inline
+        :model="state.formQuery"
+        ref="queryRef"
+        size="default" label-width="0px" class="mt5 mb5">
+      <el-form-item>
+        <ext-select
+            v-model="state.formQuery.stationIdList"
+            placeholder="站点"
+            value-key="stationId"
+            label-key="stationName"
+            clearable
+            multiple
+            @on-change="loadData"
+            :data-list="state.stationList"
+            style="min-width: 150px;"
+            class=" mr10">
+        </ext-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-radio-group v-model="state.formQuery.type" size="default" @change="handleQueryTypeChange">
+          <el-radio-button label="day">天</el-radio-button>
+          <el-radio-button label="month">月</el-radio-button>
+        </el-radio-group>
+        <el-date-picker
+            :value-format="state.dateFormat"
+            v-model="state.formQuery.dateRange"
+            :type="state.dateType"
+            @change="loadData"
+            unlink-panels
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            :shortcuts="shortcuts"
+        />
+      </el-form-item>
+
+      <el-form-item>
+        <el-button class="ml10" plain size="default" type="success" @click="loadData()">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-scrollbar>
+      <el-card>
+        <el-card shadow="hover" class="layout-padding-auto">
+          <template #header>充电用户及有效订单数</template>
+          <template #default>
+            <div style="min-height: 500px;padding: 20px;" ref="chartOneRef"></div>
+          </template>
+        </el-card>
+
+        <el-card shadow="hover" class="layout-padding-auto mt10">
+          <template #header>电费/服务费</template>
+          <template #default>
+            <div style="min-height: 500px;padding: 20px;" ref="chartTwoRef"></div>
+          </template>
+        </el-card>
+
+        <el-card shadow="hover" class="layout-padding-auto mt10">
+          <template #header>电量/平均电量</template>
+          <template #default>
+            <div style="min-height: 500px;padding: 20px;" ref="chartThreeRef"></div>
+          </template>
+        </el-card>
+
+
+      </el-card>
+
+    </el-scrollbar>
+  </div>
+</template>
+
+<script setup lang="ts" name="KanbanList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onUnmounted, markRaw} from 'vue';
+import {$body, $get} from "/@/utils/request";
+import {Msg} from "/@/utils/message";
+import * as echarts from 'echarts';
+
+import mittBus from '/@/utils/mitt';
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+import u from "/@/utils/u";
+
+//定义引用
+const queryRef = ref();
+const chartOneRef = ref();
+const chartTwoRef = ref();
+const chartThreeRef = ref();
+
+const end = new Date()
+const start = new Date()
+start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
+
+
+//定义变量
+const state = reactive({
+  formQuery: {
+    stationIdList: [],
+    dateRange: [u.date.format(start, "YYYY-MM-DD"), u.date.format(end, "YYYY-MM-DD")],
+    type: 'day'
+  },
+  stationList: [],
+  chartOne: null as any,
+  chartTwo: null as any,
+  chartThree: null as any,
+  chartData: {},
+  theme: '',
+  dateType: 'daterange',
+  dateFormat: 'YYYY-MM-DD'
+})
+
+
+const shortcuts = [
+  {
+    text: '近7天',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
+      return [start, end]
+    },
+  },
+  {
+    text: '近30天',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
+      return [start, end]
+    },
+  },
+  {
+    text: '近90天',
+    value: () => {
+      const end = new Date()
+      const start = new Date()
+      start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
+      return [start, end]
+    },
+  },
+]
+
+
+// 监听双向绑定 modelValue 的变化
+// watch(
+//         () => state.pageIndex,
+//         () => {
+//
+//         }
+// );
+
+//生命周期钩子
+onBeforeMount(() => {
+  loadStationList()
+})
+
+onMounted(() => {
+  loadData();
+});
+
+onUnmounted(() => {
+  state.chartOne?.dispose();
+  state.chartTwo?.dispose();
+  state.chartThree?.dispose();
+})
+
+window.onresize = function () {
+  //自适应大小
+  state.chartOne.resize();
+  state.chartTwo.resize();
+  state.chartThree.resize();
+};
+
+//region 方法区
+
+const handleQueryTypeChange = (type: string) => {
+  console.log(type)
+  if (type === 'day') {
+    state.dateType = 'daterange'
+    state.dateFormat = 'YYYY-MM-DD'
+  } else {
+    state.dateType = 'monthrange'
+    state.dateFormat = 'YYYY-MM-DD'
+  }
+}
+
+const loadStationList = () => {
+  $get(`/station/listStation`, {pageNum: 1024}).then((res: any) => {
+    state.stationList = res;
+    state.formQuery.stationIdList = res.map(k=>k.stationId);
+    console.log(res)
+    setTimeout(()=>{
+      loadData();
+    },200)
+  }).catch(e => {
+    console.error(e)
+  })
+}
+
+// 初始化表格数据
+const loadData = () => {
+  console.log(state.formQuery)
+  let params = {
+    stationIds: state.formQuery.stationIdList,
+    startTime: state.formQuery.dateRange[0],
+    endTime: state.formQuery.dateRange[1],
+    type: state.formQuery.type
+  }
+  for (let i = 0; i < Object.keys(params).length; i++) {
+    if (u.isEmptyOrNull(params[Object.keys(params)[i]])) {
+      return;
+    }
+  }
+  $get(`/stat/stationStatDetail`, params).then(res => {
+    console.log(res)
+    state.chartData = res;
+    buildOrderChart()
+    buildMoneyChart()
+    buildElectricChart()
+  })
+};
+
+/**
+ * 充电人数、有限订单数散点图
+ */
+const buildOrderChart = () => {
+
+  if (state.chartOne) {
+    state.chartOne.dispose();
+  }
+  state.chartOne = markRaw(echarts.init(chartOneRef.value, state.theme));
+  let {type} = state.formQuery
+  let stationIdList = Object.keys(state.chartData);
+  if (u.isEmptyOrNull(stationIdList)) {
+    Msg.message('未查询到统计数据', 'error')
+    return;
+  }
+  let xAxis = state.chartData[`${stationIdList[0]}`].map((k: any) => type==='day'?k.statDay:k.statMonth);
+  let legends:Array<string> =[];
+  state.stationList.filter((k: any) => stationIdList.includes(k.stationId)).forEach(item=>{
+    legends.push(item.stationName)
+    // legends.push(item.stationName+'-平均订单电量')
+    // legends.push(item.stationName+'-平均充电电量')
+  })
+
+  const itemStyle = {
+    opacity: 0.8,
+    shadowBlur: 10,
+    shadowOffsetX: 0,
+    shadowOffsetY: 0,
+    shadowColor: 'rgba(0,0,0,0.3)'
+  };
+
+
+  let y1Max = 0,y2Max=0;
+  let series: Array<any> = [];
+  stationIdList.forEach((stationId: string) => {
+    let tmpUserMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => k.chargeUsers))
+    y1Max = Math.max(y1Max, tmpUserMax)
+
+    let tmpOrderMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => k.validOrders))
+    y2Max = Math.max(y2Max, tmpOrderMax)
+
+    let station = state.stationList.find((k: any) => k.stationId == stationId);
+    let {stationName} = station;
+
+    series.push({
+      name: stationName,
+      type: 'scatter',
+      itemStyle:itemStyle,
+      data: state.chartData[stationId].map((k: any) =>  {
+        return [type==='day'?k.statDay:k.statMonth,k.chargeUsers,k.validOrders]
+      })
+    })
+  });
+
+
+
+
+  const schema = [
+    { name: 'date', index: 0, text: '日' },
+    { name: '充电人次', index: 1, text: '充电人次' },
+    { name: '有效订单', index: 2, text: '有效订单' },
+  ];
+
+  let option = {
+    color: ['#dd4444', '#fec42c', '#80F1BE','#353EE9'],
+    legend: {
+      top: 10,
+      data: legends,
+      textStyle: {
+        fontSize: 16
+      }
+    },
+    grid: {
+      left: '10%',
+      right: 150,
+      top: '18%',
+      bottom: '10%'
+    },
+    tooltip: {
+      backgroundColor: 'rgba(255,255,255,0.7)',
+      formatter: function (param) {
+        var value = param.value;
+        // prettier-ignore
+        return '<div style="border-bottom: 1px solid rgba(255,255,255,.3); font-size: 18px;padding-bottom: 7px;margin-bottom: 7px">'
+            + value[0] + '</br> ' +  param.seriesName + ''
+            + '</div>'
+            + schema[1].text + ':' + value[1] + '人<br>'
+            + schema[2].text + ':' + value[2] + '笔<br>'
+      }
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxis,
+    },
+    yAxis: {
+      type: 'value',
+      name: '数量',
+      nameLocation: 'end',
+      nameGap: 20,
+      nameTextStyle: {
+        fontSize: 16
+      },
+    },
+    visualMap: [
+      {
+        left: 'right',
+        top: '10%',
+        dimension: 1,
+        min: 0,
+        max: y1Max,
+        itemWidth: 30,
+        itemHeight: 120,
+        calculable: true,
+        precision: 0.1,
+        text: ['圆形大小:充电人次'],
+        textGap: 30,
+        inRange: {
+          symbolSize: [10, 70]
+        },
+        outOfRange: {
+          symbolSize: [10, 70],
+          color: ['rgba(255,255,255,0.4)']
+        },
+        controller: {
+          inRange: {
+            color: ['#c23531']
+          },
+          outOfRange: {
+            color: ['#999']
+          }
+        }
+      },
+      {
+        left: 'right',
+        bottom: '5%',
+        dimension: 2,
+        min: 0,
+        max: y2Max,
+        itemHeight: 120,
+        text: ['明暗:有效订单数'],
+        textGap: 30,
+        inRange: {
+          colorLightness: [0.9, 0.5]
+        },
+        outOfRange: {
+          color: ['rgba(255,255,255,0.4)']
+        },
+        controller: {
+          inRange: {
+            color: ['#c23531']
+          },
+          outOfRange: {
+            color: ['#999']
+          }
+        }
+      }
+    ],
+    series: series
+  };
+
+  console.log(option)
+  state.chartOne.setOption(option)
+}
+
+
+/**
+ * 充电量,柱状、折线图
+ */
+const buildElectricChart = () => {
+
+  if (state.chartThree) {
+    state.chartThree.dispose();
+  }
+  state.chartThree = markRaw(echarts.init(chartThreeRef.value, state.theme));
+  let {type} = state.formQuery
+  let stationIdList = Object.keys(state.chartData);
+  if (u.isEmptyOrNull(stationIdList)) {
+    Msg.message('未查询到统计数据', 'error')
+    return;
+  }
+  let xAxis = state.chartData[`${stationIdList[0]}`].map((k: any) => type==='day'?k.statDay:k.statMonth);
+  let legends:Array<string> =[];
+  state.stationList.filter((k: any) => stationIdList.includes(k.stationId)).forEach(item=>{
+    legends.push(item.stationName+'-总电量')
+    legends.push(item.stationName+'-平均充电电量')
+    legends.push(item.stationName+'-平均订单电量')
+  })
+
+  let y1Max = 0, y2Max = 0;
+  let series: Array<any> = [];
+  stationIdList.forEach((stationId: string) => {
+    let tmpUserMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => k.totalPower))
+    y1Max = Math.max(y1Max, tmpUserMax)
+
+    let tmpOrderMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => k.avgOrderElec))
+    y2Max = Math.max(y2Max, tmpOrderMax)
+
+    let station = state.stationList.find((k: any) => k.stationId == stationId);
+    let {stationName} = station;
+    series.push({
+      name: stationName+'-总电量',
+      type: 'bar',
+      barMaxWidth:20,
+      symbolSize: 6,
+      symbol: 'circle',
+      smooth: true,
+      itemStyle: { barBorderRadius: 5},
+      // tooltip: {
+      //   trigger:'axis',
+      //   formatter: ' {a0}  {b}<br />{a} :  {c}人 '
+      // },
+      data: state.chartData[stationId].map((k: any) => k.totalPower)
+    })
+
+
+    series.push({
+      name: stationName+'-平均充电电量',
+      type: 'line',
+      smooth: true,
+      yAxisIndex: 1,
+      data: state.chartData[stationId].map((k: any) => k.avgConnectorElec)
+    })
+
+
+    series.push({
+      name: stationName+'-平均订单电量',
+      type: 'line',
+      smooth: true,
+      yAxisIndex: 1,
+      data: state.chartData[stationId].map((k: any) => k.avgOrderElec)
+    })
+
+
+  });
+
+
+
+  y1Max = Math.max(y1Max, 80)
+  y2Max  =Math.ceil(y2Max +10)
+
+  let y1Interval = Math.ceil(y1Max / 5)
+  let y2Interval =  Math.ceil(y2Max / 5)
+
+
+  let option = {
+    tooltip: {
+      show:true,
+      trigger: 'axis',
+      axisPointer: {
+        type: 'cross',
+        crossStyle: {
+          color: '#999'
+        }
+      }
+    },
+    toolbox: {
+      feature: {
+        // dataView: {show: true, readOnly: false},
+        // magicType: {show: true, type: ['line', 'bar']},
+        // restore: {show: true},
+        // saveAsImage: {show: true}
+      }
+    },
+    legend: {
+      data: legends
+    },
+    xAxis: [
+      {
+        type: 'category',
+        data: xAxis,
+        axisPointer: {
+          type: 'shadow'
+        }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        name: '电量',
+        min: 0,
+        max: y1Max,
+        interval: y1Interval,
+        axisLabel: {
+          formatter: '{value} 度'
+        }
+      },
+      {
+        type: 'value',
+        name: '平均电量',
+        min: 0,
+        max: y2Max,
+        interval: y2Interval,
+        axisLabel: {
+          formatter: '{value} 度'
+        }
+      }
+    ],
+    series: series
+  };
+  // console.log(option)
+  state.chartThree.setOption(option)
+
+
+
+}
+
+
+/**
+ * 充电金额  电费+服务费=总费用
+ */
+const buildMoneyChart = () => {
+  if (state.chartTwo) {
+    state.chartTwo.dispose();
+  }
+  state.chartTwo = markRaw(echarts.init(chartTwoRef.value, state.theme));
+  let {type} = state.formQuery
+  let stationIdList = Object.keys(state.chartData);
+  if (u.isEmptyOrNull(stationIdList)) {
+    Msg.message('未查询到统计数据', 'error')
+    return;
+  }
+  let xAxis = state.chartData[`${stationIdList[0]}`].map((k: any) => type==='day'?k.statDay:k.statMonth);
+  let legends:Array<string> =[];
+  state.stationList.filter((k: any) => stationIdList.includes(k.stationId)).forEach(item=>{
+    legends.push(item.stationName+'-总费用')
+    legends.push(item.stationName+'-电费')
+    legends.push(item.stationName+'-服务费')
+  })
+
+  let y1Max = 0;
+  let series: Array<any> = [];
+  stationIdList.forEach((stationId: string) => {
+    let tmpUserMax = Math.max(...state.chartData[`${stationId}`].map((k: any) => Math.ceil((k.totalMoney/100))))
+    y1Max = Math.max(y1Max, tmpUserMax)
+
+    let station = state.stationList.find((k: any) => k.stationId == stationId);
+    let {stationName} = station;
+
+    series.push({
+      name: stationName+'-总费用',
+      type: 'bar',
+      barMaxWidth:20,
+      symbolSize: 10,
+      symbol: 'circle',
+      smooth: true,
+      itemStyle: { barBorderRadius: 5},
+      emphasis: {
+        focus: 'series'
+      },
+      tooltip: {
+        formatter: ' {a0}  {b}<br />{a} :  {c}元 '
+      },
+      markPoint: {
+        data: [
+          { type: 'max', name: 'Max' },
+          { type: 'min', name: 'Min' }
+        ]
+      },
+      markLine: {
+        data: [{ type: 'average', name: 'Avg' }]
+      },
+      data: state.chartData[stationId].map((k: any) => (k.totalMoney/100).toFixed(2))
+    })
+
+
+    series.push({
+      name: stationName+'-电费',
+      type: 'bar',
+      barMaxWidth:20,
+      smooth: true,
+      stack: 'Fee'+stationId,
+      markPoint: {
+        data: [
+          { type: 'max', name: 'Max' },
+          { type: 'min', name: 'Min' }
+        ]
+      },
+      data: state.chartData[stationId].map((k: any) =>  (k.elecMoney/100).toFixed(2))
+    })
+
+
+    series.push({
+      name: stationName+'-服务费',
+      type: 'bar',
+      barMaxWidth:20,
+      smooth: true,
+      stack:'Fee'+stationId,
+      markPoint: {
+        data: [
+          { type: 'max', name: 'Max' },
+          { type: 'min', name: 'Min' }
+        ]
+      },
+      data: state.chartData[stationId].map((k: any) => (k.serviceMoney/100).toFixed(2))
+    })
+
+  });
+
+  y1Max = Math.max(y1Max, 1000)+100
+
+  let y1Interval = Math.ceil(y1Max / 5)
+
+
+  let option = {
+    tooltip: {
+      show:true,
+      trigger: 'axis',
+      axisPointer: {
+        type: 'cross',
+        crossStyle: {
+          color: '#999'
+        }
+      }
+    },
+    legend: {
+      data: legends,
+      top:-5
+    },
+    xAxis: [
+      {
+        type: 'category',
+        data: xAxis,
+        axisPointer: {
+          type: 'shadow'
+        }
+      }
+    ],
+    yAxis: [
+      {
+        type: 'value',
+        name: '金额(元)',
+        min: 0,
+        max: y1Max,
+        interval: y1Interval,
+        axisLabel: {
+          formatter: '{value} '
+        }
+      },
+    ],
+    series: series
+  };
+  // console.log(option)
+  state.chartTwo.setOption(option)
+
+}
+
+//endregion
+
+</script>

+ 1 - 0
admin-web/src/views/admin/ordering/index.vue

@@ -46,6 +46,7 @@
             clearable
             url="station/listStation"
             urlMethod="get"
+            data-key=""
             label-key="stationName"
             value-key="stationId"
             @on-change="loadData(true)"

+ 10 - 0
admin-web/src/views/admin/refund/index.vue

@@ -154,6 +154,9 @@ import {Msg} from "/@/utils/message";
 import u from "/@/utils/u"
 
 
+import {useRoute} from "vue-router";
+
+const route = useRoute();
 import ExtPage from '/@/components/form/ExtPage.vue'
 
 import mittBus from '/@/utils/mitt';
@@ -218,6 +221,13 @@ onBeforeMount(() => {
 })
 
 onMounted(() => {
+  let query = route.query;
+  console.log(route.params, route.query)
+  if (query.mobilePhone) {
+    state.formQuery.mobilePhone = query.mobilePhone;
+  }
+
+
   loadData();
 
   nextTick(() => {

+ 1 - 0
admin-web/src/views/admin/station/endpoint/index.vue

@@ -48,6 +48,7 @@
             clearable
             url="station/listStation"
             urlMethod="get"
+            data-key=""
             label-key="stationName"
             value-key="stationId"
             @on-change="loadData(true)"

+ 9 - 1
admin-web/src/views/admin/station/list/index.vue

@@ -51,6 +51,7 @@
           <SvgIcon name="ele-FolderAdd"/>
           新增
         </el-button>
+
       </el-form>
 
 <!--      <el-card class="w100">
@@ -117,6 +118,7 @@
     </el-card>
   </div>
   <StationDialog ref="stationDialogRef" @refresh="loadData(true)"/>
+  <SettleDialog ref="settleDialogRef" />
 </template>
 
 <script setup lang="ts" name="StationList">
@@ -136,6 +138,7 @@ const StationDialog = defineAsyncComponent(() => import("/@/views/admin/station/
 //定义引用
 const queryRef = ref();
 const stationDialogRef = ref();
+const settleDialogRef = ref();
 
 //定义变量
 const state = reactive({
@@ -229,7 +232,8 @@ const handleGotoEndpoint = (row:any)=>{
   if(!stationId){
     return;
   }
-  router.push(`/station/endpoint/${stationId}`)
+  let resolve = router.resolve(`/station/endpoint/${stationId}`);
+  window.open(resolve.href, '_blank');
 }
 
 
@@ -276,6 +280,10 @@ const handleTableSortChange = (column, prop, order) => {
   // emit("on-sort-change", column)
 }
 
+const handleSettleShow = () => {
+  settleDialogRef.value?.open();
+}
+
 
 //endregion
 

+ 247 - 0
admin-web/src/views/admin/station/stat/dialog.vue

@@ -0,0 +1,247 @@
+<style scoped lang="scss">
+
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-drawer
+        :title="state.dialog.title"
+        v-model="state.dialog.isShowDialog"
+        width="820px"
+        append-to-body
+        destroy-on-close
+        :close-on-click-modal="false"
+    >
+      <el-form
+          :model="state.ruleForm"
+          :rules="state.rules"
+          label-position="left"
+          ref="formRef"
+          size="default"
+          label-width="100px"
+          class="mt5">
+        <el-form-item label="实际抄表电费金额(分)" prop="actualElecMoney">
+          <el-input
+              v-model="state.ruleForm.actualElecMoney"
+              placeholder="实际抄表电费金额(分)"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="实际抄表电量" prop="actualPower">
+          <el-input
+              v-model="state.ruleForm.actualPower"
+              placeholder="实际抄表电量"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="单枪平均日充电量" prop="avgConnectorElec">
+          <el-input
+              v-model="state.ruleForm.avgConnectorElec"
+              placeholder="单枪平均日充电量"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="订单平均充电量" prop="avgOrderElec">
+          <el-input
+              v-model="state.ruleForm.avgOrderElec"
+              placeholder="订单平均充电量"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="订单平均充电费用" prop="avgOrderMoney">
+          <el-input
+              v-model="state.ruleForm.avgOrderMoney"
+              placeholder="订单平均充电费用"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="充电人数" prop="chargeUsers">
+          <el-input
+              v-model="state.ruleForm.chargeUsers"
+              placeholder="充电人数"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="设备使用率" prop="connectorUsageRate">
+          <el-input
+              v-model="state.ruleForm.connectorUsageRate"
+              placeholder="设备使用率"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="总优惠金额" prop="discountAmount">
+          <el-input
+              v-model="state.ruleForm.discountAmount"
+              placeholder="总优惠金额"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="总电费" prop="elecMoney">
+          <el-input
+              v-model="state.ruleForm.elecMoney"
+              placeholder="总电费"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="总服务费" prop="serviceMoney">
+          <el-input
+              v-model="state.ruleForm.serviceMoney"
+              placeholder="总服务费"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="服务费优惠金额" prop="serviceMoneyDiscount">
+          <el-input
+              v-model="state.ruleForm.serviceMoneyDiscount"
+              placeholder="服务费优惠金额"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="统计时间" prop="statMonth">
+          <el-input
+              v-model="state.ruleForm.statMonth"
+              placeholder="统计时间"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="站点id" prop="stationId">
+          <el-input
+              v-model="state.ruleForm.stationId"
+              placeholder="站点id"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="总充电费用" prop="totalMoney">
+          <el-input
+              v-model="state.ruleForm.totalMoney"
+              placeholder="总充电费用"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="总电量" prop="totalPower">
+          <el-input
+              v-model="state.ruleForm.totalPower"
+              placeholder="总电量"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="充电有效订单数" prop="validOrders">
+          <el-input
+              v-model="state.ruleForm.validOrders"
+              placeholder="充电有效订单数"
+              clearable
+              class="wd200">
+          </el-input>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onCancel" size="default">取 消</el-button>
+          <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">{{ state.dialog.submitTxt }}</el-button>
+        </div>
+      </template>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts" name="StationStatMonthDialog">
+import {defineAsyncComponent, reactive, onMounted, ref} from 'vue';
+import {Msg} from "/@/utils/message";
+import {$body, $get} from "/@/utils/request";
+import u from '/@/utils/u'
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+//定义初始变量,重置使用
+const initState = () => ({
+  ruleForm: {
+    id: 0
+  },
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  rules: {},
+})
+
+// 定义变量内容
+const state = reactive(initState());
+
+
+// 打开弹窗
+const open = (action: string = 'add', row: any) => {
+  state.dialog.title = u.dialog.actions[action].title + "『站点统计表-月』"
+  state.dialog.submitTxt = u.dialog.actions[action].btn + "『站点统计表-月』"
+  state.dialog.isShowDialog = true;
+  if (action !== 'add') {
+    loadData(row.id);
+  }
+};
+// 关闭弹窗
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState())
+};
+// 取消
+const onCancel = () => {
+  onClose();
+};
+// 提交
+const onSubmit = () => {
+  formRef.value.validate((valid, fields) => {
+    // console.log('basic checkForm!', valid,fields)
+    if (valid) {
+      state.btnLoading = true;
+      const url = state.ruleForm.id > 0 ? "/stat/modifyStationStatMonth" : "stationStatMonth/add"
+      $body(url, state.ruleForm).then(() => {
+        state.btnLoading = false;
+        Msg.message('操作成功');
+        console.log('submit!')
+        onClose();
+        emit('refresh');
+      })
+    } else {
+      state.btnLoading = false;
+      Msg.message('表单校验失败', 'error');
+    }
+  })
+
+};
+
+const handleFormChange = (formData: any) => {
+  console.log(formData)
+}
+
+// 初始化表格数据
+const loadData = (id: any) => {
+  $get(`/stat/getStationStatMonth/${id}`).then((res: any) => {
+    state.ruleForm = res;
+  })
+}
+
+// 暴露变量
+defineExpose({
+  open
+});
+
+
+</script>

+ 252 - 6
admin-web/src/views/admin/station/stat/index.vue

@@ -1,13 +1,259 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+</style>
 <template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+
+
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+        <el-input
+            v-model="state.formQuery.statMonth"
+            placeholder="统计时间"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
 
+        <ext-select
+            v-model="state.formQuery.stationId"
+            placeholder="站点"
+            clearable
+            url="station/listStation"
+            urlMethod="get"
+            data-key=""
+            label-key="stationName"
+            value-key="stationId"
+            @on-change="loadData(true)"
+            class="wd150 mr10">
+        </ext-select>
+
+        <el-button class="ml10"  plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+      </el-form>
+
+      <el-table
+          border
+          stripe="stripe"
+          :height="state.tableData.height"
+          highlight-current-row
+          current-row-key="id"
+          row-key="id"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading"
+          @selection-change="handleTableSelectionChange"
+          @sort-change="handleTableSortChange">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column
+            v-for="field in state.tableData.columns"
+            :key="field.prop"
+            :label="field.label"
+            :column-key="field.prop"
+            :width="field.width"
+            :min-width="field.minWidth"
+            :fixed="field.fixed"
+            :sortable="field.sortable"
+            :show-overflow-tooltip="!field.fixed&&field.width>150"
+        >
+          <template #default="{row}">
+            <template v-if="field.prop==='action'">
+              <el-button v-auth="'stationStatMonth.modify'" size="small" plain type="warning" @click="onRowClick('view',row)">编辑</el-button>
+              <el-button v-auth="'stationStatMonth.list'" size="small" plain type="primary" @click="onRowClick('view',row)">查看</el-button>
+              <el-button v-auth="'statement.add'" size="small" plain type="success" @click="handleCreateStatements(row)">生成对账单</el-button>
+            </template>
+            <template v-else-if="field.prop==='stationId'">
+              <div class="text-align-center">
+                {{row.stationId}}
+                <hr>
+                {{row.stationName}}
+              </div>
+            </template>
+            <template v-else-if="['totalMoney','elecMoney','actualElecMoney','serviceMoney','serviceMoneyDiscount','discountAmount','avgOrderMoney'].includes(field.prop)">
+              {{ u.fmt.fmtMoney(row[field.prop]) }}
+            </template>
+            <template v-else>
+              <div>{{row[field.prop]}}</div>
+            </template>
+
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <ext-page class="page-pager" v-model:value="state.pageQuery" @change="loadData(false)"/>
+    </el-card>
+  </div>
+  <StationStatMonthDialog ref="stationStatMonthDialogRef" @refresh="loadData(true)"/>
 </template>
 
-<script>
-export default {
-  name: "index"
+<script setup lang="ts" name="StationStatMonthList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onBeforeUnmount} from 'vue';
+import {$body,$get} from "/@/utils/request";
+import {Msg} from "/@/utils/message";
+import u from "/@/utils/u";
+
+import ExtPage from '/@/components/form/ExtPage.vue'
+
+import mittBus from '/@/utils/mitt';
+import ExtSelect from "/@/components/form/ExtSelect.vue";
+
+const StationStatMonthDialog = defineAsyncComponent(() => import("/@/views/admin/station/stat/dialog.vue"));
+
+//定义引用
+const queryRef = ref();
+const stationStatMonthDialogRef = ref();
+
+//定义变量
+const state = reactive({
+  formQuery: {},
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 10,
+    total: 0
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array < any >,
+    loading: false,
+    columns: [
+      {label: '站点', prop: 'stationId', resizable: true,width:140,fixed:'left'},
+      {label: '统计时间', prop: 'statMonth', resizable: true,width:90},
+      {label: '订单电量', prop: 'totalPower', resizable: true,width:100},
+      {label: '订单金额', prop: 'totalMoney', resizable: true,width:100},
+      {label: '订单电费', prop: 'elecMoney', resizable: true,width:100},
+      {label: '实际抄表电费', prop: 'actualElecMoney', resizable: true,width:120},
+      {label: '订单服务费', prop: 'serviceMoney', resizable: true,width:110},
+      {label: '服务费优惠金额', prop: 'serviceMoneyDiscount', resizable: true,width:135},
+      {label: '总优惠金额', prop: 'discountAmount', resizable: true,width:110},
+      {label: '充电人数', prop: 'chargeUsers', resizable: true,width:100},
+      {label: '充电有效订单数', prop: 'validOrders', resizable: true,width:135},
+      {label: '订单平均充电费用', prop: 'avgOrderMoney', resizable: true,width:145},
+      {label: '单枪平均日充电量', prop: 'avgConnectorElec', resizable: true,width:145},
+      // {label: '创建时间', prop: 'createTime', sortable: 'custom', resizable: true,width:200,fixed: 'right',},
+      {
+        label: '操作', prop: 'action', width: 180, align: 'center', fixed: 'right',
+      }
+    ],
+  },
+})
+
+
+// 监听双向绑定 modelValue 的变化
+// watch(
+//         () => state.pageIndex,
+//         () => {
+//
+//         }
+// );
+
+//生命周期钩子
+onBeforeMount(() => {
+})
+
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 320
+  })
+
+  mittBus.on("stationStatMonth.refresh", () => {
+    loadData();
+  })
+});
+
+onBeforeUnmount(() => {
+  mittBus.off("stationStatMonth.refresh")
+})
+
+
+//region 方法区
+// 初始化表格数据
+const handleCreateStatements = (statMonth) => {
+  $get(`statements/create/${statMonth.id}`).then(()=>{
+    Msg.message(`生成对账单成功`)
+  })
 }
-</script>
 
-<style scoped>
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  $get(`/stat/listStatMonth`, {...state.formQuery, ...state.pageQuery}).then((res: any) => {
+    let {list, total} = res;
+    state.tableData.data = list;
+    state.pageQuery.total = total;
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+
+// 打开修改站点统计表-月弹窗
+const onRowClick = (type: string, row: any) => {
+  stationStatMonthDialogRef.value.open(type, row);
+};
+
+// 删除站点统计表-月
+const onRowDel = (row: any) => {
+  Msg.confirm(`此操作将永久删除:『${row.name}』,是否继续?`).then(() => {
+    $get(`/stationStatMonth/delete/${row.id}`).then(() => {
+      Msg.message("删除成功", 'success')
+    }).catch(() => {
+      Msg.message("删除失败", 'error')
+    })
+  });
+};
+
+const handleTableSelectionChange = (selection: any) => {
+  console.log("handleTableSelectionChange>>", selection)
+  // emit("on-check-change", selection)
+}
+
+const handleTableSortChange = (column, prop, order) => {
+  console.log("handleTableSortChange>>", column, prop, order)
+  // emit("on-sort-change", column)
+}
+
+
+//endregion
+
 
-</style>
+// 暴露变量
+// defineExpose({
+//     loadData,
+// });
+</script>

+ 334 - 0
admin-web/src/views/admin/station/statment/dialog.vue

@@ -0,0 +1,334 @@
+<style scoped lang="scss">
+/*  td{
+    height: 42px;
+    padding-left: 3px;
+  }*/
+
+table {
+  border-collapse: separate;
+  border-top: 1px solid #000;
+  border-left: 1px solid #000;
+}
+
+thead tr th {
+  background-color: #f8f8f9;
+  padding: 6px;
+  text-align: center;
+  border-bottom: 1px solid #000;
+  border-right: 1px solid #000;
+}
+
+tbody tr td {
+  padding: 6px;
+  height: 34px; //设置单元格最小高度
+  border-bottom: 1px solid #000;
+  border-right: 1px solid #000;
+}
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-dialog
+        :title="state.ruleForm.roleName+'对账单'"
+        v-model="state.dialog.isShowDialog"
+        width="1000px"
+        draggable
+        destroy-on-close
+        :close-on-click-modal="false"
+        @close="onClose"
+        align-center>
+
+      <template v-if="state.ruleForm.roleName==='投资者'">
+        <table border="0" cellspacing="0" :id="'settle-table-'+state.ruleForm.roleDesc" class="w100">
+          <tr style="background-color: lightsteelblue;font-weight: bold">
+            <td colspan="9" style="height: 34px;text-align: center;">深圳市快与慢充电桩服务有限公司—{{ state.ruleForm.stationName }}对账单</td>
+          </tr>
+          <tbody>
+          <tr class="text-align-center">
+            <td>客户名称</td>
+            <td :colspan="8">{{ state.ruleForm.adminUserName }}</td>
+          </tr>
+          <tr class="text-align-center">
+            <td>所属期间</td>
+            <td :colspan="8">{{ state.ruleForm.statMonth }}</td>
+          </tr>
+
+          <tr style="background-color: lightsteelblue;">
+            <td colspan="9" style="padding-left: 15px;">一、APP后台数据(含税,订单总金额为电费金额,实际电费为快与慢代收代付)(电量单位为“度”,电费和服务费以及金额单位为“元”)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td>站点名称</td>
+            <td>订单电量</td>
+            <td>①订单电费</td>
+            <td>电表电量</td>
+            <td>②电表电费</td>
+            <td>电损金额=②-①</td>
+            <td>实收对账服务费</td>
+            <td>订单实收总金额</td>
+          </tr>
+          <tr style="text-align: center">
+            <td>{{ state.ruleForm.stationName }}</td>
+            <td>{{ state.ruleForm.totalPower }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.elecMoney) }}</td>
+            <td>{{ state.ruleForm.actualPower }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.actualPower) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.elecLossMoney) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.serviceMoney - state.ruleForm.discountAmount) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.totalMoney - state.ruleForm.discountAmount) }}</td>
+          </tr>
+          <tr style="background-color: lightsteelblue">
+            <td colspan="9" style="padding-left: 15px;">二、客户结算数据(含税)(电费和服务费以及金额单位为“元”)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td>站点名称</td>
+            <td>①对账服务费</td>
+            <td>②服务费分成比例</td>
+            <td>③服务费分成</td>
+            <td>④电损金额</td>
+            <td>⑤电损承担比例</td>
+            <td>⑥电损承担金额</td>
+            <td>⑦分成金额=③-⑥</td>
+          </tr>
+          <tr style="text-align: center">
+            <td>{{ state.ruleForm.stationName }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.actualServiceMoney) }}</td>
+            <td>{{ (state.ruleForm.splittingProportion * 100).toFixed(2) }}%</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.actualServiceMoney * state.ruleForm.splittingProportion) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.elecLossMoney) }}</td>
+            <td>{{ (state.ruleForm.elecLossProportion * 100).toFixed(2) }}%</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.elecLossAmount) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.actualServiceMoney * state.ruleForm.splittingProportion - state.ruleForm.elecLossAmount) }}</td>
+          </tr>
+          <tr style="background-color: lightsteelblue">
+            <td colspan="9" style="padding-left: 15px;">三、客户结算数据(不含税)(税额和金额单位为“元”)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td colspan="2">①分成金额</td>
+            <td colspan="2"> 增值税率</td>
+            <td colspan="2">②应纳增值税额</td>
+            <td colspan="3">应付金额=①-②</td>
+          </tr>
+          <tr style="text-align: center">
+            <td colspan="2">{{ u.fmt.fmtMoney(state.ruleForm.actualServiceMoney * state.ruleForm.splittingProportion - state.ruleForm.elecLossAmount) }}</td>
+            <td colspan="2">{{ (state.ruleForm.vatRate * 100).toFixed(2) }}%</td>
+            <td colspan="2">{{ u.fmt.fmtMoney(state.ruleForm.vatAmount) }}</td>
+            <td colspan="2">{{ u.fmt.fmtMoney(state.ruleForm.actualSplittingAmount) }}</td>
+          </tr>
+          <tr style="background-color: lightsteelblue">
+            <td colspan="9" style="padding-left: 15px;">备注:</td>
+          </tr>
+          <tr>
+            <td colspan="9" style="padding-left: 15px;">
+              分成金额=实际服务费收入*分成比例<br>
+
+              注意:乙方通过运营平台收取扣除相应费用以后的“实际收取的充电服务费收益”*甲方分成比例,甲方收益分成比例为{{ (state.ruleForm.splittingProportion * 100).toFixed(2) }}%。<br>
+
+              结算账户:{{ state.ruleForm.accountName }} &nbsp;&nbsp; 联系电话:{{ state.ruleForm.telephone }} <br>
+
+              开户行:{{ state.ruleForm.bankName }} &nbsp;&nbsp; 银行账号:{{ state.ruleForm.bankCardNo }} <br>
+            </td>
+          </tr>
+          </tbody>
+        </table>
+      </template>
+
+
+      <template v-else>
+        <table border="0" cellspacing="0" :id="'settle-table-'+state.ruleForm.roleDesc"  class="w100">
+          <tr style="background-color: lightsteelblue;font-weight: bold">
+            <td colspan="8" style="height: 34px;text-align: center;">深圳市快与慢充电桩服务有限公司—{{ state.ruleForm.stationName }}对账单</td>
+          </tr>
+          <tbody>
+          <tr class="text-align-center">
+            <td>客户名称</td>
+            <td :colspan="7">{{ state.ruleForm.adminUserName }}</td>
+          </tr>
+          <tr class="text-align-center">
+            <td>所属期间</td>
+            <td :colspan="7">{{ state.ruleForm.statMonth }}</td>
+          </tr>
+
+          <tr style="background-color: lightsteelblue;">
+            <td colspan="8" style="padding-left: 15px;">一、APP后台数据(含税,订单总金额为电费金额,实际电费为快与慢代收代付)(电量单位为“度”,电费和服务费以及金额单位为“元”)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td  colspan="2">站点名称</td>
+            <td colspan="2">充电电量(度)</td>
+            <td colspan="2">订单电费(元)</td>
+            <td >实收服务费(元)</td>
+            <td>订单总金额(元)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td colspan="2">{{ state.ruleForm.stationName }}</td>
+            <td colspan="2">{{ state.ruleForm.totalPower }}</td>
+            <td colspan="2">{{ u.fmt.fmtMoney(state.ruleForm.actualElecMoney) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.serviceMoney ) }}</td>
+            <td>{{ u.fmt.fmtMoney(state.ruleForm.totalMoney ) }}</td>
+          </tr>
+          <tr style="background-color: lightsteelblue">
+            <td colspan="8" style="padding-left: 15px;">二、物业结算数据(含税)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td colspan="2">站点名称</td>
+            <td colspan="2">实付服务费(元)</td>
+            <td colspan="2">物业/场地方分成比例</td>
+            <td colspan="2">物业/场地方分成金额(元)</td>
+          </tr>
+          <tr style="text-align: center">
+            <td colspan="2">{{ state.ruleForm.stationName }}</td>
+            <td colspan="2">{{ u.fmt.fmtMoney(state.ruleForm.actualServiceMoney) }}</td>
+            <td colspan="2">{{ (state.ruleForm.splittingProportion * 100).toFixed(2) }}%</td>
+            <td colspan="2">{{ u.fmt.fmtMoney(state.ruleForm.splittingAmount) }}</td>
+          </tr>
+
+          <tr style="background-color: lightsteelblue">
+            <td colspan="8" style="padding-left: 15px;">备注:</td>
+          </tr>
+          <tr>
+            <td colspan="4" style="padding-left: 15px;">
+              甲方账户信息:<br>
+
+              结算账户:{{ state.ruleForm.accountName }} &nbsp;&nbsp;<br>
+              纳税人识别号:{{ state.ruleForm.taxNo }} <br>
+              开户行:{{ state.ruleForm.bankName }} &nbsp;&nbsp; <br>
+              银行账号:{{ state.ruleForm.bankCardNo }} <br>
+
+              物业确认(盖公章或财务章):
+            </td>
+            <td colspan="3" style="padding-left: 15px;">
+              乙方账户信息:<br>
+
+              结算账户:深圳市快与慢充电桩服务有限公司&nbsp;&nbsp;<br>
+              纳税人识别号:91440300MA5HJNDG14<br>
+              开户行:平安银行深圳深大支行&nbsp;&nbsp; <br>
+              银行账号:15419629160031<br>
+
+              乙方确认(盖公章或财务章):
+            </td>
+            <td  style="padding-left: 15px;">
+              应付账款:{{ u.fmt.fmtMoney(state.ruleForm.splittingAmount) }}元
+            </td>
+          </tr>
+          </tbody>
+        </table>
+      </template>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onCancel" size="default">关 闭</el-button>
+          <el-button :loading="state.btnLoading" type="primary" @click="onSubmit" size="default">下 载PDF</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="StationDialog">
+import {defineAsyncComponent, reactive, onMounted, ref} from 'vue';
+import {$body, $get} from "/@/utils/request";
+import u from "/@/utils/u";
+
+import html2canvas from 'html2canvas';
+import jsPDF from 'jspdf'
+// 引入组件
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+//定义初始变量,重置使用
+const initState = () => ({
+  ruleForm: {
+    id: 0,
+    roleName: '投资者',
+    roleDesc:'investor'
+  },
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  form: {}
+})
+
+// 定义变量内容
+const state = reactive(initState());
+
+
+// 打开弹窗
+const open = (action: string = 'add', row: any) => {
+  state.dialog.isShowDialog = true;
+  loadData(row.id);
+};
+// 关闭弹窗
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState())
+};
+// 取消
+const onCancel = () => {
+  onClose();
+};
+
+const loadData = (id: number) => {
+  $get(`statements/preview/${id}`).then(res => {
+    state.ruleForm = res;
+  })
+}
+
+
+const onSubmit = () => {
+  htmlToPDF(`settle-table-${state.ruleForm.roleDesc}`)
+}
+
+const htmlToPDF = async (htmlId: string, title: string = "对账单", bgColor = "#fff") => {
+  let pdfDom: HTMLElement | null = document.getElementById(htmlId) as HTMLElement
+  pdfDom.style.padding = '0 10px !important'
+  //纵向打印
+  // const A4Width = 595.28;
+  // const A4Height = 841.89;
+  const A4Width = 841.89;
+  const A4Height = 595.28;
+  let canvas = await html2canvas(pdfDom, {
+    scale: 2,
+    useCORS: true,
+    backgroundColor: bgColor,
+  });
+  let pageHeight = (canvas.width / A4Width) * A4Height;
+  let leftHeight = canvas.height;
+  let position = 0;
+  let imgWidth = A4Width;
+  let imgHeight = (A4Width / canvas.width) * canvas.height;
+  /*
+   根据自身业务需求  是否在此处键入下方水印代码
+  */
+  let pageData = canvas.toDataURL("image/jpeg", 1.0);
+  let PDF = new jsPDF("l", 'pt', 'a4');
+  if (leftHeight < pageHeight) {
+    PDF.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight);
+  } else {
+    while (leftHeight > 0) {
+      PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight);
+      leftHeight -= pageHeight;
+      position -= A4Height;
+      if (leftHeight > 0) PDF.addPage();
+    }
+  }
+  PDF.save(title + ".pdf");
+}
+
+
+// 初始化表格数据
+/*const loadData = (id: number) => {
+  $get(`station/detail/${id}`).then((res: any) => {
+    state.ruleForm = res;
+  })
+}*/
+
+// 暴露变量
+defineExpose({
+  open
+});
+
+
+</script>

+ 271 - 0
admin-web/src/views/admin/station/statment/index.vue

@@ -0,0 +1,271 @@
+<style scoped lang="scss">
+.system-container {
+
+  :deep(.el-card__body) {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    flex: 1;
+    overflow: auto;
+
+    .el-table {
+      flex: 1;
+    }
+
+  }
+}
+
+.page-content {
+  margin-bottom: 20px;
+}
+
+.page-pager {
+  background-color: #fff;
+  height: 24px;
+}
+</style>
+<template>
+  <div class="system-container layout-padding">
+    <el-card shadow="hover" class="layout-padding-auto">
+
+
+      <el-form
+          :model="state.formQuery"
+          ref="queryRef"
+          size="default" label-width="0px" class="mt5 mb5">
+        <el-input
+            v-model="state.formQuery.adminUserName"
+            placeholder="客户姓名"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+
+        <el-input
+            v-model="state.formQuery.statMonth"
+            placeholder="统计时间(月)"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+        <el-input
+            v-model="state.formQuery.stationName"
+            placeholder="站点名称"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </el-input>
+        <ext-d-select
+            type="Statement.status"
+            v-model="state.formQuery.status"
+            placeholder="状态"
+            clearable
+            @blur="loadData(true)"
+            class="wd150 mr10">
+        </ext-d-select>
+
+        <el-button class="ml10" plain size="default" type="success" @click="loadData(true)">
+          <SvgIcon name="ele-Search"/>
+          查询
+        </el-button>
+      </el-form>
+
+      <el-table
+          border
+          stripe="stripe"
+          :height="state.tableData.height"
+          highlight-current-row
+          current-row-key="id"
+          row-key="id"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading"
+          @selection-change="handleTableSelectionChange"
+          @sort-change="handleTableSortChange">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column
+            v-for="field in state.tableData.columns"
+            :key="field.prop"
+            :label="field.label"
+            :column-key="field.prop"
+            :width="field.width"
+            :min-width="field.minWidth"
+            :fixed="field.fixed"
+            :sortable="field.sortable"
+            :show-overflow-tooltip="!field.fixed&&field.width>150"
+        >
+          <template #default="{row}">
+            <template v-if="field.prop==='action'">
+              <el-button v-auth="'statement.list'" size="small" plain type="primary" @click="onRowClick('view',row)">查看</el-button>
+            </template>
+            <template v-else-if="field.prop==='stationId'">
+              <div class="text-align-center">
+                {{ row.stationId }}
+                <hr>
+                {{ row.stationName }}
+              </div>
+            </template>
+            <template v-else-if="field.prop==='status'">
+              <ext-d-label type="Statement.status" v-model="row.status"/>
+            </template>
+            <template v-else-if="['actualElecMoney','actualServiceMoney','actualSplittingAmount','discountAmount','elecLossMoney','elecMoney','serviceMoney','serviceMoneyDiscount','splittingAmount','totalMoney','vatAmount'].includes(field.prop)">
+              {{ u.fmt.fmtMoney(row[field.prop]) }}
+            </template>
+            <template v-else>
+              <div>{{ row[field.prop] }}</div>
+            </template>
+
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <ext-page class="page-pager" v-model:value="state.pageQuery" @change="loadData(false)"/>
+    </el-card>
+  </div>
+  <StatementsDialog ref="statementsDialogRef" @refresh="loadData(true)"/>
+</template>
+
+<script setup lang="ts" name="StatementsList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onBeforeUnmount} from 'vue';
+import {$body, $get} from "/@/utils/request";
+import {Msg} from "/@/utils/message";
+import u from "/@/utils/u";
+
+
+import ExtPage from '/@/components/form/ExtPage.vue'
+
+import mittBus from '/@/utils/mitt';
+import ExtDLabel from "/@/components/form/ExtDLabel.vue";
+import ExtDSelect from "/@/components/form/ExtDSelect.vue";
+
+const StatementsDialog = defineAsyncComponent(() => import("/@/views/admin/station/statment/dialog.vue"));
+
+//定义引用
+const queryRef = ref();
+const statementsDialogRef = ref();
+
+//定义变量
+const state = reactive({
+  formQuery: {},
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 10,
+    total: 0
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array<any>,
+    loading: false,
+    columns: [
+      {label: '客户姓名', prop: 'adminUserName', resizable: true, fixed: 'left', width: 150},
+      {label: '站点', prop: 'stationId', resizable: true, width: 130},
+      {label: '统计时间(月)', prop: 'statMonth', resizable: true, width: 135},
+      // {label: '状态', prop: 'status', sortable: 'custom', align: 'center', width: 130},
+      {label: '实际抄表电费金额', prop: 'actualElecMoney', resizable: true, width: 150},
+      {label: '实际参与分成的服务费', prop: 'actualServiceMoney', resizable: true, width: 180},
+      {label: '实际分成金额', prop: 'actualSplittingAmount', resizable: true, width: 140},
+
+      {label: '优惠金额', prop: 'discountAmount', resizable: true, width: 100},
+      {label: '电损电费金额', prop: 'elecLossMoney', resizable: true, width: 120},
+      {label: '订单电费金额', prop: 'elecMoney', resizable: true, width: 120},
+
+      {label: '服务费金额', prop: 'serviceMoney', resizable: true, width: 120},
+      {label: '服务费优惠金额', prop: 'serviceMoneyDiscount', resizable: true, width: 130},
+      {label: '分成金额', prop: 'splittingAmount', resizable: true, width: 100},
+
+      {label: '订单金额', prop: 'totalMoney', resizable: true, width: 100},
+      {label: '增值税额', prop: 'vatAmount', resizable: true, width: 100},
+      {label: '创建时间', prop: 'createTime', sortable: 'custom', resizable: true, width: 200, fixed: 'right'},
+      {
+        label: '操作', prop: 'action', width: 100, align: 'center', fixed: 'right',
+      }
+    ],
+  },
+})
+
+
+// 监听双向绑定 modelValue 的变化
+// watch(
+//         () => state.pageIndex,
+//         () => {
+//
+//         }
+// );
+
+//生命周期钩子
+onBeforeMount(() => {
+})
+
+onMounted(() => {
+  loadData();
+
+  nextTick(() => {
+    let bodyHeight = document.body.clientHeight;
+    let queryHeight = queryRef.value.$el.clientHeight;
+    state.tableData.height = bodyHeight - queryHeight - 320
+  })
+
+  mittBus.on("statements.refresh", () => {
+    loadData();
+  })
+});
+
+onBeforeUnmount(() => {
+  mittBus.off("statements.refresh")
+})
+
+
+//region 方法区
+// 初始化表格数据
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  $get(`/statements/listStatements`, {...state.formQuery, ...state.pageQuery}).then((res: any) => {
+    let {list, total} = res;
+    state.tableData.data = list;
+    state.pageQuery.total = total;
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+
+// 打开修改客户对账单弹窗
+const onRowClick = (type: string, row: any) => {
+  statementsDialogRef.value.open(type, row);
+};
+
+// 删除客户对账单
+const onRowDel = (row: any) => {
+  Msg.confirm(`此操作将永久删除:『${row.name}』,是否继续?`).then(() => {
+    $get(`/statements/delete/${row.id}`).then(() => {
+      Msg.message("删除成功", 'success')
+    }).catch(() => {
+      Msg.message("删除失败", 'error')
+    })
+  });
+};
+
+const handleTableSelectionChange = (selection: any) => {
+  console.log("handleTableSelectionChange>>", selection)
+  // emit("on-check-change", selection)
+}
+
+const handleTableSortChange = (column, prop, order) => {
+  console.log("handleTableSortChange>>", column, prop, order)
+  // emit("on-sort-change", column)
+}
+
+
+//endregion
+
+
+// 暴露变量
+// defineExpose({
+//     loadData,
+// });
+</script>

+ 10 - 3
admin-web/src/views/admin/user/dialog.vue

@@ -9,6 +9,7 @@
         size="620px"
         append-to-body
         destroy-on-close
+        class="pd10"
         :close-on-click-modal="false"
         @close="onClose"
     >
@@ -45,6 +46,8 @@
               v-model="state.ruleForm.password"
               placeholder="密码"
               clearable
+              show-password
+              type="password"
               class="w100">
           </el-input>
         </el-form-item>
@@ -114,7 +117,7 @@ const formRef = ref();
 //定义初始变量,重置使用
 const initState = () => ({
   ruleForm: {
-    // id: 0
+    id: 0
   },
   btnLoading: false,
   dialog: {
@@ -125,7 +128,7 @@ const initState = () => ({
   },
   rules: {
     username: [u.validator.required],
-    mobilePhone: [u.validator.required],
+    mobilePhone: [u.validator.required,u.validator.mobile],
     nickname: [u.validator.required],
     status: [u.validator.required],
     password: [u.validator.required],
@@ -175,7 +178,11 @@ const onSubmit = () => {
     if (valid) {
       state.btnLoading = true;
       const url = state.ruleForm.id > 0 ? "admin-user/modify" : "admin-user/add"
-      $body(url, state.ruleForm).then(() => {
+      $body(url, state.ruleForm).then((id) => {
+        console.log(id)
+        if(id){
+          state.ruleForm.id = id;
+        }
         state.btnLoading = false;
         Msg.message('操作成功');
         console.log('submit!')

+ 1 - 1
admin-web/src/views/admin/user/index.vue

@@ -117,7 +117,7 @@
             </template>
             <template v-else-if="'action'===field.prop">
              <el-button  v-auth="'user.modify'"  size="small" plain  type="warning" @click="onRowClick('edit',row)">编辑</el-button>
-             <el-button v-auth="'user.delete'"   size="small" plain  type="danger" @click="onRowDel(row)">删除</el-button>
+<!--             <el-button v-auth="'user.delete'"   size="small" plain  type="danger" @click="onRowDel(row)">删除</el-button>-->
             </template>
             <template v-else>
               <div>{{row[field.prop]}}</div>

+ 53 - 46
admin-web/vite.config.ts

@@ -1,59 +1,66 @@
 import vue from '@vitejs/plugin-vue';
-import { resolve } from 'path';
-import { defineConfig, loadEnv, ConfigEnv } from 'vite';
+import {resolve} from 'path';
+import {defineConfig, loadEnv, ConfigEnv} from 'vite';
 // import vueSetupExtend from 'vite-plugin-vue-setup-extend';
 
 const pathResolve = (dir: string) => {
-	return resolve(__dirname, '.', dir);
+    return resolve(__dirname, '.', dir);
 };
 
 const alias: Record<string, string> = {
-	'/@': pathResolve('./src/'),
-	'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
+    '/@': pathResolve('./src/'),
+    'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js',
 };
 
 const viteConfig = defineConfig((mode: ConfigEnv) => {
-	const env = loadEnv(mode.mode, process.cwd());
-	return {
-		// plugins: [vue(), vueSetupExtend()],
-		plugins: [vue()],
-		root: process.cwd(),
-		resolve: { alias },
-		base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,
-		optimizeDeps: {
-			include: ['element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/en', 'element-plus/es/locale/lang/zh-tw'],
-		},
-		server: {
-			host: '0.0.0.0',
-			port: env.VITE_PORT as unknown as number,
-			open: JSON.parse(env.VITE_OPEN),
-			hmr: true,
-		},
-		build: {
-			outDir: '../admin/src/main/resources/static',
-			chunkSizeWarningLimit: 1500,
-			rollupOptions: {
-				output: {
-					entryFileNames: `assets/[name].[hash].js`,
-					chunkFileNames: `assets/[name].[hash].js`,
-					assetFileNames: `assets/[name].[hash].[ext]`,
-					compact: true,
-					manualChunks: {
-						vue: ['vue', 'vue-router', 'pinia'],
-						echarts: ['echarts'],
-					},
-				},
-			},
-		},
-		css: { preprocessorOptions: { css: { charset: false } } },
-		define: {
-			__VUE_I18N_LEGACY_API__: JSON.stringify(false),
-			__VUE_I18N_FULL_INSTALL__: JSON.stringify(false),
-			__INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),
-			__NEXT_VERSION__: JSON.stringify(process.env.npm_package_version),
-			__NEXT_NAME__: JSON.stringify(process.env.npm_package_name),
-		},
-	};
+    const env = loadEnv(mode.mode, process.cwd());
+    return {
+        // plugins: [vue(), vueSetupExtend()],
+        plugins: [vue()],
+        root: process.cwd(),
+        resolve: {alias},
+        base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,
+        optimizeDeps: {
+            include: ['element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/en', 'element-plus/es/locale/lang/zh-tw'],
+        },
+        server: {
+            host: '0.0.0.0',
+            port: env.VITE_PORT as unknown as number,
+            open: JSON.parse(env.VITE_OPEN),
+            hmr: true,
+     /*       proxy: {
+                '/admin': {
+                    target: 'https://www.kuaiyuman.cn/admin/',
+                    changeOrigin: true,
+                    // rewrite: (path) => path.replace(/^\/admin/, '')
+                }
+            }*/
+        },
+        build: {
+            outDir: '../admin/src/main/resources/static',
+            chunkSizeWarningLimit: 1500,
+            rollupOptions: {
+                output: {
+                    entryFileNames: `assets/[name].[hash].js`,
+                    chunkFileNames: `assets/[name].[hash].js`,
+                    assetFileNames: `assets/[name].[hash].[ext]`,
+                    compact: true,
+                    manualChunks: {
+                        vue: ['vue', 'vue-router', 'pinia'],
+                        echarts: ['echarts'],
+                    },
+                },
+            },
+        },
+        css: {preprocessorOptions: {css: {charset: false}}},
+        define: {
+            __VUE_I18N_LEGACY_API__: JSON.stringify(false),
+            __VUE_I18N_FULL_INSTALL__: JSON.stringify(false),
+            __INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),
+            __NEXT_VERSION__: JSON.stringify(process.env.npm_package_version),
+            __NEXT_NAME__: JSON.stringify(process.env.npm_package_name),
+        },
+    };
 });
 
 export default viteConfig;

+ 1 - 2
admin/src/main/java/com/kym/admin/controller/AdminUserController.java

@@ -86,8 +86,7 @@ public class AdminUserController extends IController {
     @SaCheckPermission("user.add")
     @PostMapping("add")
     R<?> createAdminUser(@RequestBody AdminUserVo adminUserVo) {
-        adminUserService.createAdminUser(adminUserVo);
-        return R.success();
+        return resp(()->adminUserService.createAdminUser(adminUserVo));
     }
 
 

+ 9 - 0
admin/src/main/java/com/kym/admin/controller/DataDictController.java

@@ -12,6 +12,8 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 
 /**
  * <p>
@@ -59,4 +61,11 @@ public class DataDictController extends IController {
     }
 
 
+    @SysLog("查询字典保存更新接口")
+    @PostMapping("saveOrUpdate")
+    public R<?> saveOrUpdate(@RequestBody List<DataDict> dictList) {
+        return resp((t) -> dataDictService.saveOrUpdateBatch(dictList));
+    }
+
+
 }

+ 71 - 0
admin/src/main/java/com/kym/admin/controller/InvestorInfoController.java

@@ -0,0 +1,71 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.kym.common.R;
+import com.kym.entity.admin.InvestorInfo;
+import com.kym.entity.admin.queryParams.CommonQueryParam;
+import com.kym.service.admin.InvestorInfoService;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * <p>
+ * 投资者-物业信息表 前端控制器
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@RestController
+@RequestMapping("/investorInfo")
+public class InvestorInfoController {
+
+    private final InvestorInfoService investorInfoService;
+
+    public InvestorInfoController(InvestorInfoService investorInfoService) {
+        this.investorInfoService = investorInfoService;
+    }
+
+    /**
+     * 新增
+     *
+     * @param investorInfo
+     */
+    @SaCheckPermission("investor.add")
+    @PostMapping("/create")
+    R<?> create(@RequestBody InvestorInfo investorInfo) {
+        investorInfo.setId(null);
+        return R.success(investorInfoService.save(investorInfo));
+    }
+
+    /**
+     * 修改投资者/物业信息
+     * @param investorInfo
+     * @return
+     */
+    @PostMapping("/update")
+    R<?> update(@RequestBody InvestorInfo investorInfo) {
+        return R.success(investorInfoService.updateById(investorInfo));
+    }
+
+    /**
+     * 获取单个投资者/物业信息
+     *
+     * @param id
+     * @return
+     */
+    @GetMapping("/{id}")
+    R<?> getInvestorInfoById(@PathVariable("id") Long id) {
+        return R.success(investorInfoService.getById(id));
+    }
+
+    /**
+     * 投资者/物业信息列表
+     * @param params
+     * @return
+     */
+    @GetMapping("/list")
+    R<?> list(@ModelAttribute CommonQueryParam params) {
+        return R.success(investorInfoService.list(params));
+    }
+
+}

+ 49 - 16
admin/src/main/java/com/kym/admin/controller/StatController.java

@@ -1,12 +1,12 @@
 package com.kym.admin.controller;
 
 import com.kym.common.R;
+import com.kym.entity.admin.StationStatMonth;
 import com.kym.entity.admin.queryParams.StatQueryParam;
+import com.kym.entity.admin.queryParams.StatementsQueryParam;
+import com.kym.service.admin.StationStatMonthService;
 import com.kym.service.miniapp.ChargeOrderService;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * @author skyline
@@ -18,9 +18,11 @@ import org.springframework.web.bind.annotation.RestController;
 public class StatController {
 
     private final ChargeOrderService chargeOrderService;
+    private final StationStatMonthService stationStatMonthService;
 
-    public StatController(ChargeOrderService chargeOrderService) {
+    public StatController(ChargeOrderService chargeOrderService, StationStatMonthService stationStatMonthService) {
         this.chargeOrderService = chargeOrderService;
+        this.stationStatMonthService = stationStatMonthService;
     }
 
     /**
@@ -31,8 +33,7 @@ public class StatController {
      */
     @GetMapping("/stationStat")
     R<?> stationStat(@ModelAttribute StatQueryParam params) {
-        var res = chargeOrderService.stationStat(params);
-        return R.success(res);
+        return R.success(chargeOrderService.stationStat(params));
     }
 
     /**
@@ -42,20 +43,52 @@ public class StatController {
      */
     @GetMapping("/stationTodayStat")
     R<?> stationTodayStat(String stationId) {
-        var res = chargeOrderService.stationTodayStat(stationId);
-        return R.success(res);
+        return R.success(chargeOrderService.stationTodayStat(stationId));
     }
 
-    // TODO: 2023-12-01 站点平均单桩充电数据 日/月/整体维度
+    /**
+     * 站点详细数据统计
+     *
+     * @param params
+     * @return
+     */
+    @GetMapping("/stationStatDetail")
+    R<?> stationStatDetail(@ModelAttribute StatQueryParam params) {
+        return R.success(chargeOrderService.stationStatDetail(params));
+    }
 
-    // TODO: 2023-12-01 站点平均单度电费数据 日/月/整体维度
+    /**
+     * 站点月统计数据列表
+     *
+     * @param params
+     * @return
+     */
+    @GetMapping("/listStatMonth")
+    R<?> listStatMonth(@ModelAttribute StatementsQueryParam params) {
+        return R.success(stationStatMonthService.listStatMonth(params));
+    }
 
-    // 站点详细数据统计
-    @GetMapping("/stationStatDetail")
-    R<?> stationStatDetail(@ModelAttribute StatQueryParam params){
-        var res = chargeOrderService.stationStatDetail(params);
-        return R.success(res);
+    /**
+     * 单条站点月统计数据
+     *
+     * @param statMonthId
+     * @return
+     */
+    @GetMapping("/getStationStatMonth/{statMonthId}")
+    R<?> getStationStatMonth(@PathVariable("statMonthId") String statMonthId) {
+        return R.success(stationStatMonthService.getById(statMonthId));
     }
 
+    /**
+     * 修改站点月统计数据(填写电费账单数据)
+     *
+     * @param stationStatMonth
+     * @return
+     */
+    @PostMapping("/modifyStationStatMonth")
+    R<?> modifyStationStatMonth(@RequestBody StationStatMonth stationStatMonth) {
+        stationStatMonthService.modifyStationStatMonth(stationStatMonth);
+        return R.success();
+    }
 
 }

+ 61 - 0
admin/src/main/java/com/kym/admin/controller/StatementsController.java

@@ -0,0 +1,61 @@
+package com.kym.admin.controller;
+
+import com.kym.common.R;
+import com.kym.entity.admin.queryParams.StatementsQueryParam;
+import com.kym.service.admin.StatementsService;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * <p>
+ * 客户对账单 前端控制器
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@RestController
+@RequestMapping("/statements")
+public class StatementsController {
+
+    private final StatementsService statementsService;
+
+    public StatementsController(StatementsService statementsService) {
+        this.statementsService = statementsService;
+    }
+
+    /**
+     * 生成对账单(创建前先录入抄表电量和电费金额)
+     * 一次性生成对应站点投资者和物业的对账单
+     *
+     * @param statMonthId 月统计记录id
+     * @return
+     */
+    @GetMapping("/create/{statMonthId}")
+    R<?> create(@PathVariable("statMonthId") String statMonthId) {
+        statementsService.createStatements(statMonthId);
+        return R.success();
+    }
+
+    /**
+     * 预览
+     *
+     * @param statId
+     * @return
+     */
+    @GetMapping("/preview/{statId}")
+    R<?> preview(@PathVariable("statId") String statId) {
+        return R.success(statementsService.preview(statId));
+    }
+
+    /**
+     * 对账单列表
+     *
+     * @param params
+     * @return
+     */
+    @GetMapping("/listStatements")
+    R<?> list(@ModelAttribute StatementsQueryParam params) {
+        return R.success(statementsService.listStatements(params));
+    }
+
+}

+ 11 - 0
admin/src/main/java/com/kym/admin/controller/StationController.java

@@ -4,11 +4,14 @@ import cn.dev33.satoken.stp.StpUtil;
 import com.kym.common.R;
 import com.kym.common.annotation.SysLog;
 import com.kym.entity.admin.Station;
+import com.kym.entity.admin.vo.ConnectorVo;
 import com.kym.service.admin.StationService;
 import com.kym.service.cache.KymCache;
 import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 import static com.baomidou.mybatisplus.core.toolkit.ObjectUtils.isNotNull;
 
 /**
@@ -71,4 +74,12 @@ public class StationController {
         return R.success();
     }
 
+    // 新增站点
+    @SysLog("初始化新站点信息")
+    @PostMapping("/addStation")
+    R<?> addStation(@RequestBody List<ConnectorVo> stationInfo) {
+        stationService.addStation(stationInfo);
+        return R.success();
+    }
+
 }

+ 45 - 4
admin/src/main/java/com/kym/admin/jobs/StationStatJob.java

@@ -2,6 +2,7 @@ package com.kym.admin.jobs;
 
 import cn.hutool.core.date.DateUtil;
 import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import com.kym.common.utils.CommUtil;
 import com.kym.entity.admin.ConnectorInfo;
 import com.kym.entity.admin.StationStatDay;
 import com.kym.entity.admin.StationStatMonth;
@@ -10,6 +11,7 @@ import com.kym.service.admin.ConnectorInfoService;
 import com.kym.service.admin.StationStatDayService;
 import com.kym.service.admin.StationStatMonthService;
 import com.kym.service.miniapp.ChargeOrderService;
+import jakarta.annotation.PostConstruct;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
@@ -17,12 +19,15 @@ import org.springframework.stereotype.Component;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
 import java.time.temporal.TemporalAdjusters;
 import java.util.*;
 import java.util.stream.Collectors;
 
 /**
  * 站点统计定时任务
+ *
+ * @author skyline
  */
 @Component
 @Slf4j
@@ -46,9 +51,10 @@ public class StationStatJob {
     // 每天14:30执行一次,通过charge_app.t_charge_order表统计start_time为前一天且充电金额大于100的充电订单,分别统计充电人数、订单数、充电总费用、充电总电量、平均充电电量、平均充电费用、平均订单费用、平均订单电量
 
     /**
-     * 日统计,每月第一天下午14:30启动,统计上日数据
+     * 日统计,每下午14:30启动,统计上日数据
      */
     @Scheduled(cron = "0 30 14 * * ?")
+    // 定时每日下午14:30
     private void dayStat() {
         log.info("执行站点日统计定时任务-开始");
         var statDay = LocalDateTime.now().minusDays(1);
@@ -74,20 +80,43 @@ public class StationStatJob {
         log.info("执行站点月统计定时任务-结束");
     }
 
+    // 只执行一次
+//    @PostConstruct
+    private void init() {
+        log.info("执行站点初始化定时任务-开始");
+        // 2023-08-01 00:00:00
+//        var statMonth = LocalDateTime.of(2023, 12, 1, 0, 0);
+//        var endMonth = LocalDateTime.of(2023, 12, 1, 0, 0);
+//        var startTime = statMonth.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
+//        var endTime = endMonth.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
+//        var chargeOrderList = getChargeOrders(startTime, endTime);
+//        monthService.saveBatch((Collection<StationStatMonth>) getStationStat(chargeOrderList, false));
+//
+//        // 将订单按照日为单位分组
+//        var chargeOrderMap = chargeOrderList.stream().collect(Collectors.groupingBy(order ->
+//                order.getStartTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
+//        ));
+//        // chargeOrderMap按照key升序排序
+//        var chargeOrderMapSort = chargeOrderMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(
+//                Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
+//
+//        chargeOrderMapSort.forEach((k, v) -> dayService.saveBatch(((Collection<StationStatDay>) getStationStat(v, true))));
+
+        log.info("执行站点初始化定时任务-结束");
+    }
 
     private List<ChargeOrder> getChargeOrders(LocalDateTime startTime, LocalDateTime endTime) {
-        // 通过charge_app.t_charge_order表统计start_time为前一天且充电金额大于100的有效充电订单
+        // 通过charge_app.t_charge_order表统计start_time为前一天且充电金额大于0的有效充电订单
         DynamicDataSourceContextHolder.push("db-miniapp");
         var res = chargeOrderService.lambdaQuery()
                 .ge(ChargeOrder::getStartTime, startTime)
                 .le(ChargeOrder::getStartTime, endTime)
-                .gt(ChargeOrder::getTotalMoney, 100)
+                .gt(ChargeOrder::getTotalMoney, 0)
                 .list();
         DynamicDataSourceContextHolder.poll();
         return res;
     }
 
-
     private Collection<?> getStationStat(List<ChargeOrder> chargeOrderList, boolean isDayStat) {
         // 统计每个站点的connector_id数量
         var stationConnectorCountMap = connectorInfoService.list().stream().collect(Collectors.groupingBy(ConnectorInfo::getStationId, Collectors.counting()));
@@ -103,9 +132,14 @@ public class StationStatJob {
                     var totalMoney = chargeOrders.stream().mapToInt(ChargeOrder::getTotalMoney).sum();
                     var elecMoney = chargeOrders.stream().mapToInt(ChargeOrder::getElecMoney).sum();
                     var serviceMoney = chargeOrders.stream().mapToInt(ChargeOrder::getServiceMoney).sum();
+                    var serviceMoneyDiscount = chargeOrders.stream().mapToInt(ChargeOrder::getServiceMoneyDiscount).sum();
+                    var discountAmount = chargeOrders.stream().mapToInt(ChargeOrder::getDiscountAmount).sum();
                     var totalPower = chargeOrders.stream().mapToDouble(ChargeOrder::getTotalPower).sum();
                     var avgPower = chargeOrders.stream().mapToDouble(ChargeOrder::getTotalPower).average().orElse(0.0);
                     var avgOrderMoney = chargeOrders.stream().mapToInt(ChargeOrder::getTotalMoney).average().orElse(0);
+                    // 充电桩使用率 = 有订单的充电桩数量 / 总充电桩数
+                    var connectorUsageRate = (double) chargeOrders.stream().filter(CommUtil.distinctByKey(ChargeOrder::getConnectorId)).count() / stationConnectorCountMap.get(entry.getKey());
+                    ;
                     return isDayStat
                             ? new StationStatDay()
                             .setStationId(entry.getKey())
@@ -115,10 +149,15 @@ public class StationStatJob {
                             .setTotalMoney(totalMoney)
                             .setElecMoney(elecMoney)
                             .setServiceMoney(serviceMoney)
+                            .setServiceMoneyDiscount(serviceMoneyDiscount)
+                            .setDiscountAmount(discountAmount)
+
                             .setTotalPower(totalPower)
                             .setAvgOrderElec(avgPower)
                             .setAvgOrderMoney((int) avgOrderMoney)
                             .setAvgConnectorElec(totalPower / (stationConnectorCountMap.get(entry.getKey())))
+                            .setConnectorUsageRate(connectorUsageRate)
+
                             : new StationStatMonth()
                             .setStationId(entry.getKey())
                             .setStatMonth(DateUtil.format(LocalDateTime.now().minusMonths(1), "yyyy-MM"))
@@ -127,6 +166,8 @@ public class StationStatJob {
                             .setTotalMoney(totalMoney)
                             .setElecMoney(elecMoney)
                             .setServiceMoney(serviceMoney)
+                            .setServiceMoneyDiscount(serviceMoneyDiscount)
+                            .setDiscountAmount(discountAmount)
                             .setTotalPower(totalPower)
                             .setAvgOrderElec(avgPower)
                             .setAvgOrderMoney((int) avgOrderMoney)

+ 0 - 1551
admin/src/main/resources/template/statement.html

@@ -1,1551 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-    <meta name="generator" content="PhpSpreadsheet, https://github.com/PHPOffice/PhpSpreadsheet">
-    <title>Untitled Spreadsheet</title>
-    <meta name="title" content="Untitled Spreadsheet"/>
-    <meta name="company" content="Microsoft Corporation"/>
-    <style type="text/css">
-        html {
-            font-family: Calibri, Arial, Helvetica, sans-serif;
-            font-size: 11pt;
-            background-color: white
-        }
-
-        a.comment-indicator:hover + div.comment {
-            background: #ffd;
-            position: absolute;
-            display: block;
-            border: 1px solid black;
-            padding: 0.5em
-        }
-
-        a.comment-indicator {
-            background: red;
-            display: inline-block;
-            border: 1px solid black;
-            width: 0.5em;
-            height: 0.5em
-        }
-
-        div.comment {
-            display: none
-        }
-
-        table {
-            border-collapse: collapse;
-            page-break-after: always
-        }
-
-        .gridlines td {
-            border: 1px dotted black
-        }
-
-        .gridlines th {
-            border: 1px dotted black
-        }
-
-        .b {
-            text-align: center
-        }
-
-        .e {
-            text-align: center
-        }
-
-        .f {
-            text-align: right
-        }
-
-        .inlineStr {
-            text-align: left
-        }
-
-        .n {
-            text-align: right
-        }
-
-        .s {
-            text-align: left
-        }
-
-        td.style0 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style0 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style1 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style1 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style2 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style2 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style3 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style3 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style4 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            font-weight: bold;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 16pt;
-            background-color: #DDEBF7
-        }
-
-        th.style4 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            font-weight: bold;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 16pt;
-            background-color: #DDEBF7
-        }
-
-        td.style5 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            font-weight: bold;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 16pt;
-            background-color: #DDEBF7
-        }
-
-        th.style5 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            font-weight: bold;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 16pt;
-            background-color: #DDEBF7
-        }
-
-        td.style6 {
-            vertical-align: middle;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style6 {
-            vertical-align: middle;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style7 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style7 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style8 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style8 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style9 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        th.style9 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        td.style10 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        th.style10 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        td.style11 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style11 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style12 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: 'Microsoft YaHei';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style12 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: 'Microsoft YaHei';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style13 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        th.style13 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        td.style14 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style14 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style15 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style15 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style16 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        th.style16 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        td.style17 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        th.style17 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        td.style18 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style18 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style19 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style19 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style20 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: 'Microsoft YaHei';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style20 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: 'Microsoft YaHei';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style21 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style21 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: none #000000;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style22 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: 'Microsoft YaHei';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style22 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: 'Microsoft YaHei';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style23 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style23 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style24 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style24 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style25 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style25 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style26 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style26 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style27 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style27 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style28 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style28 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style29 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: 1px solid #000000 !important;
-            border-top: none #000000;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        th.style29 {
-            vertical-align: middle;
-            text-align: left;
-            padding-left: 0px;
-            border-bottom: 1px solid #000000 !important;
-            border-top: none #000000;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #DDEBF7
-        }
-
-        td.style30 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style30 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style31 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style31 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style32 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style32 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style33 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style33 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style34 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style34 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style35 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style35 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style36 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style36 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style37 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style37 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style38 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style38 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style39 {
-            vertical-align: middle;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style39 {
-            vertical-align: middle;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style40 {
-            vertical-align: middle;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style40 {
-            vertical-align: middle;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: 1px solid #000000 !important;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style41 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style41 {
-            vertical-align: middle;
-            border-bottom: none #000000;
-            border-top: none #000000;
-            border-left: none #000000;
-            border-right: none #000000;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style42 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            font-weight: bold;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 16pt;
-            background-color: #DDEBF7
-        }
-
-        th.style42 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            font-weight: bold;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 16pt;
-            background-color: #DDEBF7
-        }
-
-        td.style43 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        th.style43 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: white
-        }
-
-        td.style44 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        th.style44 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        td.style45 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        th.style45 {
-            vertical-align: middle;
-            text-align: center;
-            border-bottom: 1px solid #000000 !important;
-            border-top: 1px solid #000000 !important;
-            border-left: none #000000;
-            border-right: 1px solid #000000 !important;
-            color: #000000;
-            font-family: '微软雅黑';
-            font-size: 12pt;
-            background-color: #FFFFFF
-        }
-
-        table.sheet0 col.col0 {
-            width: 80.65555463pt
-        }
-
-        table.sheet0 col.col1 {
-            width: 117.25555421pt
-        }
-
-        table.sheet0 col.col2 {
-            width: 100.31110996pt
-        }
-
-        table.sheet0 col.col3 {
-            width: 82.68888794pt
-        }
-
-        table.sheet0 col.col4 {
-            width: 96.24444334pt
-        }
-
-        table.sheet0 col.col5 {
-            width: 122.67777637pt
-        }
-
-        table.sheet0 col.col6 {
-            width: 94.21111003pt
-        }
-
-        table.sheet0 col.col7 {
-            width: 80.65555463pt
-        }
-
-        table.sheet0 col.col8 {
-            width: 80.65555463pt
-        }
-
-        table.sheet0 col.col9 {
-            width: 42pt
-        }
-
-        table.sheet0 col.col10 {
-            width: 42pt
-        }
-
-        table.sheet0 col.col11 {
-            width: 48.79999944pt
-        }
-
-        table.sheet0 tr {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row0 {
-            height: 40pt
-        }
-
-        table.sheet0 tr.row1 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row2 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row3 {
-            height: 30pt
-        }
-
-        table.sheet0 tr.row4 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row5 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row6 {
-            height: 30pt
-        }
-
-        table.sheet0 tr.row7 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row8 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row9 {
-            height: 30pt
-        }
-
-        table.sheet0 tr.row10 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row11 {
-            height: 35pt
-        }
-
-        table.sheet0 tr.row12 {
-            height: 30pt
-        }
-
-        table.sheet0 tr.row13 {
-            height: 123pt
-        }
-
-        table.sheet0 tr.row14 {
-            height: 22pt
-        }
-
-        table.sheet0 tr.row15 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row16 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row17 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row18 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row19 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row20 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row21 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row22 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row23 {
-            height: 14.25pt
-        }
-
-        table.sheet0 tr.row24 {
-            height: 14.25pt
-        }
-
-        @page {
-            margin-left: 0.75in;
-            margin-right: 0.75in;
-            margin-top: 1in;
-            margin-bottom: 1in;
-        }
-
-        body {
-            margin-left: 0.75in;
-            margin-right: 0.75in;
-            margin-top: 1in;
-            margin-bottom: 1in;
-        }
-    </style>
-</head>
-
-<body>
-
-<table border="0" cellpadding="0" cellspacing="0" id="sheet0" class="sheet0 gridlines">
-    <col class="col0">
-    <col class="col1">
-    <col class="col2">
-    <col class="col3">
-    <col class="col4">
-    <col class="col5">
-    <col class="col6">
-    <col class="col7">
-    <col class="col8">
-    <col class="col9">
-    <col class="col10">
-    <col class="col11">
-    <tbody>
-    <tr class="row0">
-        <td class="column0 style4 s style42" colspan="9">深圳市快与慢充电桩服务有限公司—阳光科创中心对账单</td>
-    </tr>
-    <tr class="row1">
-        <td class="column0 style6 s">客户名称</td>
-        <td class="column1 style7 s style43" colspan="8">邱XX</td>
-    </tr>
-    <tr class="row2">
-        <td class="column0 style6 s">所属期间</td>
-        <td class="column1 style7 s style43" colspan="8">2023年8月1日—2023年8月31日</td>
-    </tr>
-    <tr class="row3">
-        <td class="column0 style9 s style10" colspan="9">
-            一、APP后台数据(含税,订单总金额为电费金额,实际电费为快与慢代收代付)(电量单位为“度”,电费和服务费以及金额单位为“元”)
-        </td>
-    </tr>
-    <tr class="row4">
-        <td class="column0 style11 s">站点名称</td>
-        <td class="column1 style11 s">充电电量</td>
-        <td class="column2 style12 s">①<span
-                style="color:#000000; font-family:'微软雅黑'; font-size:12pt">订单电费</span>
-        </td>
-        <td class="column3 style11 s">电表电量</td>
-        <td class="column4 style12 s">②<span
-                style="color:#000000; font-family:'微软雅黑'; font-size:12pt">电表电费</span>
-        </td>
-        <td class="column5 style11 s">电损金额=②-①</td>
-        <td class="column6 style11 s">对账服务费</td>
-        <td class="column7 style13 s style44" colspan="2">订单总金额</td>
-    </tr>
-    <tr class="row5">
-        <td class="column0 style11 s">阳光科创中心</td>
-        <td class="column1 style14 n">10441.30</td>
-        <td class="column2 style15 n">6874.05</td>
-        <td class="column3 style14 n">10800.00</td>
-        <td class="column4 style14 n">7182.57</td>
-        <td class="column5 style15 f">308.52</td>
-        <td class="column6 style15 n">8331.78</td>
-        <td class="column7 style16 f style45" colspan="2">15205.83</td>
-    </tr>
-    <tr class="row6">
-        <td class="column0 style9 s style17" colspan="9">二、客户结算数据(含税)(电费和服务费以及金额单位为“元”)</td>
-    </tr>
-    <tr class="row7">
-        <td class="column0 style18 s">站点名称</td>
-        <td class="column1 style18 s">①对账服务费</td>
-        <td class="column2 style19 s">②电损金额</td>
-        <td class="column3 style20 s style21" colspan="2">③<span
-                style="color:#000000; font-family:'微软雅黑'; font-size:12pt">实际服务费收入=</span><span
-                style="color:#000000; font-family:'Microsoft YaHei'; font-size:12pt">①</span><span
-                style="color:#000000; font-family:'微软雅黑'; font-size:12pt">-</span><span
-                style="color:#000000; font-family:'Microsoft YaHei'; font-size:12pt">②</span></td>
-        <td class="column5 style22 s">④<span
-                style="color:#000000; font-family:'微软雅黑'; font-size:12pt">分成比例</span>
-        </td>
-        <td class="column6 style19 s style19" colspan="3">分成金额=<span
-                style="color:#000000; font-family:'Microsoft YaHei'; font-size:12pt">③</span><span
-                style="color:#000000; font-family:'微软雅黑'; font-size:12pt">*</span><span
-                style="color:#000000; font-family:'Microsoft YaHei'; font-size:12pt">④</span></td>
-    </tr>
-    <tr class="row8">
-        <td class="column0 style23 s">阳光科创中心</td>
-        <td class="column1 style24 f">8331.78</td>
-        <td class="column2 style15 f">308.52</td>
-        <td class="column3 style25 f style26" colspan="2">8023.26</td>
-        <td class="column5 style27 n">45%</td>
-        <td class="column6 style28 f style28" colspan="3">3610.47</td>
-    </tr>
-    <tr class="row9">
-        <td class="column0 style9 s style29" colspan="9">三、客户结算数据(不含税)(税额和金额单位为“元”)</td>
-    </tr>
-    <tr class="row10">
-        <td class="column0 style23 s style30" colspan="2">①分成金额</td>
-        <td class="column2 style23 s style31" colspan="2">增值税率</td>
-        <td class="column4 style23 s style31" colspan="2">②应纳增值税额</td>
-        <td class="column6 style23 s style31" colspan="3">应付金额=①-②</td>
-    </tr>
-    <tr class="row11">
-        <td class="column0 style33 f style34" colspan="2">3610.47</td>
-        <td class="column2 style35 n style31" colspan="2">6%</td>
-        <td class="column4 style36 f style37" colspan="2">228.89</td>
-        <td class="column6 style36 f style37" colspan="3">3381.58</td>
-    </tr>
-    <tr class="row12">
-        <td class="column0 style9 s style10" colspan="9">备注:</td>
-    </tr>
-    <tr class="row13">
-        <td class="column0 style39 s style40" colspan="9">分成金额=实际服务费收入*分成比例<br/>
-            <br/>
-            <span style="color:#000000; font-family:'微软雅黑'; font-size:12pt">注意:乙方通过运营平台收取扣除相应费用以后的“实际收取的充电服务费收益”*甲方分成比例,甲方收益分成比例为45%。<br/>
-</span><span style="color:#000000; font-family:'微软雅黑'; font-size:12pt"><br/>
-结算账户:邱XX                        联系电话:15519381000<br/>
-<br/>
-开户行:建设银行应城支行              银行账号:6217002720009460000</span></td>
-    </tr>
-    </tbody>
-</table>
-</body>
-</html>

+ 2 - 0
common/src/main/java/com/kym/common/annotation/DynamicCache.java

@@ -11,6 +11,8 @@ import java.lang.annotation.*;
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface DynamicCache {
+    String spel() default "";
+
     String value() default "";
 
     String key() default "";

+ 37 - 1
common/src/main/java/com/kym/common/aspect/DynamicCacheAspect.java

@@ -8,9 +8,15 @@ import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
 import org.springframework.stereotype.Component;
 
 import java.lang.reflect.Method;
+import java.util.Objects;
 
 /**
  * @author skyline
@@ -21,6 +27,8 @@ import java.lang.reflect.Method;
 @Aspect
 public class DynamicCacheAspect {
     private DynamicDataCache cache = DynamicDataCache.INSTANCE;
+    private SpelExpressionParser parserSpel = new SpelExpressionParser();
+    private DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
 
     @Pointcut("@annotation(com.kym.common.annotation.DynamicCache)")
     public void cachePointCut() {
@@ -31,12 +39,16 @@ public class DynamicCacheAspect {
         MethodSignature signature = (MethodSignature) joinPoint.getSignature();
         Method method = signature.getMethod();
         DynamicCache dynamicCache = method.getAnnotation(DynamicCache.class);
+
+        //获取运行时参数的值
+        String spelParamValue = generateKeyBySpEL(dynamicCache.spel(), joinPoint);
+
         // 获取方法名
         var methodName = signature.getName();
         // 获取注解上的缓存时间
         var timeout = dynamicCache.timeout();
         // 获取注解上的缓存key
-        var key = dynamicCache.key();
+        var key = !CommUtil.isEmptyOrNull(spelParamValue) ? methodName+":"+spelParamValue : dynamicCache.key();
         var data = cache.get(CommUtil.isEmptyOrNull(key) ? methodName : key);
         if (data != null) {
             return data;
@@ -47,4 +59,28 @@ public class DynamicCacheAspect {
         cache.put(CommUtil.isEmptyOrNull(key) ? methodName : key, result, timeout >= 0 ? timeout : 5 * 60 * 1000);
         return result;
     }
+
+    /**
+     * 生成缓存key
+     *
+     * @param key
+     * @param pjp
+     * @return
+     */
+    public String generateKeyBySpEL(String key, ProceedingJoinPoint pjp) {
+        if (CommUtil.isEmptyOrNull(key)) {
+            return null;
+        }
+        Expression expression = parserSpel.parseExpression(key);
+        EvaluationContext context = new StandardEvaluationContext();
+        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
+        Object[] args = pjp.getArgs();
+        String[] paramNames = parameterNameDiscoverer.getParameterNames(methodSignature.getMethod());
+        for (int i = 0; i < args.length; i++) {
+            assert paramNames != null;
+            context.setVariable(paramNames[i], args[i]);
+        }
+        return Objects.requireNonNull(expression.getValue(context)).toString();
+    }
+
 }

+ 17 - 2
common/src/main/java/com/kym/common/utils/CommUtil.java

@@ -10,6 +10,9 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Predicate;
 
 
 /**
@@ -226,7 +229,7 @@ public class CommUtil {
     }
 
     public static String null2String(Object val) {
-        if(isEmptyOrNull(val)){
+        if (isEmptyOrNull(val)) {
             return "";
         }
         return val.toString().trim();
@@ -238,10 +241,22 @@ public class CommUtil {
         for (char ch : chars) {
             if (Character.isUpperCase(ch)) {
                 sbr.append("_").append(Character.toLowerCase(ch));
-            }else{
+            } else {
                 sbr.append(ch);
             }
         }
         return sbr.toString();
     }
+
+    /**
+     * 根据指定字段去重
+     *
+     * @param keyExtractor
+     * @param <T>
+     * @return
+     */
+    public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
+        Map<Object, Boolean> seen = new ConcurrentHashMap<>();
+        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
+    }
 }

+ 1 - 0
database/3-rollback.sql

@@ -0,0 +1 @@
+delete from  charge_admin.t_permission where  `value`='kanban.list' or `value` ='data';

+ 3 - 0
database/3.sql

@@ -0,0 +1,3 @@
+insert into charge_admin.t_permission (id, pid, name, value, weight, create_time, update_time)
+values
+    (57, 0, '数据', 'data', 2, null, null),(58, 57, '看板数据', 'kanban.list', 2, null, null);

+ 92 - 0
entity/src/main/java/com/kym/entity/admin/InvestorInfo.java

@@ -0,0 +1,92 @@
+package com.kym.entity.admin;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.kym.entity.BaseEntity;
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * <p>
+ * 投资者-物业信息表
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@Getter
+@Setter
+@TableName("t_investor_info")
+public class InvestorInfo extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 客户用户id
+     */
+    private Long adminUserId;
+
+    /**
+     * 客户姓名
+     */
+    private String adminUserName;
+
+    /**
+     * 站点id
+     */
+    private String stationId;
+
+    /**
+     * 站点
+     */
+    private String stationName;
+
+    /**
+     * 增值税率 0.06表示6%
+     */
+    private Double vatRate;
+
+    /**
+     * 分成比例 0.45表示45%
+     */
+    private Double splittingProportion;
+
+    /**
+     * 分成比例 0.30表示30%
+     */
+    private Double elecLossProportion;
+
+    /**
+     * 账户名
+     */
+    private String accountName;
+
+    /**
+     * 电话号码
+     */
+    private String telephone;
+
+    /**
+     * 开户行名称
+     */
+    private String bankName;
+
+    /**
+     * 银行卡号
+     */
+    private String bankCardNo;
+
+    /**
+     * 状态:0-无效,1-有效
+     */
+    private Byte status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 纳税人识别号
+     */
+    private String taxNo;
+}

+ 149 - 0
entity/src/main/java/com/kym/entity/admin/Statements.java

@@ -0,0 +1,149 @@
+package com.kym.entity.admin;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.kym.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+/**
+ * <p>
+ * 客户对账单
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@Getter
+@Setter
+@TableName("t_statements")
+@Accessors(chain = true)
+public class Statements extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 客户用户id
+     */
+    private Long adminUserId;
+
+    /**
+     * 客户姓名
+     */
+    private String adminUserName;
+
+    /**
+     * 站点id
+     */
+    private String stationId;
+
+    /**
+     * 站点名称
+     */
+    private String stationName;
+
+    /**
+     * 统计时间(月)
+     */
+    private String statMonth;
+
+    /**
+     * 订单电量
+     */
+    private Double totalPower;
+
+    /**
+     * 实际抄表电量
+     */
+    private Double actualPower;
+
+    /**
+     * 电损电量
+     */
+    private Double elecLossPower;
+
+    /**
+     * 订单金额(分)
+     */
+    private Integer totalMoney;
+
+    /**
+     * 订单电费金额(分)
+     */
+    private Integer elecMoney;
+
+    /**
+     * 实际抄表电费金额(分)
+     */
+    private Integer actualElecMoney;
+
+    /**
+     * 电损电费金额(分)
+     */
+    private Integer elecLossMoney;
+
+    /**
+     * 服务费金额(分)
+     */
+    private Integer serviceMoney;
+
+    /**
+     * 优惠金额(分)
+     */
+    private Integer discountAmount;
+
+    /**
+     * 服务费优惠金额(分)
+     */
+    private Integer serviceMoneyDiscount;
+
+    /**
+     * 实际参与分成的服务费(分)
+     */
+    private Integer actualServiceMoney;
+
+    /**
+     * 分成比例 0.45表示45%
+     */
+    private Double splittingProportion;
+
+    /**
+     * 分成金额(分)
+     */
+    private Integer splittingAmount;
+
+    /**
+     * 电损承担比例 0.30代表30%
+     */
+    private Double elecLossProportion;
+
+    /**
+     * 电损承担金额(分)
+     */
+    private Integer elecLossAmount;
+
+    /**
+     * 增值税率 0.06表示6%
+     */
+    private Double vatRate;
+
+    /**
+     * 增值税额(分)
+     */
+    private Integer vatAmount;
+
+    /**
+     * 实际分成金额
+     */
+    private Integer actualSplittingAmount;
+
+    /**
+     * 状态:0-无效,1-有效
+     */
+    private Byte status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 15 - 2
entity/src/main/java/com/kym/entity/admin/StationStatDay.java

@@ -2,8 +2,6 @@ package com.kym.entity.admin;
 
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.kym.entity.BaseEntity;
-import java.io.Serializable;
-import java.time.LocalDate;
 import lombok.Getter;
 import lombok.Setter;
 import lombok.experimental.Accessors;
@@ -64,6 +62,16 @@ public class StationStatDay extends BaseEntity {
      */
     private Integer serviceMoney;
 
+    /**
+     * 服务费优惠金额
+     */
+    private Integer serviceMoneyDiscount;
+
+    /**
+     * 总优惠金额
+     */
+    private Integer discountAmount;
+
     /**
      * 订单平均充电量
      */
@@ -78,4 +86,9 @@ public class StationStatDay extends BaseEntity {
      * 单枪平均日充电量
      */
     private Double avgConnectorElec;
+
+    /**
+     * 设备使用率
+     */
+    private Double connectorUsageRate;
 }

+ 32 - 2
entity/src/main/java/com/kym/entity/admin/StationStatMonth.java

@@ -1,13 +1,12 @@
 package com.kym.entity.admin;
 
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.kym.entity.BaseEntity;
 import lombok.Getter;
 import lombok.Setter;
 import lombok.experimental.Accessors;
 
-import java.time.LocalDate;
-
 /**
  * <p>
  * 站点统计表-月
@@ -29,6 +28,12 @@ public class StationStatMonth extends BaseEntity {
      */
     private String stationId;
 
+    /**
+     * 站点名称
+     */
+    @TableField(exist = false)
+    private String stationName;
+
     /**
      * 统计时间
      */
@@ -64,6 +69,16 @@ public class StationStatMonth extends BaseEntity {
      */
     private Integer serviceMoney;
 
+    /**
+     * 服务费优惠金额
+     */
+    private Integer serviceMoneyDiscount;
+
+    /**
+     * 总优惠金额
+     */
+    private Integer discountAmount;
+
     /**
      * 订单平均充电量
      */
@@ -78,4 +93,19 @@ public class StationStatMonth extends BaseEntity {
      * 单枪平均日充电量
      */
     private Double avgConnectorElec;
+
+    /**
+     * 设备使用率
+     */
+    private Double connectorUsageRate;
+
+    /**
+     * 实际抄表电量
+     */
+    private Double actualPower;
+
+    /**
+     * 实际抄表电费金额(分)
+     */
+    private Integer actualElecMoney;
 }

+ 29 - 0
entity/src/main/java/com/kym/entity/admin/queryParams/StatementsQueryParam.java

@@ -0,0 +1,29 @@
+package com.kym.entity.admin.queryParams;
+
+import com.kym.entity.common.PageParams;
+import lombok.Data;
+
+/**
+ * @author skyline
+ * @description 查询参数
+ * @date 2023-08-22 18:56
+ */
+@Data
+public class StatementsQueryParam extends PageParams {
+
+    /**
+     * 站点id
+     */
+    private String stationId;
+
+    /**
+     * 统计月份
+     */
+    private String statMonth;
+
+    /**
+     * 投资者/物业名称
+     */
+    private String adminUserName;
+
+}

+ 50 - 0
entity/src/main/java/com/kym/entity/admin/vo/ConnectorVo.java

@@ -0,0 +1,50 @@
+package com.kym.entity.admin.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * 充电设备信息
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+public class ConnectorVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 充电站id
+     */
+    private String stationId;
+
+    /**
+     * 充电站名称
+     */
+    private String stationName;
+
+    /**
+     * 充电设备id
+     */
+    private String equipmentId;
+
+    /**
+     * 充电设备接口id
+     */
+    private String connectorId;
+
+    /**
+     * 充电设备接口短编号
+     */
+    private String shortId;
+
+    /**
+     * 车位编号
+     */
+    private String parkingNo;
+
+
+}

+ 1 - 0
entity/src/main/java/com/kym/entity/admin/vo/CustomUserVo.java

@@ -28,6 +28,7 @@ public class CustomUserVo {
     private int payAmount;
     private int discountAmount;
     private int balance;
+    private int frozenAmount;
     private Long refundTimes;
     private int refundAmount;
     /**

+ 182 - 0
entity/src/main/java/com/kym/entity/admin/vo/StatementsVo.java

@@ -0,0 +1,182 @@
+package com.kym.entity.admin.vo;
+
+import com.kym.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+
+/**
+ * <p>
+ * 客户对账单vo
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@Getter
+@Setter
+@Accessors(chain = true)
+public class StatementsVo extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 客户用户id
+     */
+    private Long adminUserId;
+
+    /**
+     * 客户姓名
+     */
+    private String adminUserName;
+
+    /**
+     * 角色名
+     */
+    private String roleName;
+
+    /**
+     * 角色描述
+     */
+    private String roleDesc;
+
+    /**
+     * 站点id
+     */
+    private String stationId;
+
+    /**
+     * 站点名称
+     */
+    private String stationName;
+
+    /**
+     * 统计时间(月)
+     */
+    private String statMonth;
+
+    /**
+     * 订单电量
+     */
+    private Double totalPower;
+
+    /**
+     * 实际抄表电量
+     */
+    private Double actualPower;
+
+    /**
+     * 电损电量
+     */
+    private Double elecLossPower;
+
+    /**
+     * 订单金额(分)
+     */
+    private Integer totalMoney;
+
+    /**
+     * 订单电费金额(分)
+     */
+    private Integer elecMoney;
+
+    /**
+     * 实际抄表电费金额(分)
+     */
+    private Integer actualElecMoney;
+
+    /**
+     * 电损电费金额(分)
+     */
+    private Integer elecLossMoney;
+
+    /**
+     * 服务费金额(分)
+     */
+    private Integer serviceMoney;
+
+    /**
+     * 优惠金额(分)
+     */
+    private Integer discountAmount;
+
+    /**
+     * 服务费优惠金额(分)
+     */
+    private Integer serviceMoneyDiscount;
+
+    /**
+     * 实际参与分成的服务费(分)
+     */
+    private Integer actualServiceMoney;
+
+    /**
+     * 分成比例 0.45表示45%
+     */
+    private Double splittingProportion;
+
+    /**
+     * 分成金额(分)
+     */
+    private Integer splittingAmount;
+
+    /**
+     * 电损承担比例 0.30代表30%
+     */
+    private Double elecLossProportion;
+
+    /**
+     * 电损承担金额(分)
+     */
+    private Integer elecLossAmount;
+
+    /**
+     * 增值税率 0.06表示6%
+     */
+    private Double vatRate;
+
+    /**
+     * 增值税额(分)
+     */
+    private Integer vatAmount;
+
+    /**
+     * 实际分成金额
+     */
+    private Integer actualSplittingAmount;
+
+    /**
+     * 状态:0-无效,1-有效
+     */
+    private Integer status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 账户名
+     */
+    private String accountName;
+
+    /**
+     * 电话号码
+     */
+    private String telephone;
+
+    /**
+     * 开户行名称
+     */
+    private String bankName;
+
+    /**
+     * 银行卡号
+     */
+    private String bankCardNo;
+
+    /**
+     * 纳税人识别号
+     */
+    private String taxNo;
+}

+ 1 - 0
entity/src/main/java/com/kym/entity/miniapp/ChargeOrder.java

@@ -55,6 +55,7 @@ public class ChargeOrder extends BaseEntity implements Serializable {
     public static int STOP_REASON_BMS停止 = 2;
     public static int STOP_REASON_充电机器设备故障 = 3;
     public static int STOP_REASON_连接器断开 = 4;
+    public static int STOP_REASON_预约启动充电失败 = 5;
 
     private Long userId;
 

+ 2 - 0
entity/src/main/java/com/kym/entity/miniapp/DataDict.java

@@ -27,6 +27,8 @@ public class DataDict extends BaseEntity implements Serializable {
      */
     private String code;
 
+    private Long weight;
+
     /**
      * 名称
      */

+ 16 - 0
mapper/src/main/java/com/kym/mapper/admin/InvestorInfoMapper.java

@@ -0,0 +1,16 @@
+package com.kym.mapper.admin;
+
+import com.kym.entity.admin.InvestorInfo;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * <p>
+ * 投资者-物业信息表 Mapper 接口
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+public interface InvestorInfoMapper extends BaseMapper<InvestorInfo> {
+
+}

+ 16 - 0
mapper/src/main/java/com/kym/mapper/admin/StatementsMapper.java

@@ -0,0 +1,16 @@
+package com.kym.mapper.admin;
+
+import com.github.yulichang.base.MPJBaseMapper;
+import com.kym.entity.admin.Statements;
+
+/**
+ * <p>
+ * 客户对账单 Mapper 接口
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+public interface StatementsMapper extends MPJBaseMapper<Statements> {
+
+}

+ 31 - 0
mapper/src/main/resources/mappers/admin/InvestorInfoMapper.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.kym.mapper.admin.InvestorInfoMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.kym.entity.admin.InvestorInfo">
+        <result column="id" property="id" />
+        <result column="admin_user_id" property="adminUserId" />
+        <result column="admin_user_name" property="adminUserName" />
+        <result column="station_id" property="stationId" />
+        <result column="station_name" property="stationName" />
+        <result column="vat_rate" property="vatRate" />
+        <result column="splitting_proportion" property="splittingProportion" />
+        <result column="elec_loss_proportion" property="elecLossProportion" />
+        <result column="account_name" property="accountName" />
+        <result column="telephone" property="telephone" />
+        <result column="bank_name" property="bankName" />
+        <result column="bank_card_no" property="bankCardNo" />
+        <result column="status" property="status" />
+        <result column="remark" property="remark" />
+        <result column="tax_no" property="taxNo" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id,admin_user_id, admin_user_name, station_id,station_name, vat_rate, splitting_proportion, elec_loss_proportion,account_name, telephone, bank_name, bank_card_no, status, remark,tax_no,create_time,update_time
+    </sql>
+
+</mapper>

+ 42 - 0
mapper/src/main/resources/mappers/admin/StatementsMapper.xml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.kym.mapper.admin.StatementsMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.kym.entity.admin.Statements">
+        <result column="admin_user_id" property="adminUserId" />
+        <result column="admin_user_name" property="adminUserName" />
+        <result column="station_id" property="stationId" />
+        <result column="station_name" property="stationName" />
+        <result column="start_time" property="startTime" />
+        <result column="end_time" property="endTime" />
+        <result column="total_power" property="totalPower" />
+        <result column="actual_power" property="actualPower" />
+        <result column="elec_loss_power" property="elecLossPower" />
+        <result column="total_money" property="totalMoney" />
+        <result column="elec_money" property="elecMoney" />
+        <result column="actual_elec_money" property="actualElecMoney" />
+        <result column="elec_loss_money" property="elecLossMoney" />
+        <result column="service_money" property="serviceMoney" />
+        <result column="discount_amount" property="discountAmount" />
+        <result column="service_money_discount" property="serviceMoneyDiscount" />
+        <result column="actual_service_money" property="actualServiceMoney" />
+        <result column="splitting_proportion" property="splittingProportion" />
+        <result column="splitting_amount" property="splittingAmount" />
+        <result column="elec_loss_proportion" property="elecLossProportion" />
+        <result column="elec_loss_amount" property="elecLossAmount" />
+        <result column="vat_rate" property="vatRate" />
+        <result column="vat_amount" property="vatAmount" />
+        <result column="actual_splitting_amount" property="actualSplittingAmount" />
+        <result column="status" property="status" />
+        <result column="remark" property="remark" />
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id,admin_user_id, admin_user_name, station_id, station_name, start_time, end_time, total_power, actual_power, elec_loss_power,
+        total_money, elec_money, actual_elec_money, elec_loss_money, service_money, discount_amount, service_money_discount,
+        actual_service_money, splitting_proportion, splitting_amount, elec_loss_proportion,elec_loss_amount,vat_rate, vat_amount, actual_splitting_amount, status, remark
+    </sql>
+
+</mapper>

+ 4 - 1
mapper/src/main/resources/mappers/admin/StationStatDayMapper.xml

@@ -13,16 +13,19 @@
         <result column="total_money" property="totalMoney" />
         <result column="elec_money" property="elecMoney" />
         <result column="service_money" property="serviceMoney" />
+        <result column="service_money_discount" property="serviceMoneyDiscount" />
+        <result column="discount_amount" property="discountAmount" />
         <result column="avg_order_elec" property="avgOrderElec" />
         <result column="avg_order_money" property="avgOrderMoney" />
         <result column="avg_connector_elec" property="avgConnectorElec" />
+        <result column="connector_usage_rate" property="connectorUsageRate" />
         <result column="create_time" property="createTime" />
         <result column="update_time" property="updateTime" />
     </resultMap>
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id,station_id, stat_day, charge_users, valid_orders, total_power, total_money, elec_money, service_money, avg_order_elec, avg_order_money, avg_connector_elec,create_time, update_time
+        id,station_id, stat_day, charge_users, valid_orders, total_power, total_money, elec_money, service_money,service_money_discount,discount_amount, avg_order_elec, avg_order_money, avg_connector_elec,connector_usage_rate,create_time, update_time
     </sql>
 
 </mapper>

+ 6 - 1
mapper/src/main/resources/mappers/admin/StationStatMonthMapper.xml

@@ -13,16 +13,21 @@
         <result column="total_money" property="totalMoney" />
         <result column="elec_money" property="elecMoney" />
         <result column="service_money" property="serviceMoney" />
+        <result column="service_money_discount" property="serviceMoneyDiscount" />
+        <result column="discount_amount" property="discountAmount" />
         <result column="avg_order_elec" property="avgOrderElec" />
         <result column="avg_order_money" property="avgOrderMoney" />
         <result column="avg_connector_elec" property="avgConnectorElec" />
+        <result column="connector_usage_rate" property="connectorUsageRate" />
+        <result column="actual_power" property="actualPower" />
+        <result column="actual_elec_money" property="actualElecMoney" />
         <result column="create_time" property="createTime" />
         <result column="update_time" property="updateTime" />
     </resultMap>
 
     <!-- 通用查询结果列 -->
     <sql id="Base_Column_List">
-        id,station_id, stat_month, charge_users, valid_orders, total_power, total_money, elec_money, service_money, avg_order_elec, avg_order_money, avg_connector_elec,create_time, update_time
+        id,station_id, stat_month, charge_users, valid_orders, total_power, total_money, elec_money, service_money,service_money_discount,discount_amount, avg_order_elec, avg_order_money, avg_connector_elec,connector_usage_rate,actual_power,actual_elec_money,create_time,update_time,create_time, update_time
     </sql>
 
 </mapper>

+ 1 - 1
miniapp/src/main/java/com/kym/miniapp/aspect/AppLogAspect.java

@@ -86,7 +86,7 @@ public class AppLogAspect {
         //设置IP地址
         appLog.setIp(IPUtils.getIpAddr(request));
         //用户名、公司id、后台用户id
-        if (!apiLog.value().contains("推送") && apiLog.value().contains("拉取")) {
+        if (!apiLog.value().contains("推送") && !apiLog.value().contains("拉取")) {
             appLog.setUserId(StpUtil.getSession().getLong("userId"));
             appLog.setCompanyId(StpUtil.getSession().getLong("companyId"));
             appLog.setUsername(StpUtil.getSession().getString("username"));

+ 11 - 0
miniapp/src/main/java/com/kym/miniapp/controller/DataDictController.java

@@ -1,5 +1,7 @@
 package com.kym.miniapp.controller;
 
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.dev33.satoken.annotation.SaMode;
 import com.kym.common.IQuery;
 import com.kym.common.R;
 import com.kym.common.controller.IController;
@@ -11,6 +13,8 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.List;
+
 
 /**
  * <p>
@@ -56,4 +60,11 @@ public class DataDictController extends IController {
     }
 
 
+    @SaCheckPermission(value = {"dict.add", "dict.modify"}, mode = SaMode.OR)
+    @PostMapping("saveOrUpdate")
+    public R<?> saveOrUpdate(@RequestBody List<DataDict> dictList) {
+        return resp((t) -> dataDictService.saveOrUpdate(dictList));
+    }
+
+
 }

+ 22 - 11
miniapp/src/main/java/com/kym/miniapp/jobs/StartChargeDelayJob.java

@@ -1,6 +1,7 @@
 package com.kym.miniapp.jobs;
 
 import com.baomidou.dynamic.datasource.annotation.DS;
+import com.google.common.util.concurrent.RateLimiter;
 import com.kym.common.utils.CommUtil;
 import com.kym.entity.miniapp.ChargeOrder;
 import com.kym.entity.miniapp.delay.DelayChargeOrder;
@@ -30,6 +31,7 @@ import java.util.concurrent.Executors;
 @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) // 设置成单例
 public class StartChargeDelayJob implements DelayService<DelayChargeOrder> {
 
+    private final static RateLimiter rateLimiter = RateLimiter.create(4);
 
     /**
      * 预约订单队列
@@ -40,7 +42,7 @@ public class StartChargeDelayJob implements DelayService<DelayChargeOrder> {
     /**
      * 线程池
      */
-    private final ExecutorService executor = Executors.newFixedThreadPool(2);
+    private final ExecutorService executor = Executors.newFixedThreadPool(1);
 
     public StartChargeDelayJob(ChargeOrderService chargeOrderService, ChargeService chargeService) {
         this.chargeOrderService = chargeOrderService;
@@ -69,34 +71,43 @@ public class StartChargeDelayJob implements DelayService<DelayChargeOrder> {
         START_DELAY_QUEUE.addAll(delayList);
 
         // 开启线程处理队列消息
-        executor.execute(() -> {
-            ThreadLocal<String> threadLocal = new ThreadLocal<>();
-            log.info("预约充电订单处理线程:{}", Thread.currentThread().getName());
-            DelayedItem<DelayChargeOrder> delayedItem;
-            while (true) {
+        while (true) {
+            executor.execute(() -> {
+                ThreadLocal<String> threadLocal = new ThreadLocal<>();
+                log.info("预约充电订单处理线程:{}", Thread.currentThread().getName());
+                DelayedItem<DelayChargeOrder> delayedItem;
+
                 try {
                     // 线程休眠100ms
                     Thread.sleep(100);
                     delayedItem = START_DELAY_QUEUE.take();
+                    log.info("出队预约充电订单:{},队列剩余:{}", delayedItem.data.getStartChargeSeq(), START_DELAY_QUEUE.size());
                     // 启动充电
                     var order = delayedItem.data;
                     threadLocal.set(order.getStartChargeSeq());
                     chargeService.queryStartCharge(order.getUserId(), order.getConnectorId(), null, false, null, null);
                     log.info("预约充电启动成功:用户:{},订单号:{},预约启动时间:{}", order.getUserId(), order.getStartChargeSeq(), order.getStartTime());
                     // 线程休眠100ms
-                    Thread.sleep(100);
+                    Thread.sleep(200);
                 } catch (Exception e) {
                     if (e instanceof InterruptedException) {
                         log.error("预约充电队列take异常", e);
                     } else {
-                        log.info("预约启动充电失败,订单号:{}", threadLocal.get(), e);
+                        log.info("预约启动充电失败,订单号:{}", threadLocal.get());
+                        log.error(e.getMessage());
+                        // 启动失败将订单状态修改为充电状态已结束,订单状态已确认,结束原因:预约启动失败
+                        chargeOrderService.lambdaUpdate()
+                                .eq(ChargeOrder::getStartChargeSeq, threadLocal.get())
+                                .set(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_已结束)
+                                .set(ChargeOrder::getOrderStatus, ChargeOrder.ORDER_STATUS_失败)
+                                .set(ChargeOrder::getStopReason, ChargeOrder.STOP_REASON_预约启动充电失败)
+                                .update();
                     }
                 } finally {
                     threadLocal.remove();
                 }
-            }
-        });
-
+            });
+        }
     }
 
 

+ 3 - 3
miniapp/src/main/java/com/kym/miniapp/jobs/StopChargeDelayJob.java

@@ -41,7 +41,7 @@ public class StopChargeDelayJob implements DelayService<DelayChargeOrder> {
     /**
      * 线程池
      */
-    private final ExecutorService executor = Executors.newFixedThreadPool(2);
+    private final ExecutorService executor = Executors.newFixedThreadPool(1);
 
     public StopChargeDelayJob(ChargeOrderService chargeOrderService, ChargeService chargeService) {
         this.chargeOrderService = chargeOrderService;
@@ -78,7 +78,7 @@ public class StopChargeDelayJob implements DelayService<DelayChargeOrder> {
             DelayedItem<DelayChargeOrder> delayedItem;
             while (true) {
                 try {
-                    Thread.sleep(200);
+                    Thread.sleep(100);
                     delayedItem = STOP_DELAY_QUEUE.take();
                     // 停止充电
                     var order = delayedItem.data;
@@ -89,7 +89,7 @@ public class StopChargeDelayJob implements DelayService<DelayChargeOrder> {
                         chargeService.queryStopCharge(order.getUserId(), order.getConnectorId());
                         log.info("预约充电停止成功:用户:{},订单号:{},预约停止时间:{}", order.getUserId(), order.getStartChargeSeq(), order.getEndTime());
                         // 线程休眠100ms
-                        Thread.sleep(100);
+                        Thread.sleep(200);
                     } else {
                         log.error("预约充电停止异常:订单不匹配:原订单:{},当前设备最新订单:{}", order.getStartChargeSeq(), currentChargeOrder.getStartChargeSeq());
                         throw new BusinessException("预约充电停止异常");

+ 1 - 1
service/src/main/java/com/kym/service/admin/AdminUserService.java

@@ -30,7 +30,7 @@ public interface AdminUserService extends MPJBaseService<AdminUser> {
 
     PageBean<AdminUserVo> listAdminUser(CommonQueryParam params);
 
-    void createAdminUser(AdminUserVo adminUserVo);
+    String createAdminUser(AdminUserVo adminUserVo);
 
     Object detail(long id);
 

+ 20 - 0
service/src/main/java/com/kym/service/admin/InvestorInfoService.java

@@ -0,0 +1,20 @@
+package com.kym.service.admin;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.kym.entity.admin.InvestorInfo;
+import com.kym.entity.admin.queryParams.CommonQueryParam;
+import com.kym.entity.common.PageBean;
+
+
+/**
+ * <p>
+ * 投资者-物业信息表 服务类
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+public interface InvestorInfoService extends IService<InvestorInfo> {
+
+    PageBean<InvestorInfo> list(CommonQueryParam params);
+}

+ 24 - 0
service/src/main/java/com/kym/service/admin/StatementsService.java

@@ -0,0 +1,24 @@
+package com.kym.service.admin;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.kym.entity.admin.Statements;
+import com.kym.entity.admin.queryParams.StatementsQueryParam;
+import com.kym.entity.admin.vo.StatementsVo;
+import com.kym.entity.common.PageBean;
+
+/**
+ * <p>
+ * 客户对账单 服务类
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+public interface StatementsService extends IService<Statements> {
+
+    void createStatements(String statMonthId);
+
+    PageBean<Statements> listStatements(StatementsQueryParam params);
+
+    StatementsVo preview(String statId);
+}

+ 3 - 0
service/src/main/java/com/kym/service/admin/StationService.java

@@ -3,6 +3,7 @@ package com.kym.service.admin;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.github.yulichang.base.MPJBaseService;
 import com.kym.entity.admin.Station;
+import com.kym.entity.admin.vo.ConnectorVo;
 import com.kym.entity.admin.vo.StationVo;
 import com.kym.entity.enplus.EnStationStatsInfo;
 import com.kym.entity.enplus.EnStationStatusInfo;
@@ -30,4 +31,6 @@ public interface StationService extends MPJBaseService<Station> {
     public void pullEnStationInfos();
 
     void modifyStation(Station station);
+
+    void addStation(List<ConnectorVo> connectorVos);
 }

+ 5 - 0
service/src/main/java/com/kym/service/admin/StationStatMonthService.java

@@ -2,6 +2,8 @@ package com.kym.service.admin;
 
 import com.kym.entity.admin.StationStatMonth;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.kym.entity.admin.queryParams.StatementsQueryParam;
+import com.kym.entity.common.PageBean;
 
 /**
  * <p>
@@ -13,4 +15,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
  */
 public interface StationStatMonthService extends IService<StationStatMonth> {
 
+    PageBean<StationStatMonth> listStatMonth(StatementsQueryParam params);
+
+    void modifyStationStatMonth(StationStatMonth stationStatMonth);
 }

+ 0 - 5
service/src/main/java/com/kym/service/admin/impl/ActivityServiceImpl.java

@@ -45,11 +45,6 @@ import static com.kym.entity.admin.Activity.DISCOUNT_TYPE_服务费折扣权益;
 @Slf4j
 public class ActivityServiceImpl extends MPJBaseServiceImpl<ActivityMapper, Activity> implements ActivityService {
 
-    /**
-     * 线程池
-     */
-    private final ExecutorService executor = Executors.newFixedThreadPool(2);
-
     private final ActivityStationService activityStationService;
     private final RechargeRightsService rechargeRightsService;
     private final UserRechargeRightsService userRechargeRightsService;

+ 2 - 1
service/src/main/java/com/kym/service/admin/impl/AdminUserServiceImpl.java

@@ -110,7 +110,7 @@ public class AdminUserServiceImpl extends MPJBaseServiceImpl<AdminUserMapper, Ad
     }
 
     @Override
-    public void createAdminUser(AdminUserVo adminUserVo) {
+    public String createAdminUser(AdminUserVo adminUserVo) {
         var password = MD5.digestHex(adminUserVo.getPassword().concat(MD5.digestHex(adminUserVo.getMobilePhone()).substring(0, 5)));
         // 用户
         var adminUser = new AdminUser();
@@ -124,6 +124,7 @@ public class AdminUserServiceImpl extends MPJBaseServiceImpl<AdminUserMapper, Ad
                 .setAdminUserId(adminUser.getId())
                 .setRoleId(adminUserVo.getRoleId());
         adminUserRoleService.save(adminUserRole);
+        return String.valueOf(adminUser.getId());
     }
 
     @Override

+ 36 - 0
service/src/main/java/com/kym/service/admin/impl/InvestorInfoServiceImpl.java

@@ -0,0 +1,36 @@
+package com.kym.service.admin.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.github.pagehelper.PageHelper;
+import com.kym.common.utils.CommUtil;
+import com.kym.entity.admin.InvestorInfo;
+import com.kym.entity.admin.queryParams.CommonQueryParam;
+import com.kym.entity.common.PageBean;
+import com.kym.mapper.admin.InvestorInfoMapper;
+import com.kym.service.admin.InvestorInfoService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+
+/**
+ * <p>
+ * 投资者-物业信息表 服务实现类
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@Service
+@DS("db-admin")
+public class InvestorInfoServiceImpl extends ServiceImpl<InvestorInfoMapper, InvestorInfo> implements InvestorInfoService {
+
+    @Override
+    public PageBean<InvestorInfo> list(CommonQueryParam params) {
+        PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        var list = lambdaQuery()
+                .like(CommUtil.isNotEmptyAndNull(params.getMobilePhone()), InvestorInfo::getTelephone, params.getMobilePhone())
+                .like(CommUtil.isNotEmptyAndNull(params.getUsername()), InvestorInfo::getAdminUserName, params.getUsername())
+                .list();
+        return new PageBean<>(list);
+    }
+}

+ 1 - 0
service/src/main/java/com/kym/service/admin/impl/RoleServiceImpl.java

@@ -31,6 +31,7 @@ public class RoleServiceImpl extends MPJBaseServiceImpl<RoleMapper, Role> implem
     @Transactional(rollbackFor = Exception.class)
     @Override
     public Object add(Role role) {
+        role.setId(null);
         return save(role);
     }
 

+ 137 - 0
service/src/main/java/com/kym/service/admin/impl/StatementsServiceImpl.java

@@ -0,0 +1,137 @@
+package com.kym.service.admin.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.github.pagehelper.PageHelper;
+import com.github.yulichang.base.MPJBaseServiceImpl;
+import com.github.yulichang.toolkit.JoinWrappers;
+import com.github.yulichang.wrapper.MPJLambdaWrapper;
+import com.kym.common.exception.BusinessException;
+import com.kym.common.utils.CommUtil;
+import com.kym.entity.admin.*;
+import com.kym.entity.admin.queryParams.StatementsQueryParam;
+import com.kym.entity.admin.vo.StatementsVo;
+import com.kym.entity.common.PageBean;
+import com.kym.mapper.admin.StatementsMapper;
+import com.kym.service.admin.*;
+import com.kym.service.cache.KymCache;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * <p>
+ * 客户对账单 服务实现类
+ * </p>
+ *
+ * @author skyline
+ * @since 2023-12-27
+ */
+@Service
+@DS("db-admin")
+public class StatementsServiceImpl extends MPJBaseServiceImpl<StatementsMapper, Statements> implements StatementsService {
+
+    private final InvestorInfoService investorInfoService;
+
+    private final StationStatMonthService stationStatMonthService;
+
+    private final AdminUserRoleService adminUserRoleService;
+
+    public StatementsServiceImpl(InvestorInfoService investorInfoService, StationStatMonthService stationStatMonthService, AdminUserRoleService adminUserRoleService) {
+        this.investorInfoService = investorInfoService;
+        this.stationStatMonthService = stationStatMonthService;
+        this.adminUserRoleService = adminUserRoleService;
+    }
+
+    @Override
+    @Transactional
+    public void createStatements(String statMonthId) {
+        // 站点统计信息
+        var statMonthInfo = stationStatMonthService.lambdaQuery()
+                .eq(StationStatMonth::getId, statMonthId)
+                .one();
+
+        // 校验站点月统计信息已填写实际抄表电量和实际抄表电费金额
+        if (statMonthInfo.getActualPower() == null || statMonthInfo.getActualElecMoney() == null) {
+            throw new BusinessException("请先完善月统计实际抄表电量和实际抄表电费金额");
+        }
+
+        // 站点关联客户和物业信息
+        var investorInfoList = investorInfoService.lambdaQuery()
+                .eq(InvestorInfo::getStationId, statMonthInfo.getStationId())
+                .list();
+
+        if (CommUtil.isEmptyOrNull(investorInfoList)) {
+            throw new BusinessException("请先完善站点相关投资者/物业信息");
+        }
+
+        // 创建客户对账单
+        var res = investorInfoList.stream().map(investorInfo -> {
+            // 实际参与分成的服务费=总服务费-优惠金额-电损电费
+            var actualServiceMoney = statMonthInfo.getServiceMoney() - statMonthInfo.getDiscountAmount();
+            // 总电损电费
+            var elecLossMoney = statMonthInfo.getActualElecMoney() - statMonthInfo.getElecMoney();
+            var elecLossAmount = (int) (elecLossMoney * investorInfo.getElecLossProportion());
+            // 分成金额 = 实际参与分成的服务费 * 分成比例 - 电损金额 * 电损承担比例
+            var splittingAmount = (int) (actualServiceMoney * investorInfo.getSplittingProportion()) - elecLossAmount;
+            return new Statements()
+                    .setAdminUserId(investorInfo.getAdminUserId())
+                    .setAdminUserName(investorInfo.getAdminUserName())
+                    .setStationId(investorInfo.getStationId())
+                    .setStationName(KymCache.INSTANCE.getStationNameById(investorInfo.getStationId()))
+                    .setStatMonth(statMonthInfo.getStatMonth())
+                    .setTotalPower(statMonthInfo.getTotalPower())
+                    .setActualPower(statMonthInfo.getActualPower()) // 实际抄表电量
+                    .setElecLossPower(statMonthInfo.getActualPower() - statMonthInfo.getTotalPower()) // 电损电量
+                    .setTotalMoney(statMonthInfo.getTotalMoney())
+                    .setElecMoney(statMonthInfo.getElecMoney())
+                    .setActualElecMoney(statMonthInfo.getActualElecMoney())// 实际抄表电费
+                    .setElecLossMoney(elecLossMoney) // 电损电费(分)
+                    .setServiceMoney(statMonthInfo.getServiceMoney())
+                    .setDiscountAmount(statMonthInfo.getDiscountAmount())
+                    .setServiceMoneyDiscount(statMonthInfo.getServiceMoneyDiscount())
+                    .setActualServiceMoney(actualServiceMoney) // 实际参与分成的服务费(分)
+                    .setSplittingProportion(investorInfo.getSplittingProportion()) // 分成比例 0.45表示45%
+                    .setSplittingAmount(splittingAmount)  // 分成金额(分)
+                    .setElecLossProportion(investorInfo.getElecLossProportion()) // 电损承担比例
+                    .setElecLossAmount((int) (elecLossMoney * investorInfo.getElecLossProportion()))    // 电损承担金额
+                    .setVatRate(investorInfo.getVatRate()) // 增值税率 0.06表示6%
+                    .setVatAmount((int) (splittingAmount / (1 + investorInfo.getVatRate()) * investorInfo.getVatRate() * 1.12)) // 增值税额(分)
+                    .setActualSplittingAmount(splittingAmount - (int) (splittingAmount / (1 + investorInfo.getVatRate()) * investorInfo.getVatRate() * 1.12)); // 实际分成金额(分)
+        }).toList();
+        saveBatch(res);
+    }
+
+    @Override
+    public PageBean<Statements> listStatements(StatementsQueryParam params) {
+        PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        var res = lambdaQuery()
+                .eq(!CommUtil.isEmptyOrNull(params.getStationId()), Statements::getStationId, params.getStationId())
+                .eq(!CommUtil.isEmptyOrNull(params.getStatMonth()), Statements::getStatMonth, params.getStatMonth())
+                .like(!CommUtil.isEmptyOrNull(params.getAdminUserName()), Statements::getAdminUserName, params.getAdminUserName())
+                .orderByDesc(Statements::getStatMonth)
+                .list();
+        return new PageBean<>(res);
+    }
+
+    @Override
+    public StatementsVo preview(String statId) {
+        var statements = getById(statId);
+        var investorInfo = investorInfoService.lambdaQuery().eq(InvestorInfo::getAdminUserId, statements.getAdminUserId()).one();
+        // 查询角色
+        MPJLambdaWrapper<AdminUserRole> wrapper = JoinWrappers.lambda(AdminUserRole.class)
+                .select(Role::getRoleName,Role::getRoleDesc)
+                .leftJoin(Role.class,Role::getId,AdminUserRole::getRoleId)
+                .eq(AdminUserRole::getAdminUserId,statements.getAdminUserId());
+        var res = adminUserRoleService.selectJoinMap(wrapper);
+
+
+
+        var statementsVo = new StatementsVo();
+        statementsVo.setRoleName(res.get("role_name").toString());
+        statementsVo.setRoleDesc(res.get("role_desc").toString());
+        BeanUtils.copyProperties(statements, statementsVo);
+        BeanUtils.copyProperties(investorInfo, statementsVo);
+        return statementsVo;
+    }
+}

+ 44 - 4
service/src/main/java/com/kym/service/admin/impl/StationServiceImpl.java

@@ -10,7 +10,9 @@ import com.kym.common.utils.AESUtil;
 import com.kym.common.utils.CommUtil;
 import com.kym.entity.admin.ConnectorInfo;
 import com.kym.entity.admin.EquipmentInfo;
+import com.kym.entity.admin.EquipmentRelation;
 import com.kym.entity.admin.Station;
+import com.kym.entity.admin.vo.ConnectorVo;
 import com.kym.entity.admin.vo.StationVo;
 import com.kym.entity.enplus.EnEquipmentInfo;
 import com.kym.entity.enplus.EnStationStatsInfo;
@@ -18,6 +20,7 @@ import com.kym.entity.enplus.EnStationStatusInfo;
 import com.kym.mapper.admin.StationMapper;
 import com.kym.service.admin.ConnectorInfoService;
 import com.kym.service.admin.EquipmentInfoService;
+import com.kym.service.admin.EquipmentRelationService;
 import com.kym.service.admin.StationService;
 import com.kym.service.cache.KymCache;
 import com.kym.service.enplus.EnPlusService;
@@ -46,11 +49,13 @@ public class StationServiceImpl extends MPJBaseServiceImpl<StationMapper, Statio
     private final EnPlusService enPlusService;
     private final EquipmentInfoService equipmentInfoService;
     private final ConnectorInfoService connectorInfoService;
+    private final EquipmentRelationService equipmentRelationService;
 
-    public StationServiceImpl(EnPlusService enPlusService, EquipmentInfoService equipmentInfoService, ConnectorInfoService connectorInfoService) {
+    public StationServiceImpl(EnPlusService enPlusService, EquipmentInfoService equipmentInfoService, ConnectorInfoService connectorInfoService, EquipmentRelationService equipmentRelationService) {
         this.enPlusService = enPlusService;
         this.equipmentInfoService = equipmentInfoService;
         this.connectorInfoService = connectorInfoService;
+        this.equipmentRelationService = equipmentRelationService;
     }
 
     @PostConstruct
@@ -80,9 +85,11 @@ public class StationServiceImpl extends MPJBaseServiceImpl<StationMapper, Statio
             station.getEquipmentInfos().forEach(enEquipmentInfo ->
                     enEquipmentInfo.setShortId(KymCache.INSTANCE.getShortIdByEquipmentIdOrConnectorId(enEquipmentInfo.getEquipmentId()))
                             .setParkingNo(KymCache.INSTANCE.getParkNoByEquipmentIdOrConnectorId(enEquipmentInfo.getEquipmentId())));
-            var list = stations.stream().filter(item -> item.getStationId().equals(station.getStationId())).toList();
-            if (!CommUtil.isEmptyOrNull(list)) {
-                station.setPictures(stations.stream().filter(item -> item.getStationId().equals(station.getStationId())).toList().get(0).getPictures());
+            var res = stations.stream().filter(item -> item.getStationId().equals(station.getStationId())).toList();
+            if (!res.isEmpty()) {
+                station.setPictures(res.get(0).getPictures());
+            } else {
+                station.setPictures(null);
             }
         });
 
@@ -195,4 +202,37 @@ public class StationServiceImpl extends MPJBaseServiceImpl<StationMapper, Statio
                 .eq(Station::getStationId, station.getStationId()).update();
     }
 
+    @Override
+    @Transactional
+    public void addStation(List<ConnectorVo> connectorVos) {
+        // 1、组装t_station数据
+
+        // 2、组装t_equipment_info数据
+        var equipmentInfos = connectorVos.stream().map(item -> {
+            var equipmentInfo = new EquipmentInfo();
+            BeanUtils.copyProperties(item, equipmentInfo);
+            return equipmentInfo;
+        }).toList();
+        equipmentInfoService.saveBatch(equipmentInfos);
+
+        // 3、组装t_connector_info数据
+        var connectorInfos = connectorVos.stream().map(item -> {
+            var connectorInfo = new ConnectorInfo();
+            BeanUtils.copyProperties(item, connectorInfo);
+            return connectorInfo;
+        }).toList();
+        connectorInfoService.saveBatch(connectorInfos);
+
+        // 4、组装t_equipment_relation数据
+        var equipmentRelations = connectorVos.stream().map(item -> {
+            var equipmentRelation = new EquipmentRelation();
+            BeanUtils.copyProperties(item, equipmentRelation);
+            return equipmentRelation;
+        }).toList();
+        equipmentRelationService.saveBatch(equipmentRelations);
+
+        // 5、更新KymCache数据
+        // TODO redis发布订阅?
+    }
+
 }

+ 26 - 1
service/src/main/java/com/kym/service/admin/impl/StationStatMonthServiceImpl.java

@@ -1,9 +1,14 @@
 package com.kym.service.admin.impl;
 
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.github.pagehelper.PageHelper;
+import com.kym.common.utils.CommUtil;
 import com.kym.entity.admin.StationStatMonth;
+import com.kym.entity.admin.queryParams.StatementsQueryParam;
+import com.kym.entity.common.PageBean;
 import com.kym.mapper.admin.StationStatMonthMapper;
 import com.kym.service.admin.StationStatMonthService;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.kym.service.cache.KymCache;
 import org.springframework.stereotype.Service;
 
 /**
@@ -17,4 +22,24 @@ import org.springframework.stereotype.Service;
 @Service
 public class StationStatMonthServiceImpl extends ServiceImpl<StationStatMonthMapper, StationStatMonth> implements StationStatMonthService {
 
+    @Override
+    public PageBean<StationStatMonth> listStatMonth(StatementsQueryParam params) {
+        PageHelper.startPage(params.getPageNum(), params.getPageSize());
+        var res = lambdaQuery()
+                .eq(!CommUtil.isEmptyOrNull(params.getStationId()), StationStatMonth::getStationId, params.getStationId())
+                .eq(!CommUtil.isEmptyOrNull(params.getStatMonth()), StationStatMonth::getStatMonth, params.getStatMonth())
+                .orderByDesc(StationStatMonth::getStatMonth)
+                .list();
+        var page = new PageBean<>(res);
+        page.setList(res.stream().peek(item -> item.setStationName(KymCache.INSTANCE.getStationNameById(item.getStationId()))).toList());
+        return page;
+    }
+
+    @Override
+    public void modifyStationStatMonth(StationStatMonth stationStatMonth) {
+        lambdaUpdate().eq(StationStatMonth::getId, stationStatMonth.getId())
+                .set(CommUtil.isNotEmptyAndNull(stationStatMonth.getActualPower()), StationStatMonth::getActualPower, stationStatMonth.getActualPower())
+                .set(CommUtil.isNotEmptyAndNull(stationStatMonth.getActualElecMoney()), StationStatMonth::getActualElecMoney, stationStatMonth.getActualElecMoney())
+                .update();
+    }
 }

+ 6 - 6
service/src/main/java/com/kym/service/cache/KymCache.java

@@ -80,6 +80,11 @@ public enum KymCache {
         return STATION_MAPPING.get(stationId);
     }
 
+    /**
+     * 操作员对应有权限的站点
+     *
+     * @param map
+     */
     public void putAdminUser2Stations(Map<Long, List<String>> map) {
         ADMIN_USER_STATIONS.putAll(map);
     }
@@ -106,12 +111,7 @@ public enum KymCache {
      * @return
      */
     public List<String> getAdminUserStationIds(Long adminUserId) {
-        var res = ADMIN_USER_STATIONS.get(adminUserId);
-//        if (res == null) {
-//            var adminUserStation = adminUserStationService.lambdaQuery().eq(AdminUserStation::getAdminUserId, adminUserId).one();
-//            res = adminUserStation == null ? null : adminUserStation.getStationId();
-//        }
-        return res;
+        return ADMIN_USER_STATIONS.get(adminUserId);
     }
 
     public void putConnectorId2ParkingNo(Map<String, String> map) {

+ 1 - 1
service/src/main/java/com/kym/service/enplus/impl/EnNotifyServiceImpl.java

@@ -315,7 +315,7 @@ public class EnNotifyServiceImpl implements EnNotifyService {
 
                 // 用户充值权益
                 var userRechargeRights = userRechargeRightsService.lambdaQuery()
-                        .eq(UserRechargeRights::getId, orderRechargeRights.getId())
+                        .eq(UserRechargeRights::getId, orderRechargeRights.getUserRightsId())
                         .eq(UserRechargeRights::getUserId, chargeOrder.getUserId())
                         .eq(UserRechargeRights::getRightsId, orderRechargeRights.getRightsId())
                         .one();

+ 2 - 0
service/src/main/java/com/kym/service/enplus/impl/EnPlusServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.crypto.digest.HMac;
 import cn.hutool.crypto.digest.HmacAlgorithm;
 import com.alibaba.fastjson2.JSONObject;
+import com.kym.common.annotation.DynamicCache;
 import com.kym.common.constant.ResponseEnum;
 import com.kym.common.enums.EnPlusApi;
 import com.kym.common.exception.BusinessException;
@@ -236,6 +237,7 @@ public class EnPlusServiceImpl implements EnPlusService {
      * @return
      */
     @Override
+    @DynamicCache(spel = "#connectorId")
     public JSONObject queryEquipBusinessPolicy(String equipBizSeq, String connectorId) {
         var param = """
                 {

+ 4 - 0
service/src/main/java/com/kym/service/miniapp/DataDictService.java

@@ -4,6 +4,8 @@ import com.github.yulichang.base.MPJBaseService;
 import com.kym.common.IQuery;
 import com.kym.entity.miniapp.DataDict;
 
+import java.util.List;
+
 
 /**
  * <p>
@@ -18,4 +20,6 @@ public interface DataDictService extends MPJBaseService<DataDict> {
     Object list(IQuery<DataDict> query);
 
     Object listV2(IQuery<DataDict> query);
+
+    void saveOrUpdate(List<DataDict> dictList);
 }

+ 5 - 2
service/src/main/java/com/kym/service/miniapp/impl/ChargeOrderServiceImpl.java

@@ -76,7 +76,8 @@ public class ChargeOrderServiceImpl extends MPJBaseServiceImpl<ChargeOrderMapper
     public ChargeOrder getChargingOrderByUserId(Long userId) {
         return lambdaQuery()
                 .eq(ChargeOrder::getUserId, userId)
-                .in(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_预约中, ChargeOrder.CHARGE_STATUS_启动中, ChargeOrder.CHARGE_STATUS_充电中)
+                .eq(ChargeOrder::getOrderStatus, ChargeOrder.ORDER_STATUS_未知)
+                .in(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_预约中, ChargeOrder.CHARGE_STATUS_启动中, ChargeOrder.CHARGE_STATUS_充电中, ChargeOrder.CHARGE_STATUS_停止中)
                 .one();
     }
 
@@ -124,7 +125,7 @@ public class ChargeOrderServiceImpl extends MPJBaseServiceImpl<ChargeOrderMapper
         result = result.stream().map(item ->
                         item.setShortId(KymCache.INSTANCE.getShortIdByEquipmentIdOrConnectorId(item.getConnectorId()))
                                 .setStationName(KymCache.INSTANCE.getStationNameById(item.getStationId())))
-                .collect(Collectors.toList());
+                .toList();
 
         // stationId,startChargeSeq,connectorId,startTime,endTime,totalPower,totalMoney,elecMoney,serviceMoney,orderStatus,chargeStatus,stopReason,invoiceStatus
         var rows = result.stream().map(item -> {
@@ -231,6 +232,8 @@ public class ChargeOrderServiceImpl extends MPJBaseServiceImpl<ChargeOrderMapper
     @Override
     @DS("db-admin")
     public Map<String, ?> stationStatDetail(StatQueryParam params) {
+
+        CommUtil.asserts(CommUtil.isNotEmptyAndNull(params.getStationIds()),"站点不能为空");
         if (params.getType().equals(StatQueryParam.TYPE_DAY)) {
             return stationStatDayService.lambdaQuery()
                     .in(StationStatDay::getStationId, params.getStationIds())

+ 14 - 2
service/src/main/java/com/kym/service/miniapp/impl/ChargeServiceImpl.java

@@ -176,6 +176,7 @@ public class ChargeServiceImpl implements ChargeService {
                     .set(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_已取消)
                     .eq(ChargeOrder::getStartChargeSeq, chargeOrder.getStartChargeSeq())
                     .update();
+            LOGGER.info("取消/清除预约订单:{}", chargeOrder.getStartChargeSeq());
         } else {
             LOGGER.info("用户没有预约中的订单");
         }
@@ -202,7 +203,11 @@ public class ChargeServiceImpl implements ChargeService {
         var account = checkCharge(userId, connectorId, isBooking, startTime);
 
         // 是否有之前预约充电创建的订单记录,有则直接用,没有则创建
-        ChargeOrder order = chargeOrderService.lambdaQuery().eq(ChargeOrder::getConnectorId, connectorId).in(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_已取消, ChargeOrder.CHARGE_STATUS_预约中).one();
+        ChargeOrder order = chargeOrderService.lambdaQuery()
+                .eq(ChargeOrder::getUserId, userId)
+                .eq(ChargeOrder::getConnectorId, connectorId)
+                .in(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_已取消, ChargeOrder.CHARGE_STATUS_预约中)
+                .one();
 
         if (order == null) {
             // 充电订单号/设备认证号
@@ -242,7 +247,14 @@ public class ChargeServiceImpl implements ChargeService {
             if (!CommUtil.isEmptyOrNull(endTime)) {
                 order.setEndTime(endTime);
             }
-            chargeOrderService.lambdaUpdate().set(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_预约中).set(ChargeOrder::getIsBooking, order.getIsBooking()).set(ChargeOrder::getStartTime, startTime).set(!CommUtil.isEmptyOrNull(endTime), ChargeOrder::getEndTime, endTime).eq(ChargeOrder::getStartChargeSeq, order.getStartChargeSeq()).update();
+            chargeOrderService.lambdaUpdate()
+                    .set(ChargeOrder::getChargeStatus, ChargeOrder.CHARGE_STATUS_预约中)
+                    .set(ChargeOrder::getIsBooking, order.getIsBooking())
+                    .set(ChargeOrder::getStartTime, startTime)
+                    .set(!CommUtil.isEmptyOrNull(endTime), ChargeOrder::getEndTime, endTime)
+                    .eq(ChargeOrder::getUserId, userId)
+                    .eq(ChargeOrder::getStartChargeSeq, order.getStartChargeSeq())
+                    .update();
 
             var delayChargeOrder = new DelayChargeOrder();
             BeanUtils.copyProperties(order, delayChargeOrder);

+ 29 - 3
service/src/main/java/com/kym/service/miniapp/impl/DataDictServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ClassUtil;
 import cn.hutool.core.util.ReflectUtil;
 import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
+import com.github.pagehelper.PageHelper;
 import com.github.yulichang.base.MPJBaseServiceImpl;
 import com.kym.common.IQuery;
 import com.kym.common.utils.CommUtil;
@@ -12,10 +13,14 @@ import com.kym.entity.miniapp.DataDict;
 import com.kym.mapper.miniapp.DataDictMapper;
 import com.kym.service.miniapp.DataDictService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 
 /**
@@ -33,8 +38,9 @@ public class DataDictServiceImpl extends MPJBaseServiceImpl<DataDictMapper, Data
 
     @Override
     public Object list(IQuery<DataDict> query) {
+        QueryChainWrapper<DataDict> wrapper = getWrapper(query.query, DataDict.class);
 //        PageHelper.startPage(query.pageNum, query.pageSize);
-        List<DataDict> list = list();
+        List<DataDict> list = wrapper.list();
         return new PageBean<>(list);
     }
 
@@ -42,9 +48,23 @@ public class DataDictServiceImpl extends MPJBaseServiceImpl<DataDictMapper, Data
     @Override
     public Object listV2(IQuery<DataDict> query) {
         QueryChainWrapper<DataDict> wrapper = getWrapper(query.query, DataDict.class);
+        wrapper.select("distinct(code)");
         Long count = wrapper.count();
+        PageHelper.startPage(query.pageNum, query.pageSize);
         List<DataDict> list = wrapper.list();
-        return IQuery.newPageBean(count, list);
+        if (CommUtil.isNotEmptyAndNull(list)) {
+
+            List<DataDict> list1 = lambdaQuery().in(CommUtil.isNotEmptyAndNull(list), DataDict::getCode, list.stream().map(DataDict::getCode).collect(Collectors.toList())).list();
+            return IQuery.newPageBean(count, list1.stream().filter(CommUtil.distinctByKey(DataDict::getCode)).toList());
+        } else {
+            return IQuery.newPageBean(0, Collections.emptyList());
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void saveOrUpdate(List<DataDict> dictList) {
+        saveOrUpdateBatch(dictList);
     }
 
 
@@ -64,7 +84,13 @@ public class DataDictServiceImpl extends MPJBaseServiceImpl<DataDictMapper, Data
                 }
                 Object fieldValue = ReflectUtil.getFieldValue(query, field);
                 if (!CommUtil.isEmptyOrNull(fieldValue)) {
-                    wrapper.eq(CommUtil.toDbColumn(field.getName()), fieldValue);
+                    Class<?> aClass = field.getType();
+
+                    if (ClassUtil.isBasicType(aClass)) {
+                        wrapper.eq(CommUtil.toDbColumn(field.getName()), fieldValue);
+                    } else if (String.class.isAssignableFrom(aClass)) {
+                        wrapper.like(CommUtil.toDbColumn(field.getName()), fieldValue);
+                    }
                 }
             }
         }

+ 9 - 2
service/src/main/java/com/kym/service/miniapp/impl/InvoiceServiceImpl.java

@@ -74,6 +74,13 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
         // orderDetails 获取申请开票的订单
         var userId = StpUtil.getLoginIdAsLong();
         var orders = chargeOrderService.getChargeOrdersBySeqs(params.getStartChargeSeqs());
+
+        // 校验未完结的订单 orderStatus = 0
+        if (orders.stream().anyMatch(o -> o.getOrderStatus().equals(ChargeOrder.ORDER_STATUS_未知))) {
+            log.error("存在未知状态的订单:{}", orders.stream().filter(o -> o.getOrderStatus().equals(ChargeOrder.ORDER_STATUS_未知)).map(ChargeOrder::getStartChargeSeq).toArray());
+            throw new BusinessException("请勿勾选未完结的订单");
+        }
+
         // 校验订单是已开票或者开票中
         var invoiced = orders.stream().filter(o -> o.getInvoiceStatus().equals(ChargeOrder.INVOICE_STATUS_开票中) || o.getInvoiceStatus().equals(ChargeOrder.INVOICE_STATUS_已开票)).toList();
         if (!invoiced.isEmpty()) {
@@ -81,7 +88,6 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
             throw new BusinessException("存在已开票或开票中的订单");
         }
 
-
         var orderDetails = orders.stream().map(item -> new InvoiceOrderDetail()
                         .setStartChargeSeq(item.getStartChargeSeq())
                         .setTotalPower(item.getTotalPower()).setTotalMoney(item.getTotalMoney())
@@ -132,7 +138,8 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
                 .like(CommUtil.isNotEmptyAndNull(params.getTaxId()), Invoice::getTaxId, params.getTaxId())
                 .like(CommUtil.isNotEmptyAndNull(params.getInvoiceType()), Invoice::getInvoiceType, params.getInvoiceType())
                 .like(CommUtil.isNotEmptyAndNull(params.getEmail()), Invoice::getEmail, params.getEmail())
-                .eq(params.getStatus() != null, Invoice::getStatus, params.getStatus());
+                .eq(params.getStatus() != null, Invoice::getStatus, params.getStatus())
+                .orderByDesc(Invoice::getId);
         var list = selectJoinList(InvoiceVo.class, wrapper);
         return new PageBean<>(list);
     }

+ 2 - 0
service/src/main/java/com/kym/service/miniapp/impl/UserServiceImpl.java

@@ -254,6 +254,7 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
         // 用户余额,退款次数,退款金额
         var account = accountService.lambdaQuery().in(Account::getUserId, result.stream().map(CustomUserVo::getUserId).toList()).list();
         var user2Balance = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getBalance)));
+        var user2FrozenAmount = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getFrozenAmount)));
         var refund = refundLogService.lambdaQuery().in(RefundLog::getUserId, result.stream().map(CustomUserVo::getUserId).toList()).list();
         // refund按照用户维度计算退款次数和退款总金额
         var user2RefundAmount = refund.stream().collect(Collectors.groupingBy(RefundLog::getUserId, Collectors.summingInt(RefundLog::getRefund)));
@@ -263,6 +264,7 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
         // 将用户余额,退款次数,退款金额放入result中
         var res = result.stream().peek(vo-> {
             vo.setBalance(user2Balance.getOrDefault(vo.getUserId(), 0));
+            vo.setFrozenAmount(user2FrozenAmount.getOrDefault(vo.getUserId(), 0));
             vo.setRefundTimes(user2RefundTimes.getOrDefault(vo.getUserId(), 0L));
             vo.setRefundAmount(user2RefundAmount.getOrDefault(vo.getUserId(), 0));
             vo.setRefundDiscountAmount(user2RefundDiscountAmount.getOrDefault(vo.getUserId(), 0));

+ 2 - 2
service/src/main/java/com/kym/service/wechat/impl/WxPayServiceImpl.java

@@ -428,7 +428,7 @@ public class WxPayServiceImpl implements WxPayService {
             // 退款日志
             var refundLog = new RefundLog().setUserId(payLogs.get(0).getUserId()).setOutTradeNo(payLogs.get(0).getOutTradeNo())
                     .setTotal(payLogs.get(0).getTotal())
-                    .setRefund(refundAmount.get() - account.getDiscountAmount())
+                    .setRefund(refundAmount.get())
                     .setDiscountAmount(account.getDiscountAmount())
                     .setOutRefundNo(OrderUtils.getOrderNo());
             refundLogService.save(refundLog);
@@ -714,6 +714,7 @@ public class WxPayServiceImpl implements WxPayService {
                     .setInvoiceTitle(buyerInformation.getName())
                     .setTaxId(buyerInformation.getTaxpayer_id())
                     .setAddress(buyerInformation.getAddress())
+                    .setTelephone(buyerInformation.getTelephone())
                     .setBankName(buyerInformation.getBank_name())
                     .setBankAccount(buyerInformation.getBank_account())
                     .setEmail(buyerInformation.getEmail())
@@ -837,7 +838,6 @@ public class WxPayServiceImpl implements WxPayService {
                 .setTaxpayer_id(invoice.getTaxId())
                 .setTelephone(invoice.getTelephone())
                 .setAddress(invoice.getAddress())
-                .setTelephone(invoice.getPhone())
                 .setBank_name(invoice.getBankName())
                 .setBank_account(invoice.getBankAccount());