Sfoglia il codice sorgente

配置服务添加

zuypeng 11 mesi fa
parent
commit
29c80e0b5c

+ 15 - 0
admin-web/src/router/route.ts

@@ -380,6 +380,21 @@ export const adminRoutes: Array<RouteRecordRaw> = [
                             icon: 'ele-Collection',
                         },
                     },
+                    {
+                        path: '/org/config',
+                        name: 'orgConfig',
+                        component: () => import('/@/views/admin/config/index.vue'),
+                        meta: {
+                            title: '参数配置',
+                            isLink: '',
+                            isHide: false,
+                            isKeepAlive: true,
+                            isAffix: false,
+                            isIframe: false,
+                            perm: "config.list",
+                            icon: 'ele-Collection',
+                        },
+                    },
                     {
                         path: '/org/faq',
                         name: 'adminFaq',

+ 176 - 0
admin-web/src/views/admin/config/dialog.vue

@@ -0,0 +1,176 @@
+<style scoped lang="scss">
+
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-dialog
+        :title="state.dialog.title"
+        v-model="state.dialog.isShowDialog"
+        width="820px"
+        draggable
+        destroy-on-close
+        :close-on-click-modal="false"
+        align-center>
+      <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="参数key" prop="key" class="wd300">
+          <el-input
+              v-model="state.ruleForm.key"
+              placeholder="配置参数的key"
+              clearable
+              class="w100">
+          </el-input>
+        </el-form-item>
+        <el-form-item label="值类型" prop="valueType" class="wd300">
+          <el-input
+              v-model="state.ruleForm.valueType"
+              placeholder="值类型"
+              clearable
+              class="w100">
+          </el-input>
+        </el-form-item>
+
+        <el-form-item label="状态" prop="status" class="wd300">
+          <ext-boolean v-model="state.ruleForm.status"></ext-boolean>
+          <!--          <el-input
+                        v-model="state.ruleForm.status"
+                        placeholder="状态:0-无效,1-有效"
+                        clearable
+                        class="w100">
+                    </el-input>-->
+        </el-form-item>
+
+        <el-form-item label="配置值" prop="value" class="w100">
+          <el-input
+              v-model="state.ruleForm.value"
+              placeholder="配置的参数值"
+              clearable
+              type="textarea"
+              :rows="5"
+              class="w100">
+          </el-input>
+        </el-form-item>
+
+
+        <el-form-item label="备注" prop="remark" class="w100">
+          <el-input
+              v-model="state.ruleForm.remark"
+              placeholder="备注"
+              clearable
+              type="textarea"
+              :rows="5"
+              maxlength="500"
+              show-word-limit
+              class="w100">
+          </el-input>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+				<span 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>
+				</span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="ConfigDialog">
+import {defineAsyncComponent, reactive, onMounted, ref} from 'vue';
+import {Msg} from "/@/utils/message";
+import {$body, $get} from "/@/utils/request";
+import u from '/@/utils/u'
+import ExtBoolean from "/@/components/form/ExtBoolean.vue";
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+const formRef = ref();
+//定义初始变量,重置使用
+const initState = () => ({
+  ruleForm: {
+    id: 0
+  },
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+    type: '',
+    title: '',
+    submitTxt: '',
+  },
+  rules: {
+    key:[u.validator.required],
+    value:[u.validator.required],
+    valueType:[u.validator.required],
+  },
+})
+
+// 定义变量内容
+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);
+  } else {
+    state.ruleForm = Object.assign(state.ruleForm, row);
+  }
+};
+// 关闭弹窗
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState())
+};
+// 取消
+const onCancel = () => {
+  onClose();
+};
+// 提交
+const onSubmit = () => {
+  formRef.value.validate((v: boolean) => {
+    if (v) {
+      state.btnLoading = true;
+      const url = state.ruleForm.id > 0 ? "config/modify" : "config/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: number) => {
+  $get(`config/detail/${id}`).then((res: any) => {
+    state.ruleForm = res;
+  })
+}
+
+// 暴露变量
+defineExpose({
+  open
+});
+
+
+</script>

+ 231 - 0
admin-web/src/views/admin/config/index.vue

@@ -0,0 +1,231 @@
+<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">
+      <ext-query-form
+          class="page-search"
+          ref="queryRef"
+          v-model="state.formQuery"
+          :columns="state.columns"
+          :import-config="state.importConfig"
+          :export-config="state.exportConfig"
+          @on-change="loadData(true)"
+          @imported="loadData(true)">
+        <!--  <template #extraQuery></template>
+          <template #extraLeft></template>
+          <template #extraRight></template>-->
+        <template #extQuery>
+          <el-button v-auth="'config.add'" size="default" plain type="success" class="ml10" @click="handleRowClick('add',null)">
+            <SvgIcon name="ele-FolderAdd"/>
+            新增
+          </el-button>
+        </template>
+      </ext-query-form>
+
+      <el-table
+          border
+          stripe="stripe"
+          :height="state.tableData.height"
+          highlight-current-row
+          current-row-key="id"
+          row-key="id"
+          @on-row-click="handleRowClick('view',$event)"
+          :data="state.tableData.data"
+          v-loading="state.tableData.loading">
+        <template #empty>
+          <el-empty></el-empty>
+        </template>
+        <el-table-column
+            v-for="field in state.columns"
+            :key="field.prop"
+            :type="field.type"
+            :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==='name'">
+              <div class="cursor-pointer" style="color:var(--el-color-primary-light-1)"
+                   @click="handleRowClick('view', row)">
+                {{ row[field.prop] }}
+              </div>
+            </template>
+            <template v-else-if="field.prop==='type'">
+              <ext-d-label type="Object.type" :model-value="row[field.prop]"></ext-d-label>
+            </template>
+            <template v-else-if="['prepayMoney','amount','amountReceivable','amountReceived','cardBalance','coinMoney','discountAmount','discountMoney'].includes(field.prop)">
+              {{ u.fmt.fmtMoney(row[field.prop]) }}
+            </template>
+            <template v-else-if="field.prop==='idleRemainTime'||field.prop==='operationRemainTime'">
+              {{ u.fmt.fmtDuration(row[field.prop]) }}
+            </template>
+            <template v-else-if="['createTime','updateTime'].includes(field.prop)">
+              {{ u.fmt.fmtDateTime(row[field.prop]) }}
+            </template>
+            <template v-else-if="field.prop==='action'">
+              <el-button v-auth="'config.modify'" type="warning" size="small" text @click="handleRowClick('edit',row)"> 编辑</el-button>
+              <el-button v-auth="'config.remove'" type="danger" size="small" text @click="handleRowDelete(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>
+  <ConfigDialog ref="configDialogRef" @refresh="loadData(true)"/>
+</template>
+
+<script setup lang="ts" name="ConfigList">
+import {defineAsyncComponent, reactive, onMounted, onBeforeMount, ref, getCurrentInstance, nextTick, onBeforeUnmount} from 'vue';
+import {$body, $get} from "/@/utils/request";
+import u from '/@/utils/u'
+import {Msg} from "/@/utils/message";
+import {Session} from "/@/utils/storage";
+
+
+import ExtPage from '/@/components/form/ExtPage.vue'
+import ExtQueryForm from "/@/components/form/ExtAdminQueryForm.vue";
+
+import mittBus from '/@/utils/mitt';
+
+import {ElButton} from 'element-plus'
+
+
+const ConfigDialog = defineAsyncComponent(() => import("/@/views/admin/config/dialog.vue"));
+
+//定义引用
+const queryRef = ref();
+const configDialogRef = ref();
+
+//定义变量
+const state = reactive({
+  formQuery: {},
+  pageQuery: {
+    pageNum: 1,
+    pageSize: 1024,
+    total: 0
+  },
+  tableData: {
+    height: 500,
+    data: [] as Array<any>,
+    loading: false
+  },
+  importConfig: {},
+  exportConfig: {},
+  columns: [
+    {label: '配置key', width: 180, prop: 'key', query: true, type: 'text', resizable: true},
+    {label: '值类型', width: 180, prop: 'valueType', query: true, type: 'text', resizable: true},
+    {label: '状态', prop: 'status', sortable: 'custom', align: 'center', query: true, type: 'dict', conf: {dict: 'Config.status'}},
+    {label: '备注', width: 180, prop: 'remark', query: true, type: 'text', resizable: true},
+    {label: '创建时间', width: 180, prop: 'createTime', query: false, sortable: 'custom', type: 'datetime', resizable: true, conf: {format: (val: any) => u.fmt.fmtDate(val)}},
+    {label: '更新时间', width: 180, prop: 'updateTime', query: false, sortable: 'custom', type: 'datetime', resizable: true, conf: {format: (val: any) => u.fmt.fmtDate(val)}},
+    {
+      label: '操作', prop: 'action', type: 'render', 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 - 220
+  })
+
+});
+
+onBeforeUnmount(() => {
+})
+
+
+//region 方法区
+// 初始化表格数据
+const loadData = (refresh: boolean = false) => {
+  if (refresh) {
+    state.pageQuery.pageNum = 1;
+  }
+  state.tableData.loading = true;
+  $body(`/config/list`, {...state.formQuery, ...state.pageQuery}).then((res: any) => {
+    let {list, count} = res;
+    state.tableData.data = list;
+    state.pageQuery.total = count;
+    state.tableData.loading = false;
+  }).catch(e => {
+    console.error(e)
+    state.tableData.loading = false;
+  })
+};
+
+// 打开详情页弹窗
+const handleRowClick = (type: string, row: any) => {
+  configDialogRef.value.open(type, row);
+};
+// 删除点击
+const handleRowDelete = (row: any) => {
+  Msg.confirm(`此操作将禁用配置项,是否继续?`).then(() => {
+    $get(`/config/remove/${row.id}`).then(() => {
+      Msg.message("操作成功", 'success')
+    }).catch(() => {
+      Msg.message("操作失败", 'error')
+    })
+  });
+};
+
+//endregion
+
+
+// 暴露变量
+// defineExpose({
+//     loadData,
+// });
+</script>

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

@@ -189,7 +189,6 @@ const onCancel = () => {
 // 提交
 const onSubmit = () => {
   formRef.value.validate((valid, fields) => {
-    // //console.log('basic checkForm!', valid,fields)
     if (valid) {
       state.btnLoading = true;
       const url = state.ruleForm.id > 0 ? "admin-user/modify" : "admin-user/add"

+ 103 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/ConfigController.java

@@ -0,0 +1,103 @@
+package com.kym.admin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.kym.common.R;
+import com.kym.common.annotation.SysLog;
+import com.kym.common.controller.IController;
+import com.kym.entity.Config;
+import com.kym.entity.queryParams.ConfigQueryParam;
+import com.kym.service.ConfigService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+/**
+ * 配置表接口
+ *
+ * @author yaop
+ * @since 2025-06-13 T19:17:14.374702500
+ */
+@RestController
+@RequestMapping("config")
+public class ConfigController extends IController {
+
+    @Resource
+    private ConfigService configService;
+
+
+    /**
+     * 配置表新增接口
+     *
+     * @param config 新增配置表
+     * @return Res
+     */
+    @SysLog("新增配置")
+    @SaCheckPermission("config.add")
+    @PostMapping("add")
+    public R<?> add(@Valid @RequestBody Config config) {
+//other params checked
+        return resp(() -> configService.add(config));
+    }
+
+    /**
+     * 配置表编辑
+     *
+     * @return Res
+     */
+    @SysLog("配置表更新")
+    @SaCheckPermission("config.modify")
+    @PostMapping("modify")
+    public R<?> modify(@Valid @RequestBody Config config) {
+        return resp((t) -> configService.modify(config));
+    }
+
+
+    /**
+     * 配置表查询接口
+     *
+     * @param query 条件构造对象
+     * @return Res
+     * @since 2025-06-13T19:17:14.374702500
+     */
+    @SysLog("配置表列表")
+    @SaCheckPermission("config.list")
+    @PostMapping("list")
+    public R<?> list(@RequestBody ConfigQueryParam query) {
+        return resp(() -> configService.list(query));
+    }
+
+
+    /**
+     * 配置表查询详情接口
+     *
+     * @return Res
+     * @since 2025-06-13T19:17:14.374702500
+     */
+    @SysLog("配置表列表")
+    @SaCheckPermission("config.list")
+    @PostMapping("detail/{id}")
+    public R<?> detail(@PathVariable long id) {
+        return resp(() -> configService.detail(id));
+    }
+
+
+    /**
+     * 配置表删除接口
+     *
+     * @return Res
+     * @since 2025-06-13T19:17:14.374702500
+     */
+    @SysLog("配置表删除")
+    @SaCheckPermission("remove.list")
+    @PostMapping("remove/{id}")
+    public R<?> remove(@PathVariable long id) {
+        return resp((t) -> configService.remove(id));
+    }
+
+
+}

+ 8 - 0
car-wash-admin/src/main/java/com/kym/admin/jobs/StatJob.java

@@ -1,6 +1,7 @@
 package com.kym.admin.jobs;
 
 import cn.hutool.core.date.DateUtil;
+import com.kym.common.utils.ConfigUtil;
 import com.kym.entity.DailyStat;
 import com.kym.entity.MonthStat;
 import com.kym.service.*;
@@ -43,6 +44,10 @@ public class StatJob {
      */
     @Scheduled(cron = "0 30 0 * * ?")
     public void generateDailyStat() {
+        if (!ConfigUtil.isNodeAble()) {
+            //集群环境下仅主节点执行日终统计跑批任务
+            return;
+        }
         log.info("执行站点日统计定时任务-开始");
         LocalDate yesterday = LocalDate.now().minusDays(1);
 
@@ -81,6 +86,9 @@ public class StatJob {
      */
     @Scheduled(cron = "0 0 15 5 * ?")
     private void generateMonthStat() {
+        if (!ConfigUtil.isNodeAble()) {
+            return;
+        }
         log.info("执行站点月统计定时任务-开始");
         var statMonth = LocalDate.now().minusMonths(1);
         var amountMap = washOrderService.sumMonthAmount(statMonth);

+ 97 - 0
car-wash-common/src/main/java/com/kym/common/utils/ConfigUtil.java

@@ -0,0 +1,97 @@
+package com.kym.common.utils;
+
+import cn.hutool.json.JSONUtil;
+import com.kym.entity.Config;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 配置工具类,应用节点内存持有
+ */
+public class ConfigUtil {
+
+    private static final List<Config> configs = new CopyOnWriteArrayList<>();
+
+    /**
+     * 初始化内存配置
+     *
+     * @return
+     */
+    public static void initConfig(List<Config> configList) {
+        configs.clear();
+        configList.removeIf(k -> !k.isStatus());
+        configs.addAll(configList);
+    }
+
+    /**
+     * 获取内存配置
+     *
+     * @param key
+     * @return
+     */
+    public static <T> T getConfig(String key, Class<T> resultType, T defaultValue) {
+        Optional<Config> cfg = configs.stream().filter(k -> k.getKey().equals(key)).findFirst();
+        if (cfg.isPresent()) {
+            return JSONUtil.toBean(cfg.get().getValue(), resultType);
+        } else {
+            return defaultValue;
+        }
+    }
+
+    /**
+     * 获取内存配置
+     *
+     * @param key
+     * @return
+     */
+    public static <T> T getConfig(String key, Class<T> resultType) {
+        Optional<Config> cfg = configs.stream().filter(k -> k.getKey().equals(key)).findFirst();
+        return cfg.map(config -> JSONUtil.toBean(config.getValue(), resultType)).orElse(null);
+    }
+
+    /**
+     * 获取内存配置
+     *
+     * @param key
+     * @return
+     */
+    public static String getConfig(String key) {
+        Config cfg = configs.stream().filter(k -> k.getKey().equals(key)).findFirst().orElse(new Config());
+        return cfg.getValue();
+    }
+
+    /**
+     * 更新内存配置
+     *
+     * @param config
+     */
+    public static void updateConfig(Config config) {
+        configs.removeIf(k -> k.getKey().equals(config.getKey()));
+        if (config.isStatus()) {
+            configs.add(config);
+        }
+    }
+
+
+    /**
+     * 获取当前应用节点标识 启动参数加上-Dnode=node1进行 区分
+     *
+     * @return
+     */
+    public static String getNode() {
+        return System.getProperty("node", "node1");
+    }
+
+
+    /**
+     * 判断当前节点是否为主观配置的主应用节点(集群环境下需要人工选举主节点)
+     * @return
+     */
+    public static boolean isNodeAble() {
+        return System.getProperty("node", "node1").equals(getConfig("master.node"));
+    }
+
+
+}

+ 54 - 0
car-wash-entity/src/main/java/com/kym/entity/Config.java

@@ -0,0 +1,54 @@
+package com.kym.entity;
+
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import jakarta.validation.constraints.Max;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.experimental.Accessors;
+import org.hibernate.validator.constraints.Length;
+
+import java.io.Serializable;
+
+/**
+ * 配置表 (存放公共配置,如集群模式下的主节点、系统通知模板)
+ *
+ * @author yaop
+ * @date 2025-06-11T20:59:23.189438600
+ */
+@Getter
+@Setter
+@TableName("t_config")
+@Accessors(chain = true)
+public class Config extends BaseEntity implements Serializable {
+    private long id;
+
+    private long companyId;
+    /**
+     * 配置key  唯一
+     */
+    @Length(max = 128)
+    private String key;
+    /**
+     * 配置值
+     */
+    @Length(max = 512)
+    private String value;
+
+    /**
+     * 值类型
+     */
+    @Length(max = 128)
+    private String valueType;
+    /**
+     * 状态:0-无效,1-有效
+     */
+    private boolean status;
+    /**
+     * 备注
+     */
+    @Max(value = 512)
+    private String remark;
+
+
+}

+ 31 - 0
car-wash-entity/src/main/java/com/kym/entity/queryParams/ConfigQueryParam.java

@@ -0,0 +1,31 @@
+package com.kym.entity.queryParams;
+
+import com.kym.entity.common.PageParams;
+import jakarta.validation.constraints.Max;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+@Data
+public class ConfigQueryParam extends PageParams {
+    /**
+     * 配置key  唯一
+     */
+    private String key;
+    /**
+     * 配置值
+     */
+    private String value;
+
+    /**
+     * 值类型
+     */
+    private String valueType;
+    /**
+     * 状态:0-无效,1-有效
+     */
+    private Boolean status;
+    /**
+     * 备注
+     */
+    private String remark;
+}

+ 13 - 0
car-wash-mapper/src/main/java/com/kym/mapper/ConfigMapper.java

@@ -0,0 +1,13 @@
+package com.kym.mapper;
+
+import com.kym.entity.Config;
+import com.kym.mapper.mybatisplus.MyBaseMapper;
+
+/**
+ * <p>
+ * 配置表 Mapper 接口
+ * </p>
+ */
+public interface ConfigMapper extends  MyBaseMapper<Config> {
+
+}

+ 20 - 0
car-wash-service/src/main/java/com/kym/service/ConfigService.java

@@ -0,0 +1,20 @@
+package com.kym.service;
+
+import com.kym.entity.Config;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.ConfigQueryParam;
+import com.kym.service.mybatisplus.MyBaseService;
+
+public interface ConfigService  extends MyBaseService<Config> {
+    Number add(Config config);
+
+    void modify(Config config);
+
+    PageBean<Config> list(ConfigQueryParam query);
+
+    Config detail(long id);
+
+    void remove(long id);
+
+    void initConfig();
+}

+ 85 - 0
car-wash-service/src/main/java/com/kym/service/cache/PubSubService.java

@@ -0,0 +1,85 @@
+package com.kym.service.cache;
+
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.kym.common.utils.ConfigUtil;
+import com.kym.entity.Config;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.function.BiConsumer;
+
+/**
+ * redis 发布订阅工具(集群节点之间同步数据使用)
+ */
+@Service
+@Slf4j
+public class PubSubService {
+
+    public static final String CHANNEL = "channel";
+
+    @Resource
+    private StringRedisTemplate redisTemplate;
+
+
+    /**
+     * 发布消息
+     *
+     * @param channel 通信信道
+     * @param message
+     */
+    public void publish(String channel, String message) {
+        redisTemplate.convertAndSend(channel, message);
+    }
+
+
+    /**
+     * 订阅消息
+     *
+     * @param channel
+     * @param handler
+     */
+    public void subscribe(String channel, BiConsumer<String, String> handler) {
+        redisTemplate.getConnectionFactory().getConnection().subscribe(new MessageListener() {
+            @Override
+            public void onMessage(Message message, byte[] pattern) {
+                var receiveChannel = new String(message.getChannel());
+                var receiveMsg = new String(message.getBody());
+                handler.accept(receiveChannel, receiveMsg);
+                log.info("receive redis channel subscribe channel:{},\nmessage:{}", receiveChannel, receiveMsg);
+            }
+        }, channel.getBytes(StandardCharsets.UTF_8));
+    }
+
+
+    @PostConstruct
+    public void init() {
+        //构造监听
+        new Thread(() -> {
+            subscribe(CHANNEL, new BiConsumer<String, String>() {
+                @Override
+                public void accept(String channel, String jsonBody) {
+                    log.debug("deal with subscribe!!!");
+                    JSONObject entries = JSONUtil.parseObj(jsonBody);
+                    String configNode = entries.getStr("node");
+                    if (configNode.equals(ConfigUtil.getNode())) {
+                        log.info("自节点订阅无须处理。");
+                        return;
+                    }
+                    boolean configPresent = entries.containsKey("config");
+                    if (configPresent) {
+                        Config config = JSONUtil.toBean(entries.getJSONObject("config"), Config.class);
+                        ConfigUtil.updateConfig(config);
+                    }
+                }
+            });
+        }).start();
+    }
+}

+ 107 - 0
car-wash-service/src/main/java/com/kym/service/impl/ConfigServiceImpl.java

@@ -0,0 +1,107 @@
+package com.kym.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.github.pagehelper.PageHelper;
+import com.kym.common.utils.CommUtil;
+import com.kym.common.utils.ConfigUtil;
+import com.kym.entity.Config;
+import com.kym.entity.common.PageBean;
+import com.kym.entity.queryParams.ConfigQueryParam;
+import com.kym.mapper.ConfigMapper;
+import com.kym.service.ConfigService;
+import com.kym.service.cache.PubSubService;
+import com.kym.service.mybatisplus.MyBaseServiceImpl;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+
+/**
+ * 配置表业务类
+ *
+ * @author yaop
+ * @date 2025-06-15T19:17:14.374702500
+ */
+@Service("configService")
+public class ConfigServiceImpl extends MyBaseServiceImpl<ConfigMapper, Config> implements ConfigService {
+
+    @Resource
+    private PubSubService pubSubService;
+
+    @Override
+    public Number add(Config config) {
+//        config.setCompanyId(1)
+        config.setStatus(true);
+        var rows =  insertIgnore(config);
+
+        if(rows>0){
+            pubSubService.publish(PubSubService.CHANNEL,
+                    JSONUtil.toJsonStr(
+                            Map.of("node", ConfigUtil.getNode(), "config", config)
+                    ));
+        }
+
+        return rows;
+    }
+
+    @Override
+    public void modify(Config config) {
+        UpdateWrapper<Config> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.set("value", config.getValue());
+        updateWrapper.set("remark", config.getRemark());
+        updateWrapper.set("valueType", config.getValueType());
+        updateWrapper.eq("key", config.getKey());
+        update(updateWrapper);
+
+        pubSubService.publish(PubSubService.CHANNEL,
+                JSONUtil.toJsonStr(
+                        Map.of("node", ConfigUtil.getNode(), "config", config)
+                ));
+    }
+
+    @Override
+    public PageBean<Config> list(ConfigQueryParam query) {
+        PageHelper.startPage(query.getPageNum(), query.getPageSize());
+        var list = lambdaQuery()
+                .like(CommUtil.isNotEmptyAndNull(query.getKey()), Config::getKey, query.getKey())
+                .like(CommUtil.isNotEmptyAndNull(query.getValue()), Config::getValue, query.getValue())
+                .like(CommUtil.isNotEmptyAndNull(query.getValueType()), Config::getValueType, query.getValueType())
+                .like(CommUtil.isNotEmptyAndNull(query.getRemark()), Config::getRemark, query.getRemark())
+                .like(CommUtil.isNotEmptyAndNull(query.getStatus()), Config::isStatus, query.getStatus())
+                .list();
+        return new PageBean<>(list);
+    }
+
+    @Override
+    public Config detail(long id) {
+        return getById(id);
+    }
+
+    @Override
+    public void remove(long id) {
+        Config cfg = getById(id);
+        cfg.setStatus(false);
+        UpdateWrapper<Config> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.set("status", false);
+        updateWrapper.eq("id", id);
+        update(updateWrapper);
+
+        pubSubService.publish(PubSubService.CHANNEL,
+                JSONUtil.toJsonStr(
+                        Map.of("node", ConfigUtil.getNode(), "config", cfg)
+                ));
+
+    }
+
+    @PostConstruct
+    @Override
+    public void initConfig() {
+        var list = lambdaQuery().list();
+        ConfigUtil.initConfig(list);
+    }
+
+
+}

+ 0 - 0
zabbix/docker-compose.yaml


+ 13 - 0
zabbix/readme.md

@@ -0,0 +1,13 @@
+zabbix 监控说明,服务端采用docker-compose服务模式安装,客户端采用yum工具安装
+
+### 组件清单
+
+#### 1. zabbix-server
+
+#### 2. zabbix-web
+
+#### 3. zabbix-mysql
+
+#### 4.zabbix-agent(监控客户端)
+
+仅本应用需要安装在监控目标宿主机上