消息推送初始化
diff --git a/backend/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java b/backend/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
new file mode 100644
index 0000000..1378bcf
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
@@ -0,0 +1,103 @@
+package com.supwisdom.dlpay.api.bean;
+
+public class KafkaXgMessage {
+
+  private String title;//提醒标题
+  private String content;//提醒消息内容
+  private String gids;//gid发送列表,逗号隔开
+  private Boolean alltarget;//是否平台全发送
+  private String platform;//android,ios,all
+  private int type;//TYPE_NOTIFICATION = 1,TYPE_MESSAGE = 2
+
+  private String expiretime;//过期时间
+  private Boolean callback;//是否回调返回
+  private int retries;//最多重试次数
+  private String custom; //扩展参数,原样传递
+
+  //TODO: 下面的参数在epaymessager中已不用,要赋值请放入custom的json串中
+//  private String url;//跳转url
+//  private String msg_type;//提醒类型 字典 epay_pos_scan_code_pay、
+//  private String refno;//流水参考号
+//  private String amount;//消费金额,元,小数点后两位
+
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public String getGids() {
+    return gids;
+  }
+
+  public void setGids(String gids) {
+    this.gids = gids;
+  }
+
+  public Boolean getAlltarget() {
+    return alltarget;
+  }
+
+  public void setAlltarget(Boolean alltarget) {
+    this.alltarget = alltarget;
+  }
+
+  public String getPlatform() {
+    return platform;
+  }
+
+  public void setPlatform(String platform) {
+    this.platform = platform;
+  }
+
+  public int getType() {
+    return type;
+  }
+
+  public void setType(int type) {
+    this.type = type;
+  }
+
+  public String getExpiretime() {
+    return expiretime;
+  }
+
+  public void setExpiretime(String expiretime) {
+    this.expiretime = expiretime;
+  }
+
+  public Boolean getCallback() {
+    return callback;
+  }
+
+  public void setCallback(Boolean callback) {
+    this.callback = callback;
+  }
+
+  public int getRetries() {
+    return retries;
+  }
+
+  public void setRetries(int retries) {
+    this.retries = retries;
+  }
+
+  public String getCustom() {
+    return custom;
+  }
+
+  public void setCustom(String custom) {
+    this.custom = custom;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/MsgTemplateRepositoryImpl.java b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/MsgTemplateRepositoryImpl.java
new file mode 100644
index 0000000..f6efb5b
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/dao/impl/MsgTemplateRepositoryImpl.java
@@ -0,0 +1,37 @@
+package com.supwisdom.dlpay.portal.dao.impl;
+
+import com.supwisdom.dlpay.framework.jpa.BaseRepository;
+import com.supwisdom.dlpay.framework.jpa.Finder;
+import com.supwisdom.dlpay.framework.jpa.page.Pagination;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean;
+import com.supwisdom.dlpay.portal.dao.MsgTemplateRepository;
+import com.supwisdom.dlpay.portal.domain.TBMsgTemplate;
+import org.hibernate.transform.Transformers;
+import org.jetbrains.annotations.NotNull;
+
+public class MsgTemplateRepositoryImpl extends BaseRepository implements MsgTemplateRepository {
+  @NotNull
+  @Override
+  public Pagination getMsgTemplateList(@NotNull MsgTemplateSearchBean bean) {
+    String title = bean.getTitle();
+    String content = bean.getContent();
+    int pageno = bean.getPageno();
+    int pagesize = bean.getPagesize();
+    StringBuilder sql = new StringBuilder("select * from tb_msg_template where 1=1");
+    if (!StringUtil.isEmpty(title)) {
+      sql.append(" and title like :title");
+    }
+    if (!StringUtil.isEmpty(content)) {
+      sql.append(" and content like :content");
+    }
+    Finder f = Finder.create(sql.toString());
+    if (!StringUtil.isEmpty(title)) {
+      f.setParameter("title", "%" + title.trim() + "%");
+    }
+    if (!StringUtil.isEmpty(content)) {
+      f.setParameter("content", "%" + content.trim() + "%");
+    }
+    return findNative(f, Transformers.aliasToBean(TBMsgTemplate.class), pageno, pagesize);
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java
index 2679855..729cc71 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBAnnex.java
@@ -5,7 +5,9 @@
 import javax.persistence.*;
 
 @Entity
-@Table(name = "tb_annex")
+@Table(name = "tb_annex",indexes = {
+    @Index(name = "annex_idx",columnList = "fbid")
+})
 public class TBAnnex {
   @Id
   @GenericGenerator(name = "idGenerator", strategy = "uuid")
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java
index 951b947..00db202 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBFeedback.java
@@ -6,7 +6,9 @@
 import java.util.List;
 
 @Entity
-@Table(name = "tb_feedback")
+@Table(name = "tb_feedback",
+        indexes = {@Index(name = "feedback_idx1",columnList = "userid"),
+        @Index(name = "feedback_idx2",columnList = "fbtime,replystatus")})
 public class TBFeedback {
   @Id
   @GenericGenerator(name = "idGenerator", strategy = "uuid")
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBMsgTemplate.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBMsgTemplate.java
new file mode 100644
index 0000000..a15da35
--- /dev/null
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBMsgTemplate.java
@@ -0,0 +1,67 @@
+package com.supwisdom.dlpay.portal.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+
+@Entity
+@Table(name = "tb_msg_template")
+public class TBMsgTemplate {
+  @Id
+  @GenericGenerator(name = "idGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "idGenerator")
+  @Column(name = "id", nullable = false, length = 32)
+  private String id;
+
+  @Column(name = "title", nullable = false, length = 30)
+  private String title;
+
+  @Column(name = "content", nullable = false, length = 200)
+  private String content;
+
+  @Column(name = "updatetime", length = 14)
+  private String updatetime;
+
+  @Column(name = "operid", length = 32)
+  private String operid;
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public void setContent(String content) {
+    this.content = content;
+  }
+
+  public String getUpdatetime() {
+    return updatetime;
+  }
+
+  public void setUpdatetime(String updatetime) {
+    this.updatetime = updatetime;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+}
diff --git a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java
index 4a65054..5fe72b9 100644
--- a/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java
+++ b/backend/src/main/java/com/supwisdom/dlpay/portal/domain/TBReply.java
@@ -5,7 +5,9 @@
 import javax.persistence.*;
 
 @Entity
-@Table(name = "tb_reply")
+@Table(name = "tb_reply",indexes = {
+    @Index(name = "reply_idx",columnList = "fbid")
+})
 public class TBReply {
   @Id
   @GenericGenerator(name = "idGenerator", strategy = "uuid")
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
new file mode 100644
index 0000000..63591dc
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
@@ -0,0 +1,38 @@
+package com.supwisdom.dlpay.api
+
+import mu.KotlinLogging
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.scheduling.annotation.AsyncConfigurer
+import org.springframework.scheduling.annotation.EnableAsync
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
+import java.lang.reflect.Method
+import java.util.concurrent.Executor
+
+@Configuration
+@EnableAsync
+class SpringAsyncConfig : AsyncConfigurer {
+
+    @Bean(name = ["kafkaSendMessageAsyncTask"])
+    fun kafkaSendMessageAsyncTaskExecutor(): Executor {
+        return ThreadPoolTaskExecutor().apply {
+            corePoolSize = 20
+            maxPoolSize = 100
+            setWaitForTasksToCompleteOnShutdown(true)
+        }
+    }
+
+    override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler? {
+        return MyAsyncUncaughtExceptionHandler()
+    }
+}
+
+class MyAsyncUncaughtExceptionHandler : AsyncUncaughtExceptionHandler {
+    private val logger = KotlinLogging.logger { }
+    override fun handleUncaughtException(ex: Throwable, method: Method, vararg params: Any?) {
+        logger.error { "Async Task execute error: <${method.name}>, exception <${ex.message}>" }
+    }
+}
+
+
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
new file mode 100644
index 0000000..7e35c90
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
@@ -0,0 +1,77 @@
+package com.supwisdom.dlpay.api.service.impl
+
+import com.google.gson.Gson
+import com.supwisdom.dlpay.api.bean.KafkaXgMessage
+import com.supwisdom.dlpay.api.service.KafkaSendMsgService
+import com.supwisdom.dlpay.framework.util.DateUtil
+import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.mobile.dao.MsgDao
+import com.supwisdom.dlpay.mobile.domain.TBMsg
+import com.supwisdom.dlpay.mobile.service.MobileApiService
+import mu.KotlinLogging
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.kafka.core.KafkaTemplate
+import org.springframework.scheduling.annotation.Async
+import org.springframework.stereotype.Service
+
+@Service
+class KafkaSendMsgServiceImpl : KafkaSendMsgService {
+    val logger = KotlinLogging.logger { }
+    @Autowired
+    private lateinit var kafkaTemplate: KafkaTemplate<String, String>
+    @Autowired
+    private lateinit var msgDao: MsgDao
+    @Autowired
+    private lateinit var mobileApiService: MobileApiService
+
+    val gson = Gson()
+
+    val topic = "jpush-messages"
+
+    @Async("kafkaSendMessageAsyncTask")
+    override fun sendJpushMessage(userid: String, title: String, content: String, refno: String, extras: MutableMap<String, String>, tenantId: String?) {
+        val musers = mobileApiService.findByUseridAndStatus(userid, TradeDict.STATUS_NORMAL)
+        var msg = TBMsg().apply {
+            this.content = content
+            this.title = title
+            this.refno = refno
+            this.userid = userid
+            this.lastupdate = DateUtil.getNow()
+            this.refno = refno
+            this.extras = gson.toJson(extras)
+        }
+        if (musers == null || musers.isEmpty()) {
+            msg.pushresult = "没有手机用户,消息未推送"
+            msgDao.save(msg)
+            return
+        }
+        msg = msgDao.save(msg)
+        var uids = ""
+        musers.forEach {
+            uids += "${it.uid},"
+            var platform = "android"
+            if ("ios".equals(it.lastloginplatform, true)) {
+                platform = "ios"
+            }
+            val message = KafkaXgMessage()
+            message.alltarget = false
+            message.callback = true
+            message.content = msg.content
+            message.title = msg.title
+            message.platform = platform
+            message.retries = 3
+            extras["refno"] = refno
+            message.custom = gson.toJson(extras)
+            message.expiretime = DateUtil.getNewTime(DateUtil.getNow(), 300)
+            message.gids = it.uid
+            if (it.lastloginplatform.isNullOrEmpty()) {
+                message.platform = "all"
+                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
+            } else {
+                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
+            }
+        }
+        msg.pusheduids = uids
+        msgDao.save(msg)
+    }
+}
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
new file mode 100644
index 0000000..66d3f20
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
@@ -0,0 +1,35 @@
+package com.supwisdom.dlpay.api.service
+
+import com.supwisdom.dlpay.mobile.dao.MsgDao
+import mu.KotlinLogging
+import org.apache.kafka.clients.consumer.ConsumerRecord
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.kafka.annotation.KafkaListener
+import org.springframework.stereotype.Component
+
+interface KafkaSendMsgService {
+    fun sendJpushMessage(userid: String, title: String, content: String, refno: String, extras: MutableMap<String, String>, tenantId: String?)
+}
+
+@Component
+class KafkaMsgListener {
+    val logger = KotlinLogging.logger { }
+    @Autowired
+    private lateinit var msgDao: MsgDao
+
+    @KafkaListener(topics = ["jpush-messages-result"],
+            autoStartup = "\${spring.kafka.listen.auto.start:true}",
+            groupId = "0")
+    fun listen(record: ConsumerRecord<String, String>) {
+        logger.debug { "${record.key()},${record.value()}" }
+        if (!record.key().isNullOrEmpty()) {
+            val opt = msgDao.findById(record.key())
+            if (opt.isPresent) {
+                val msg = opt.get()
+                msg.pushresult = record.value()
+                msgDao.save(msg)
+                return
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
index 50f52a3..089c1a5 100644
--- a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/PortalApi.kt
@@ -7,9 +7,14 @@
 import com.supwisdom.dlpay.framework.service.OperatorDetailService
 import com.supwisdom.dlpay.framework.service.SystemUtilService
 import com.supwisdom.dlpay.framework.util.StringUtil
+import com.supwisdom.dlpay.mobile.domain.TBMsg
 import com.supwisdom.dlpay.portal.bean.FeedbackSearchBean
+import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean
+import com.supwisdom.dlpay.portal.bean.SendMsgBean
+import com.supwisdom.dlpay.portal.domain.TBMsgTemplate
 import com.supwisdom.dlpay.portal.domain.TBReply
 import com.supwisdom.dlpay.portal.service.FeedbackService
+import com.supwisdom.dlpay.portal.service.MsgService
 import com.supwisdom.dlpay.portal.util.PortalConstant
 import mu.KotlinLogging
 import org.jose4j.jwt.ReservedClaimNames
@@ -32,6 +37,8 @@
     lateinit var apiJwtRepository: ApiJwtRepository
     @Autowired
     lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    lateinit var msgService: MsgService
     val logger = KotlinLogging.logger { }
 
     @RequestMapping("/test")
@@ -40,7 +47,7 @@
     }
 
     @RequestMapping("user/logout")
-    fun logout(@RequestHeader("Authorization") auth: String?): ResponseEntity<Any>{
+    fun logout(@RequestHeader("Authorization") auth: String?): ResponseEntity<Any> {
         if (auth == null) {
             return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()
         }
@@ -111,8 +118,8 @@
         }
     }
 
-    @RequestMapping(value= ["/feedback/reply/save"],method = [RequestMethod.POST])
-    fun saveFeedbackReply(@RequestBody reply:TBReply): JsonResult? {
+    @RequestMapping(value = ["/feedback/reply/save"], method = [RequestMethod.POST])
+    fun saveFeedbackReply(@RequestBody reply: TBReply): JsonResult? {
         return try {
             val p = SecurityContextHolder.getContext().authentication
             val oper = operatorDetailService.findByOperid(p.name)
@@ -128,4 +135,43 @@
             JsonResult.error("保存留言回复异常")
         }
     }
+
+    @RequestMapping("/template/list")
+    fun getMsgTemplateList(bean: MsgTemplateSearchBean): JsonResult? {
+        return try {
+            val page = msgService.getMsgTemplateList(bean)
+            if (page.list == null || page.list.size == 0) {
+                return JsonResult.ok().put("msg", "无数据")
+            }
+            return JsonResult.ok().put("page", page)
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("查询消息模板列表异常")
+        }
+    }
+
+    @RequestMapping(value = ["/template/save"],method = [RequestMethod.POST])
+    fun saveMsgTemplate(@RequestBody template:TBMsgTemplate):JsonResult?{
+        return try {
+            val p = SecurityContextHolder.getContext().authentication
+            val oper = operatorDetailService.findByOperid(p.name)
+            template.operid = oper.operid
+            msgService.saveMsgTemplate(template)
+            return JsonResult.ok()
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error("保存消息推送模板异常")
+        }
+    }
+
+    @RequestMapping(value = ["/template/sendmsg"],method = [RequestMethod.POST])
+    fun sendMsg(@RequestBody bean: SendMsgBean):JsonResult?{
+        return try {
+            msgService.sendMsg(bean)
+            return JsonResult.ok()
+        } catch (e: Exception) {
+            logger.error { e.message }
+            JsonResult.error(e.message)
+        }
+    }
 }
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/MsgTemplateSearchBean.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/MsgTemplateSearchBean.kt
new file mode 100644
index 0000000..0b3ca4a
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/MsgTemplateSearchBean.kt
@@ -0,0 +1,8 @@
+package com.supwisdom.dlpay.portal.bean
+
+class MsgTemplateSearchBean {
+    var title: String = ""
+    var content: String = ""
+    var pageno: Int = 0
+    var pagesize: Int = 10
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/SendMsgBean.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/SendMsgBean.kt
new file mode 100644
index 0000000..de40251
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/bean/SendMsgBean.kt
@@ -0,0 +1,8 @@
+package com.supwisdom.dlpay.portal.bean
+
+class SendMsgBean {
+    var templateid: String = ""
+    var userid: String = ""
+    var refno: String = ""
+    var data: Map<String,String> = HashMap()
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/MsgTemplateDao.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/MsgTemplateDao.kt
new file mode 100644
index 0000000..2c5f093
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/MsgTemplateDao.kt
@@ -0,0 +1,10 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.portal.domain.TBMsgTemplate
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface MsgTemplateDao : JpaRepository<TBMsgTemplate, String>,MsgTemplateRepository {
+
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/MsgTemplateRepository.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/MsgTemplateRepository.kt
new file mode 100644
index 0000000..f6ca359
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/dao/MsgTemplateRepository.kt
@@ -0,0 +1,8 @@
+package com.supwisdom.dlpay.portal.dao
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean
+
+interface MsgTemplateRepository {
+    fun getMsgTemplateList(bean: MsgTemplateSearchBean): Pagination
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/MsgServiceImpl.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/MsgServiceImpl.kt
new file mode 100644
index 0000000..0c59f94
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/Impl/MsgServiceImpl.kt
@@ -0,0 +1,62 @@
+package com.supwisdom.dlpay.portal.service.Impl
+
+import com.supwisdom.dlpay.api.service.KafkaSendMsgService
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.framework.service.SystemUtilService
+import com.supwisdom.dlpay.mobile.dao.MsgDao
+import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean
+import com.supwisdom.dlpay.portal.bean.SendMsgBean
+import com.supwisdom.dlpay.portal.dao.MsgTemplateDao
+import com.supwisdom.dlpay.portal.domain.TBMsgTemplate
+import com.supwisdom.dlpay.portal.service.MsgService
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.stereotype.Service
+import java.lang.RuntimeException
+import java.util.regex.Pattern
+
+@Service
+class MsgServiceImpl : MsgService {
+    @Autowired
+    lateinit var msgTemplateDao: MsgTemplateDao
+    @Autowired
+    lateinit var msgDao: MsgDao
+    @Autowired
+    lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    lateinit var kafkaSendMsgService: KafkaSendMsgService
+
+    override fun getMsgTemplateList(bean: MsgTemplateSearchBean): Pagination {
+        return msgTemplateDao.getMsgTemplateList(bean)
+    }
+
+    override fun saveMsgTemplate(template: TBMsgTemplate) {
+        template.updatetime = systemUtilService.sysdatetime.hostdatetime
+        msgTemplateDao.save(template)
+    }
+
+    override fun sendMsg(bean: SendMsgBean) {
+        val optional = msgTemplateDao.findById(bean.templateid)
+        if (optional.isPresent) {
+            val template = optional.get()
+            var content = template.content
+            val data = bean.data
+            //正则匹配所有${xx}
+            val pattern: Pattern = Pattern.compile("\\$\\{[\\w]*}")
+            val matcher = pattern.matcher(content)
+            while (matcher.find()) {
+                val source = matcher.group()
+                //截取${}括号中的内容
+                val param = source.substring(2, source.length - 1)
+                if (data.containsKey(param)) {
+                    val value = data[param].toString()
+                    content = content.replace(source, value)
+                } else {
+                    throw RuntimeException("未设置参数<${param}>的值")
+                }
+            }
+            kafkaSendMsgService.sendJpushMessage(bean.userid,template.title,content,bean.refno,mutableMapOf(),"")
+        }else {
+            throw RuntimeException("未找到id为:<${bean.templateid}>的模板,请确认模板id")
+        }
+    }
+}
\ No newline at end of file
diff --git a/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt
new file mode 100644
index 0000000..ebec659
--- /dev/null
+++ b/backend/src/main/kotlin/com/supwisdom/dlpay/portal/service/MsgService.kt
@@ -0,0 +1,12 @@
+package com.supwisdom.dlpay.portal.service
+
+import com.supwisdom.dlpay.framework.jpa.page.Pagination
+import com.supwisdom.dlpay.portal.bean.MsgTemplateSearchBean
+import com.supwisdom.dlpay.portal.bean.SendMsgBean
+import com.supwisdom.dlpay.portal.domain.TBMsgTemplate
+
+interface MsgService {
+    fun getMsgTemplateList(bean:MsgTemplateSearchBean):Pagination
+    fun saveMsgTemplate(template:TBMsgTemplate)
+    fun sendMsg(bean: SendMsgBean)
+}
diff --git a/backend/src/test/java/test.java b/backend/src/test/java/test.java
new file mode 100644
index 0000000..1659da6
--- /dev/null
+++ b/backend/src/test/java/test.java
@@ -0,0 +1,32 @@
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class test {
+  public static void main(String[] args) {
+    String sqlFragment = "您的验证码为:${code},订单号:${billno},本次消费:${money}元,请勿泄漏于他人。";
+    Map<String, String> map = new HashMap<>();
+    map.put("code", "23423");
+    map.put("billno", "6116c8e6e0c0464995f4e480473b6a1e");
+    map.put("money", "12.34");
+     Pattern pattern = Pattern.compile("\\$\\{[\\w]*}");
+    Matcher matcher = pattern.matcher(sqlFragment);
+    //循环,字符串中有多少个符合的,就循环多少次
+    while(matcher.find()){
+      //每一个符合正则的字符串
+      String e = matcher.group();
+      //截取出括号中的内容
+      String substring = e.substring(2, e.length()-1);
+      //字符串截取
+      //进行替换
+      String value = "init";
+      if (map.containsKey(substring)) {
+        value = map.get(substring);
+      }
+			sqlFragment = sqlFragment.replace(e, value);
+
+    }
+    System.out.println(sqlFragment);
+  }
+}
diff --git a/frontend/src/api/msg-template.js b/frontend/src/api/msg-template.js
new file mode 100644
index 0000000..3f19fca
--- /dev/null
+++ b/frontend/src/api/msg-template.js
@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+export function getMsgTemplateList(query) {
+  return request({
+    url: '/template/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function saveMsgTemplateList(data) {
+  return request({
+    url: '/template/save',
+    method: 'post',
+    data
+  })
+}
diff --git a/frontend/src/views/pushmsg/index.vue b/frontend/src/views/pushmsg/index.vue
index cd8e058..6e9d286 100644
--- a/frontend/src/views/pushmsg/index.vue
+++ b/frontend/src/views/pushmsg/index.vue
@@ -1,8 +1,243 @@
 <template>
-  <div>消息推送</div>
+  <div class="app-container">
+    <div class="filter-container">
+      <div class="filter-item" style="margin-right:15px">模板标题</div>
+      <el-input
+        v-model="formData.title"
+        placeholder="标题"
+        style="width: 350px;margin-right:50px"
+        class="filter-item"
+      />
+      <div class="filter-item" style="margin-right:15px">模板内容</div>
+      <el-input
+        v-model="formData.content"
+        placeholder="内容"
+        style="width: 350px;margin-right:50px"
+        class="filter-item"
+      />
+      <el-button
+        class="filter-item"
+        type="primary"
+        icon="el-icon-search"
+        @click="handleFilter()"
+      >
+        搜索
+      </el-button>
+    </div>
+    <el-button type="primary" icon="el-icon-circle-plus-outline" @click="addTemplate()">
+      新增模板
+    </el-button>
+    <el-table
+      :key="tableKey"
+      v-loading="listLoading"
+      :data="list"
+      border
+      fit
+      highlight-current-row
+      style="width: 100%;margin-top:10px"
+    >
+      <el-table-column label="模板id" width="150">
+        <template slot-scope="{row}">
+          <span>{{ row.id }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="模板标题" width="150" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.title }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="模板内容" align="center">
+        <template slot-scope="{row}">
+          <span>{{ row.content }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="修改日期" align="center" width="160">
+        <template slot-scope="{row}">
+          <span>{{ dateFormat(row.updatetime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="100">
+        <template slot-scope="{row}">
+          <el-tooltip class="item" effect="dark" content="修改" placement="bottom">
+            <el-button type="primary" icon="el-icon-edit" circle size="mini" @click="updateTemplate(row)" />
+          </el-tooltip>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="formData.pageno"
+      :limit.sync="formData.pagesize"
+      style="margin-top:0;"
+      @pagination="getMsgTemplateList"
+    />
+    <el-dialog
+      :title="title"
+      :visible.sync="templateDialogVisible"
+      width="45%"
+    >
+      <div>
+        <el-form ref="templateForm" :model="templateForm" :rules="rules" label-width="100px">
+          <el-form-item label="模板标题" prop="title" class="form-input-item">
+            <el-input
+              v-model="templateForm.title"
+              maxlength="30"
+              show-word-limit
+              style="width:80%"
+            />
+          </el-form-item>
+          <el-form-item label="模板内容" prop="content">
+            <el-input
+              v-model="templateForm.content"
+              type="textarea"
+              maxlength="200"
+              :rows="8"
+              placeholder="变量格式:${xxx};
+ 示例:您本次食堂消费:${money}元,欢迎下次光临。"
+              show-word-limit
+              style="width:80%"
+            />
+          </el-form-item>
+        </el-form>
+      </div>
+      <div style="text-align:center">
+        <el-button
+          type="primary"
+          @click="saveTemplate('templateForm')"
+        >保存
+        </el-button>
+        <el-button @click="templateDialogVisible = false">取消</el-button>
+      </div>
+    </el-dialog>
+  </div>
 </template>
 <script>
+import {
+  getMsgTemplateList,
+  saveMsgTemplateList
+} from '@/api/msg-template'
+import moment from 'moment'
+import Pagination from '@/components/Pagination'
 export default {
-  name: 'PushMsg'
+  name: 'PushMsg',
+  components: {
+    Pagination
+  },
+  data() {
+    return {
+      formData: {
+        title: '',
+        content: '',
+        pageno: 1,
+        pagesize: 10
+      },
+      listLoading: false,
+      tableKey: 0,
+      list: null,
+      total: 0,
+      title: '',
+      templateDialogVisible: false,
+      templateForm: {
+        id: '',
+        title: '',
+        content: ''
+      },
+      rules: {
+        title: [
+          { required: true, message: '请输入模板标题', trigger: 'blur' }
+        ],
+        content: [
+          { required: true, message: '请输入模板内容', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getMsgTemplateList()
+  },
+  methods: {
+    getMsgTemplateList() {
+      this.listLoading = true
+      getMsgTemplateList(this.formData).then(response => {
+        if (response.page) {
+          this.list = response.page.list
+          this.total = response.page.totalCount
+        } else {
+          this.list = null
+          this.total = 0
+        }
+        this.listLoading = false
+      }).catch(error => {
+        this.$message({
+          message: error.msg || '请求异常',
+          type: 'error'
+        })
+        this.listLoading = false
+      })
+    },
+    handleFilter() {
+      this.formData.pageno = 1
+      this.getMsgTemplateList()
+    },
+    dateFormat(date) {
+      if (date === null) {
+        return ''
+      }
+      return moment(date, 'YYYYMMDDHHmmss').format('YYYY-MM-DD HH:mm:ss')
+    },
+    addTemplate() {
+      this.title = '新增模板'
+      this.resetForm('templateForm')
+      this.templateDialogVisible = true
+    },
+    updateTemplate(row) {
+      this.title = '修改模板'
+      this.resetForm('templateForm')
+      this.templateForm = Object.assign({}, row)
+      this.templateDialogVisible = true
+    },
+    saveTemplate(formName) {
+      this.$refs[formName].validate((valid) => {
+        if (valid) {
+          saveMsgTemplateList(this.templateForm).then(response => {
+            this.$notify({
+              title: '成功',
+              message: '保存成功!',
+              type: 'success',
+              duration: 2000
+            })
+            this.templateDialogVisible = false
+            this.getMsgTemplateList()
+          }).catch(error => {
+            this.$message({
+              message: error.msg || '请求异常',
+              type: 'error'
+            })
+          })
+        } else {
+          return false
+        }
+      })
+    },
+    resetForm(formName) {
+      this.templateForm = {
+        id: '',
+        title: '',
+        content: ''
+      }
+      this.$nextTick(() => {
+        this.$refs[formName].clearValidate()
+      })
+    }
+  }
 }
 </script>
+<style>
+.form-input-item
+  .el-input__inner{
+    padding-right: 50px;
+  }
+
+</style>