手机通知公告
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java b/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
index 9bc836a..1378bcf 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/api/bean/KafkaXgMessage.java
@@ -6,18 +6,20 @@
   private String content;//提醒消息内容
   private String gids;//gid发送列表,逗号隔开
   private Boolean alltarget;//是否平台全发送
-  private String platform;//android,ios
+  private String platform;//android,ios,all
   private int type;//TYPE_NOTIFICATION = 1,TYPE_MESSAGE = 2
 
-  private String url;//跳转url
   private String expiretime;//过期时间
   private Boolean callback;//是否回调返回
-  private String msg_type;//提醒类型 字典 epay_pos_scan_code_pay、
-  private String refno;//流水参考号
   private int retries;//最多重试次数
-  private String amount;//消费金额,元,小数点后两位
   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;
@@ -67,14 +69,6 @@
     this.type = type;
   }
 
-  public String getUrl() {
-    return url;
-  }
-
-  public void setUrl(String url) {
-    this.url = url;
-  }
-
   public String getExpiretime() {
     return expiretime;
   }
@@ -91,22 +85,6 @@
     this.callback = callback;
   }
 
-  public String getMsg_type() {
-    return msg_type;
-  }
-
-  public void setMsg_type(String msg_type) {
-    this.msg_type = msg_type;
-  }
-
-  public String getRefno() {
-    return refno;
-  }
-
-  public void setRefno(String refno) {
-    this.refno = refno;
-  }
-
   public int getRetries() {
     return retries;
   }
@@ -115,14 +93,6 @@
     this.retries = retries;
   }
 
-  public String getAmount() {
-    return amount;
-  }
-
-  public void setAmount(String amount) {
-    this.amount = amount;
-  }
-
   public String getCustom() {
     return custom;
   }
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeDao.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeDao.java
new file mode 100644
index 0000000..800c10e
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeDao.java
@@ -0,0 +1,19 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import javax.persistence.LockModeType;
+
+@Repository
+public interface NoticeDao extends JpaRepository<TNotice, String>, JpaSpecificationExecutor<TNotice> {
+  TNotice getById(String id);
+
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @Query("from TNotice t where t.id=?1 ")
+  TNotice getByIdForUpdate(String id);
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeMsgDao.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeMsgDao.java
new file mode 100644
index 0000000..35a6dd4
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/NoticeMsgDao.java
@@ -0,0 +1,30 @@
+package com.supwisdom.dlpay.framework.dao;
+
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.jpa.repository.QueryHints;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.persistence.LockModeType;
+import javax.persistence.QueryHint;
+import java.util.List;
+
+@Repository
+public interface NoticeMsgDao extends JpaRepository<TNoticeMsg, String> {
+  TNoticeMsg getByMsgid(String msgid);
+
+  @Transactional
+  @Lock(LockModeType.PESSIMISTIC_WRITE)
+  @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "0")})
+  @Query("from TNoticeMsg where msgid=?1")
+  TNoticeMsg getByMsgidForUpdateNowait(String msgid);
+
+  @Query("from TNoticeMsg where noticeId=?1 order by createtime desc")
+  List<TNoticeMsg> findAllByNoticeId(String noticeid);
+
+  @Query("from TNoticeMsg t where t.status='normal' and t.pushmode='delay' and t.pushSettime<=?1 and t.sendkafka=false order by t.createtime")
+  List<TNoticeMsg> findAllDelayNoticeByDatetime(String datetime);
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java
index e125f7a..945f510 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/dao/RoleFunctionDao.java
@@ -19,11 +19,11 @@
     List<TRoleFunction> findByRoleId(String roleId);
 
     @Query(value = "select tt.id,tt.pid,tt.name,tt.checked,tt.open from  " +
-            " ( select f.id||'' as id ,f.parentid||'' as pid,f.name,case when rf.id is null then 0 else 1 end as checked,case when f.parentid=-1 then 1 else 0 end as open from tb_function f " +
+            " ( select f.id||'' as id ,f.parentid||'' as pid,f.name,case when rf.id is null then 0 else 1 end as checked,case when f.parentid=-1 then 1 else 0 end as open,f.ordernum from tb_function f " +
             " left join tb_role_function rf on rf.functionid = f.id and rf.roleid=?1  " +
             " union all " +
-            " select r.id||'_res' as id,r.function_id||'' as pid,r.name,case when p.id is null then 0 else 1 end as checked,0 as open from tb_resource  r " +
-            " left join tb_permission p on p.resid = r.id and p.roleid=?1 ) tt order by tt.id " , nativeQuery = true)
+            " select r.id||'_res' as id,r.function_id||'' as pid,r.name,case when p.id is null then 0 else 1 end as checked,0 as open,999999 as ordernum from tb_resource  r " +
+            " left join tb_permission p on p.resid = r.id and p.roleid=?1 ) tt order by tt.pid,tt.ordernum" , nativeQuery = true)
     List<NodeData> findByRoleIdNative(String roleId);
 
     void deleteByRoleId(String roleId);
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNotice.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNotice.java
new file mode 100644
index 0000000..08e645d
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNotice.java
@@ -0,0 +1,149 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 通知公告表(全体通知)
+ * */
+@Entity
+@Table(name = "TB_NOTICE")
+public class TNotice {
+  @Id
+  @GenericGenerator(name = "nidGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "nidGenerator")
+  @Column(name = "ID", nullable = false, length = 32)
+  private String id;
+
+  @Column(name = "TITLE", nullable = false, length = 200)
+  private String title; //标题
+
+  @Column(name = "CONTENT", nullable = false, length = 2000)
+  private String content; //内容
+
+  @Column(name = "PLATFORM", nullable = false, length = 10)
+  private String platform; //all-全部;ios-苹果;android-安卓
+
+  @Column(name = "CREATEDATE", nullable = false, length = 8)
+  private String createdate; //创建日期yyyyMMdd
+
+  @Column(name = "CREATETIME", length = 6)
+  private String createtime; //创建日期hh24miss
+
+  @Column(name = "OPERID", length = 32)
+  private String operid; //创建者的operid
+
+  @Column(name = "CREATOR", length = 200)
+  private String creator; //创建者名称
+
+  @Column(name = "SENDCNT", nullable = false, precision = 9)
+  private Integer sendcnt = 0;
+
+  @Column(name = "LINKURL", length = 1000)
+  private String linkurl;
+
+  @Column(name = "LASTSAVED", length = 14)
+  private String lastsaved; //最后更新时间
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  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 getPlatform() {
+    return platform;
+  }
+
+  public void setPlatform(String platform) {
+    this.platform = platform;
+  }
+
+  public String getCreatedate() {
+    return createdate;
+  }
+
+  public void setCreatedate(String createdate) {
+    this.createdate = createdate;
+  }
+
+  public String getCreatetime() {
+    return createtime;
+  }
+
+  public void setCreatetime(String createtime) {
+    this.createtime = createtime;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getCreator() {
+    return creator;
+  }
+
+  public void setCreator(String creator) {
+    this.creator = creator;
+  }
+
+  public Integer getSendcnt() {
+    return sendcnt;
+  }
+
+  public void setSendcnt(Integer sendcnt) {
+    this.sendcnt = sendcnt;
+  }
+
+  public String getLinkurl() {
+    return linkurl;
+  }
+
+  public void setLinkurl(String linkurl) {
+    this.linkurl = linkurl;
+  }
+
+  public String getLastsaved() {
+    return lastsaved;
+  }
+
+  public void setLastsaved(String lastsaved) {
+    this.lastsaved = lastsaved;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNoticeMsg.java b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNoticeMsg.java
new file mode 100644
index 0000000..2a417e0
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/framework/domain/TNoticeMsg.java
@@ -0,0 +1,160 @@
+package com.supwisdom.dlpay.framework.domain;
+
+import org.hibernate.annotations.GenericGenerator;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 通知公告发布明细表(全体通知)
+ * */
+@Entity
+@Table(name = "TB_NOTICE_MSG")
+public class TNoticeMsg {
+  @Id
+  @GenericGenerator(name = "midGenerator", strategy = "uuid")
+  @GeneratedValue(generator = "midGenerator")
+  @Column(name = "msgid", nullable = false, length = 32)
+  private String msgid;
+
+  @Column(name = "notice_id", nullable = false, length = 32)
+  private String noticeId;
+
+  @Column(name = "alltarget", nullable = false, length = 10)
+  private Boolean alltarget = true; //是否全体通知公告
+
+  @Column(name = "pushmode", nullable = false, length = 10)
+  private String pushmode = "atonce"; //atonce-立即发布,delay-延迟定时发布
+
+  @Column(name = "push_settime", length = 14)
+  private String pushSettime; //定时发布的设定时间
+
+  @Column(name = "publisher", length = 200)
+  private String publisher; //发布者
+
+  @Column(name = "operid", length = 32)
+  private String operid; //发布者operid
+
+  @Column(name = "status", nullable = false, length = 10)
+  private String status;
+
+  @Column(name = "sendkafka", nullable = false, length = 10)
+  private Boolean sendkafka=false;
+
+  @Column(name = "pushtime", length = 14)
+  private String pushtime;
+
+  @Column(name = "createtime", length = 14)
+  private String createtime;
+
+  @Column(name = "pushresult", length = 600)
+  private String pushresult;
+
+  @Column(name = "tenantid", length = 20)
+  @NotNull
+  private String tenantId;
+
+  public String getMsgid() {
+    return msgid;
+  }
+
+  public void setMsgid(String msgid) {
+    this.msgid = msgid;
+  }
+
+  public String getNoticeId() {
+    return noticeId;
+  }
+
+  public void setNoticeId(String noticeId) {
+    this.noticeId = noticeId;
+  }
+
+  public Boolean getAlltarget() {
+    return alltarget;
+  }
+
+  public void setAlltarget(Boolean alltarget) {
+    this.alltarget = alltarget;
+  }
+
+  public String getPushmode() {
+    return pushmode;
+  }
+
+  public void setPushmode(String pushmode) {
+    this.pushmode = pushmode;
+  }
+
+  public String getPushSettime() {
+    return pushSettime;
+  }
+
+  public void setPushSettime(String pushSettime) {
+    this.pushSettime = pushSettime;
+  }
+
+  public String getPublisher() {
+    return publisher;
+  }
+
+  public void setPublisher(String publisher) {
+    this.publisher = publisher;
+  }
+
+  public String getOperid() {
+    return operid;
+  }
+
+  public void setOperid(String operid) {
+    this.operid = operid;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public Boolean getSendkafka() {
+    return sendkafka;
+  }
+
+  public void setSendkafka(Boolean sendkafka) {
+    this.sendkafka = sendkafka;
+  }
+
+  public String getPushtime() {
+    return pushtime;
+  }
+
+  public void setPushtime(String pushtime) {
+    this.pushtime = pushtime;
+  }
+
+  public String getCreatetime() {
+    return createtime;
+  }
+
+  public void setCreatetime(String createtime) {
+    this.createtime = createtime;
+  }
+
+  public String getPushresult() {
+    return pushresult;
+  }
+
+  public void setPushresult(String pushresult) {
+    this.pushresult = pushresult;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public void setTenantId(String tenantId) {
+    this.tenantId = tenantId;
+  }
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/system/controller/NoticeController.java b/payapi/src/main/java/com/supwisdom/dlpay/system/controller/NoticeController.java
new file mode 100644
index 0000000..71bfae7
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/system/controller/NoticeController.java
@@ -0,0 +1,192 @@
+package com.supwisdom.dlpay.system.controller;
+
+import com.supwisdom.dlpay.api.bean.JsonResult;
+import com.supwisdom.dlpay.api.service.KafkaSendMsgService;
+import com.supwisdom.dlpay.framework.data.SystemDateTime;
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.service.SystemUtilService;
+import com.supwisdom.dlpay.framework.util.DateUtil;
+import com.supwisdom.dlpay.framework.util.PageResult;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.framework.util.WebConstant;
+import com.supwisdom.dlpay.system.service.NoticeService;
+import com.supwisdom.dlpay.util.ConstantUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+@RequestMapping("/notice")
+public class NoticeController {
+  @Autowired
+  private NoticeService noticeService;
+  @Autowired
+  private SystemUtilService systemUtilService;
+  @Autowired
+  private KafkaSendMsgService kafkaSendMsgService;
+
+  @GetMapping("/index")
+  public String noticeIndex() {
+    return "system/notice/index";
+  }
+
+  @GetMapping("/list")
+  @PreAuthorize("hasPermission('/notice/index','')")
+  @ResponseBody
+  public PageResult<TNotice> searchNotice(@RequestParam("page") Integer pageNo,
+                                          @RequestParam("limit") Integer pageSize,
+                                          @RequestParam(value = "startdate", required = false) String startdate,
+                                          @RequestParam(value = "enddate", required = false) String enddate,
+                                          @RequestParam(value = "searchkey", required = false) String searchkey) {
+    try {
+      if (null == pageNo || pageNo < 1) pageNo = WebConstant.PAGENO_DEFAULT;
+      if (null == pageSize || pageSize < 1) pageSize = WebConstant.PAGESIZE_DEFAULT;
+      return noticeService.getNotice(startdate, enddate, searchkey, pageNo, pageSize);
+    } catch (Exception e) {
+      e.printStackTrace();
+      return new PageResult<>(99, "系统查询错误");
+    }
+  }
+
+  @GetMapping("/load4add")
+  @PreAuthorize("hasPermission('/notice/load4add','')")
+  public String load4Add() {
+    return "system/notice/add";
+  }
+
+  @PostMapping("/edit")
+  @PreAuthorize("hasPermission('/notice/edit','')")
+  @ResponseBody
+  public JsonResult edit(@RequestParam("noticeId") String noticeId,
+                         @RequestParam("title") String title,
+                         @RequestParam("content") String content,
+                         @RequestParam("platform") String platform,
+                         @RequestParam(value = "linkurl", required = false) String linkurl,
+                         @AuthenticationPrincipal UserDetails operUser) {
+    try {
+      TOperator oper = (TOperator) operUser;
+      String prefix = "";
+      TNotice notice = null;
+      if (StringUtil.isEmpty(noticeId)) {
+        prefix = "新增";
+        notice = new TNotice();
+        SystemDateTime dt = systemUtilService.getSysdatetime();
+        notice.setCreatedate(dt.getHostdate());
+        notice.setCreatetime(dt.getHosttime());
+        notice.setOperid(oper.getOperid());
+        notice.setCreator(oper.getOpername());
+        notice.setTenantId(oper.getTenantId());
+      } else {
+        prefix = "修改";
+        notice = noticeService.getById(noticeId);
+        if (null == notice) return JsonResult.error("未找到原始记录,请重新查询后在操作。");
+        if (notice.getSendcnt() > 0) return JsonResult.error("公告已发布,不能修改");
+      }
+      if (StringUtil.isEmpty(title)) return JsonResult.error("公告标题不能为空");
+      if (StringUtil.isEmpty(content)) return JsonResult.error("公告内容不能为空");
+      if (!ConstantUtil.PHONE_PLATFORM_ALL.equals(platform) && !ConstantUtil.PHONE_PLATFORM_ANDROID.equals(platform) && !ConstantUtil.PHONE_PLATFORM_IOS.equals(platform))
+        return JsonResult.error("请选择推送终端");
+      notice.setPlatform(platform);
+      notice.setTitle(title.trim());
+      notice.setContent(content.trim());
+      if (StringUtil.isEmpty(linkurl)) {
+        notice.setLinkurl(null);
+      } else {
+        notice.setLinkurl(linkurl.trim());
+      }
+
+      if (noticeService.doSaveOrUpdateNotice(notice)) {
+        return JsonResult.ok(prefix + "成功!");
+      } else {
+        return JsonResult.error(prefix + "失败!");
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+      if (StringUtil.isEmpty(noticeId)) {
+        return JsonResult.error("新增失败,业务异常!").put("exception", e);
+      } else {
+        return JsonResult.error("修改失败,业务异常!").put("exception", e);
+      }
+    }
+  }
+
+  @PostMapping("/delete")
+  @PreAuthorize("hasPermission('/notice/delete','')")
+  @ResponseBody
+  public JsonResult delete(@RequestParam("noticeId") String noticeId) {
+    try {
+      TNotice notice = noticeService.getById(noticeId);
+      if (null == notice) return JsonResult.error("未找到原始记录,请重新查询后在操作。");
+      if (notice.getSendcnt() > 0) return JsonResult.error("公告已发布,不能删除!");
+      if (noticeService.deleteNotice(notice)) {
+        return JsonResult.ok("删除成功!");
+      } else {
+        return JsonResult.error("删除失败!");
+      }
+    } catch (Exception e) {
+      e.printStackTrace();
+      return JsonResult.error("删除失败,业务异常!").put("exception", e);
+    }
+  }
+
+  @GetMapping("/load4publish")
+  @PreAuthorize("hasPermission('/notice/load4publish','')")
+  public String load4Publish() {
+    return "system/notice/publish";
+  }
+
+  @PostMapping("/publish")
+  @PreAuthorize("hasPermission('/notice/publish','')")
+  @ResponseBody
+  public JsonResult publish(@RequestParam("noticeId") String noticeId,
+                            @RequestParam("pushmode") String pushmode,
+                            @RequestParam(value = "settime", required = false) String settime,
+                            @AuthenticationPrincipal UserDetails operUser) {
+    try {
+      TOperator oper = (TOperator) operUser;
+      TNotice notice = noticeService.getById(noticeId);
+      if (null == notice) return JsonResult.error("未找到原始记录,请重新查询后在操作。");
+      if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode) && !ConstantUtil.PHONE_NOTICE_PUSHMODE_DELAY.equals(pushmode))
+        return JsonResult.error("请选择是立即发布还是定时发布");
+      if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode) && !DateUtil.checkDatetimeValid(settime, "yyyy-MM-dd HH:mm:ss"))
+        return JsonResult.error("定时发布请设定正确的发布时间");
+      if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode) && DateUtil.compareDatetime(DateUtil.unParseToDateFormat(settime), systemUtilService.getSysdatetime().getHostdatetime(), DateUtil.DATETIME_FMT) <= 0)
+        return JsonResult.error("设定的发布时间必须比当前时间大");
+
+      TNoticeMsg msg = noticeService.doPublishNotice(notice.getId(), pushmode, settime, oper);
+      if (ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode)) {
+        kafkaSendMsgService.sendNoticeMessage(msg.getMsgid()); //异步立即推送
+      }
+      return JsonResult.ok("发布成功!");
+    } catch (Exception e) {
+      e.printStackTrace();
+      return JsonResult.error("发布失败,业务异常!").put("exception", e);
+    }
+  }
+
+  @GetMapping("/load4detail")
+  @PreAuthorize("hasPermission('/notice/load4detail','')")
+  public String load4PublishDetails() {
+    return "system/notice/details";
+  }
+
+  @GetMapping("/detaillist")
+  @PreAuthorize("hasPermission('/notice/load4detail','')")
+  @ResponseBody
+  public PageResult<TNoticeMsg> searchNoticePublishDetails(@RequestParam("noticeId") String noticeId) {
+    try {
+      return noticeService.getNoticePublishDetails(noticeId);
+    } catch (Exception e) {
+      e.printStackTrace();
+      return new PageResult<>(99, "系统查询错误");
+    }
+  }
+
+
+
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/system/service/NoticeService.java b/payapi/src/main/java/com/supwisdom/dlpay/system/service/NoticeService.java
new file mode 100644
index 0000000..1bb9425
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/system/service/NoticeService.java
@@ -0,0 +1,34 @@
+package com.supwisdom.dlpay.system.service;
+
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.util.PageResult;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+public interface NoticeService {
+  @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, readOnly = true)
+  PageResult<TNotice> getNotice(String startdate, String enddate, String searchkey, int pageNo, int pageSize);
+
+  @Transactional(rollbackFor = Exception.class, readOnly = true)
+  TNotice getById(String id);
+
+  @Transactional(rollbackFor = Exception.class)
+  boolean doSaveOrUpdateNotice(TNotice notice);
+
+  @Transactional(rollbackFor = Exception.class)
+  boolean deleteNotice(TNotice notice);
+
+  @Transactional(rollbackFor = Exception.class)
+  TNoticeMsg doPublishNotice(String noticeId, String pushmode, String settime, TOperator oper)throws Exception;
+
+  @Transactional(rollbackFor = Exception.class, readOnly = true)
+  PageResult<TNoticeMsg> getNoticePublishDetails(String noticeId);
+
+  @Transactional(rollbackFor = Exception.class, readOnly = true)
+  List<TNoticeMsg> getDelayNoticeByDatetime(String datetime);
+
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/system/service/impl/NoticeServiceImpl.java b/payapi/src/main/java/com/supwisdom/dlpay/system/service/impl/NoticeServiceImpl.java
new file mode 100644
index 0000000..7f26776
--- /dev/null
+++ b/payapi/src/main/java/com/supwisdom/dlpay/system/service/impl/NoticeServiceImpl.java
@@ -0,0 +1,128 @@
+package com.supwisdom.dlpay.system.service.impl;
+
+import com.supwisdom.dlpay.framework.dao.NoticeDao;
+import com.supwisdom.dlpay.framework.dao.NoticeMsgDao;
+import com.supwisdom.dlpay.framework.domain.TNotice;
+import com.supwisdom.dlpay.framework.domain.TNoticeMsg;
+import com.supwisdom.dlpay.framework.domain.TOperator;
+import com.supwisdom.dlpay.framework.service.SystemUtilService;
+import com.supwisdom.dlpay.framework.util.DateUtil;
+import com.supwisdom.dlpay.framework.util.PageResult;
+import com.supwisdom.dlpay.framework.util.StringUtil;
+import com.supwisdom.dlpay.framework.util.TradeDict;
+import com.supwisdom.dlpay.system.service.NoticeService;
+import com.supwisdom.dlpay.util.ConstantUtil;
+import com.supwisdom.dlpay.util.WebCheckException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.Root;
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class NoticeServiceImpl implements NoticeService {
+  @Autowired
+  private NoticeDao noticeDao;
+  @Autowired
+  private NoticeMsgDao noticeMsgDao;
+  @Autowired
+  private SystemUtilService systemUtilService;
+
+  @Override
+  public PageResult<TNotice> getNotice(String startdate, String enddate, String searchkey, int pageNo, int pageSize) {
+    Pageable pageable = PageRequest.of(pageNo - 1, pageSize, Sort.by(Sort.Direction.DESC, "createdate"));
+    Page<TNotice> page = noticeDao.findAll(new Specification<TNotice>() {
+      @Override
+      public Predicate toPredicate(Root<TNotice> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
+        List<Predicate> predicates = new ArrayList<>();
+        if (!StringUtil.isEmpty(startdate)) {
+          predicates.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createdate").as(String.class), DateUtil.unParseToDateFormat(startdate)));
+        }
+        if (!StringUtil.isEmpty(enddate)) {
+          predicates.add(criteriaBuilder.lessThanOrEqualTo(root.get("createdate").as(String.class), DateUtil.unParseToDateFormat(enddate)));
+        }
+        if (!StringUtil.isEmpty(searchkey)) {
+          predicates.add(criteriaBuilder.like(root.get("title").as(String.class), "%" + searchkey.trim() + "%"));
+        }
+        return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
+      }
+    }, pageable);
+    return new PageResult<>(page);
+  }
+
+  @Override
+  public TNotice getById(String id) {
+    if (!StringUtil.isEmpty(id)) return noticeDao.getById(id.trim());
+    return null;
+  }
+
+  @Override
+  public boolean doSaveOrUpdateNotice(TNotice notice) {
+    if (null != notice) {
+      notice.setLastsaved(systemUtilService.getSysdatetime().getHostdatetime());
+      noticeDao.save(notice);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public boolean deleteNotice(TNotice notice) {
+    if (null != notice) {
+      noticeDao.delete(notice);
+      return true;
+    }
+    return false;
+  }
+
+  @Override
+  public TNoticeMsg doPublishNotice(String noticeId, String pushmode, String settime, TOperator oper) throws Exception {
+    TNotice notice = noticeDao.getByIdForUpdate(noticeId);
+    if (null == notice) throw new WebCheckException("未找到原始记录,请重新查询后在操作。");
+
+    TNoticeMsg msg = new TNoticeMsg();
+    msg.setNoticeId(notice.getId());
+    msg.setAlltarget(true);
+    msg.setPushmode(pushmode);
+    if (!ConstantUtil.PHONE_NOTICE_PUSHMODE_ATONCE.equals(pushmode)) {
+      msg.setPushSettime(DateUtil.unParseToDateFormat(settime));
+    }
+    msg.setPublisher(oper.getOpername());
+    msg.setOperid(oper.getOperid());
+    msg.setStatus(TradeDict.STATUS_NORMAL);
+    msg.setSendkafka(false);
+    msg.setCreatetime(systemUtilService.getSysdatetime().getHostdatetime());
+    msg.setTenantId(notice.getTenantId());
+    msg.setPushresult("待推送");
+
+    notice.setSendcnt(notice.getSendcnt() + 1);
+    notice.setLastsaved(systemUtilService.getSysdatetime().getHostdatetime());
+    noticeDao.save(notice);
+    return noticeMsgDao.save(msg);
+  }
+
+  @Override
+  public PageResult<TNoticeMsg> getNoticePublishDetails(String noticeId) {
+    if (!StringUtil.isEmpty(noticeId)) {
+      List<TNoticeMsg> list = noticeMsgDao.findAllByNoticeId(noticeId);
+      return new PageResult<>(list);
+    }
+    return new PageResult<>(99, "无数据");
+  }
+
+  @Override
+  public List<TNoticeMsg> getDelayNoticeByDatetime(String datetime) {
+    List<TNoticeMsg> list = noticeMsgDao.findAllDelayNoticeByDatetime(datetime);
+    if (!StringUtil.isEmpty(list)) return list;
+    return new ArrayList<>(0);
+  }
+}
diff --git a/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java b/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
index ba129e6..4f6d410 100644
--- a/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
+++ b/payapi/src/main/java/com/supwisdom/dlpay/util/ConstantUtil.java
@@ -91,4 +91,17 @@
   public static final String OPERCHK_CHKMODE_DELETE = "删除";
 
   public static final String CHKFILE_DELIMITER = "|";
+
+  public static final String PHONE_PLATFORM_ALL = "all";
+  public static final String PHONE_PLATFORM_ANDROID = "android";
+  public static final String PHONE_PLATFORM_IOS = "ios";
+
+  public static final String PHONE_NOTICE_PUSHMODE_ATONCE = "atonce"; //立即发布
+  public static final String PHONE_NOTICE_PUSHMODE_DELAY = "delay"; //延时发布
+
+  /**
+   * kafka消息类型
+   * */
+  public static final String KAFKA_MAGTYPE_NOTICE = "dlsmk_phone_notice";
+  public static final String KAFKA_MAGTYPE_CONSUME = "dlsmk_card_consume";
 }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
index d993aea..f489996 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/PayApiApplication.kt
@@ -4,6 +4,7 @@
 import io.lettuce.core.ReadFrom
 import net.javacrumbs.shedlock.core.LockProvider
 import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider
+import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.SpringApplication
@@ -148,7 +149,7 @@
     }
 }
 
-
+@EnableSchedulerLock(defaultLockAtMostFor = "PT30M")
 @SpringBootApplication
 @EnableDiscoveryClient
 @EnableScheduling
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
index e771add..f01d870 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/async_tasks.kt
@@ -71,6 +71,15 @@
         }
     }
 
+    @Bean(name = ["kafkaSendMessageAsyncTask"])
+    fun kafkaSendMessageAsyncTaskExecutor(): Executor {
+        return ThreadPoolTaskExecutor().apply {
+            corePoolSize = 20
+            maxPoolSize = 100
+            setWaitForTasksToCompleteOnShutdown(true)
+        }
+    }
+
     override fun getAsyncUncaughtExceptionHandler(): AsyncUncaughtExceptionHandler? {
         return MyAsyncUncaughtExceptionHandler()
     }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt
index 1d8e2fc..e7caa77 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/scheduler_task.kt
@@ -7,10 +7,12 @@
 import com.supwisdom.dlpay.api.repositories.ShopaccService
 import com.supwisdom.dlpay.api.service.ConsumePayService
 import com.supwisdom.dlpay.api.service.DtlQueryResultService
+import com.supwisdom.dlpay.api.service.KafkaSendMsgService
 import com.supwisdom.dlpay.api.service.TransactionServiceProxy
 import com.supwisdom.dlpay.framework.service.SystemUtilService
 import com.supwisdom.dlpay.framework.util.ApplicationUtil
 import com.supwisdom.dlpay.framework.util.TradeDict
+import com.supwisdom.dlpay.system.service.NoticeService
 import com.supwisdom.dlpay.util.ConstantUtil
 import net.javacrumbs.shedlock.core.SchedulerLock
 import org.springframework.beans.factory.annotation.Autowired
@@ -150,4 +152,34 @@
             }
         }
     }
+}
+
+/**
+ * 定时通知公告的推送任务
+ * */
+@Component
+class NoticePushMessageTask {
+    @Autowired
+    private lateinit var noticeService: NoticeService
+    @Autowired
+    private lateinit var systemUtilService: SystemUtilService
+    @Autowired
+    private lateinit var kafkaSendMsgService: KafkaSendMsgService
+
+    @Scheduled(cron = "\${send.delay.notice.task.cron:-}")
+    @SchedulerLock(name = "SendDelayNoticeTask", lockAtMostForString = "PT10M")
+    fun doSendDelayNotice() {
+        try {
+            val hostdatetime = systemUtilService.sysdatetime.hostdatetime
+            noticeService.getDelayNoticeByDatetime(hostdatetime).forEach {
+                try {
+                    kafkaSendMsgService.sendNoticeMessage(it.msgid)
+                } catch (e: Exception) {
+                    e.printStackTrace()
+                }
+            }
+        } catch (ex: Exception) {
+            ex.printStackTrace()
+        }
+    }
 }
\ No newline at end of file
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
index 290bfaa..c913448 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/impl/kafka_service_impl.kt
@@ -3,11 +3,17 @@
 import com.google.gson.Gson
 import com.supwisdom.dlpay.api.bean.KafkaXgMessage
 import com.supwisdom.dlpay.api.service.KafkaSendMsgService
+import com.supwisdom.dlpay.framework.dao.NoticeDao
+import com.supwisdom.dlpay.framework.dao.NoticeMsgDao
+import com.supwisdom.dlpay.framework.service.SystemUtilService
 import com.supwisdom.dlpay.framework.util.DateUtil
+import com.supwisdom.dlpay.framework.util.StringUtil
 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 com.supwisdom.dlpay.util.ConstantUtil
+import com.supwisdom.dlpay.util.WebCheckException
 import mu.KotlinLogging
 import org.springframework.beans.factory.annotation.Autowired
 import org.springframework.kafka.core.KafkaTemplate
@@ -15,19 +21,26 @@
 import org.springframework.stereotype.Service
 
 @Service
-class KafkaSendMsgServiceImpl:KafkaSendMsgService{
+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 noticeMsgDao: NoticeMsgDao
+    @Autowired
+    private lateinit var noticeDao: NoticeDao
+    @Autowired
     private lateinit var mobileApiService: MobileApiService
+    @Autowired
+    private lateinit var systemUtilService: SystemUtilService
+
     val gson = Gson()
 
     val topic = "jpush-messages"
 
-    @Async
+    @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 {
@@ -63,16 +76,60 @@
             message.custom = gson.toJson(extras)
             message.expiretime = DateUtil.getNewTime(DateUtil.getNow(), 300)
             message.gids = it.uid
-            if(it.lastloginplatform.isNullOrEmpty()){
-                message.platform="ios"
+            if (it.lastloginplatform.isNullOrEmpty()) {
+                message.platform = "all"
                 kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
-                message.platform="android"
-                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
-            }else{
+//                message.platform="ios"
+//                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
+//                message.platform="android"
+//                kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
+            } else {
                 kafkaTemplate.send(topic, msg.msgid, gson.toJson(message))
             }
         }
         msg.pusheduids = uids
         msgDao.save(msg)
     }
+
+    @Async("kafkaSendMessageAsyncTask")
+    override fun sendNoticeMessage(noticeMsgid: String) {
+        val noticeMsg = noticeMsgDao.getByMsgidForUpdateNowait(noticeMsgid) ?: return
+        try {
+            if (noticeMsg.sendkafka) return
+            val notice = noticeDao.getById(noticeMsg.noticeId) ?: throw WebCheckException("未找到对应的通知公告")
+
+            val extras = mutableMapOf<String, String>("msgid" to noticeMsg.msgid, "msg_type" to ConstantUtil.KAFKA_MAGTYPE_NOTICE)
+            if (!StringUtil.isEmpty(notice.linkurl)) extras["url"] = notice.linkurl
+            val message = KafkaXgMessage().apply {
+                this.title = notice.title
+                this.content = notice.content
+                this.alltarget = true //gids不需要传递
+                this.platform = notice.platform
+                this.type = 1 //TYPE_NOTIFICATION = 1,TYPE_MESSAGE = 2
+                this.callback = true
+                this.retries = 3
+                this.custom = gson.toJson(extras)
+            }
+            println(gson.toJson(message))
+            kafkaTemplate.send(topic, noticeMsg.msgid, gson.toJson(message))
+
+            noticeMsg.sendkafka = true
+            noticeMsg.pushtime = systemUtilService.sysdatetime.hostdatetime
+            noticeMsg.pushresult = "已推送"
+            noticeMsgDao.save(noticeMsg)
+        } catch (wex: WebCheckException) {
+            noticeMsg.sendkafka = false
+            noticeMsg.pushtime = systemUtilService.sysdatetime.hostdatetime
+            noticeMsg.pushresult = "推送失败! ${wex.message}"
+            noticeMsgDao.save(noticeMsg)
+            return
+        } catch (e: Exception) {
+            e.printStackTrace()
+            noticeMsg.sendkafka = false
+            noticeMsg.pushtime = systemUtilService.sysdatetime.hostdatetime
+            noticeMsg.pushresult = "推送失败"
+            noticeMsgDao.save(noticeMsg)
+            return
+        }
+    }
 }
diff --git a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
index a5cc0e8..b874abc 100644
--- a/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
+++ b/payapi/src/main/kotlin/com/supwisdom/dlpay/api/service/kafka_service.kt
@@ -1,5 +1,6 @@
 package com.supwisdom.dlpay.api.service
 
+import com.supwisdom.dlpay.framework.dao.NoticeMsgDao
 import com.supwisdom.dlpay.mobile.dao.MsgDao
 import mu.KotlinLogging
 import org.apache.kafka.clients.consumer.ConsumerRecord
@@ -9,22 +10,35 @@
 
 interface KafkaSendMsgService {
     fun sendJpushMessage(userid: String, title: String, content: String, refno: String, extras: MutableMap<String, String>, tenantId: String?)
+
+    fun sendNoticeMessage(noticeMsgid: String)
 }
 @Component
 class  KafkaMsgListener{
     val logger = KotlinLogging.logger { }
     @Autowired
     private lateinit var msgDao: MsgDao
+    @Autowired
+    private lateinit var noticeMsgDao: NoticeMsgDao
 
     @KafkaListener(topics = ["jpush-messages-result"],autoStartup = "\${spring.kafka.listen.auto.start:true}")
-    fun listen(record :ConsumerRecord<String, String>) {
+    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){
+        if (!record.key().isNullOrEmpty()) {
+            val opt = msgDao.findById(record.key())
+            if (opt.isPresent) {
                 var msg = opt.get()
                 msg.pushresult = record.value()
                 msgDao.save(msg)
+                return
+            }
+
+            val noticemsg = noticeMsgDao.findById(record.key())
+            if (noticemsg.isPresent) {
+                var nmsg = noticemsg.get()
+                nmsg.pushresult = record.value()
+                noticeMsgDao.save(nmsg)
+                return
             }
         }
     }
diff --git a/payapi/src/main/resources/application.properties b/payapi/src/main/resources/application.properties
index 0a2b535..2c5c6a0 100644
--- a/payapi/src/main/resources/application.properties
+++ b/payapi/src/main/resources/application.properties
@@ -35,6 +35,7 @@
 query.third.transdtl.result.cron=7 0/1 * * * ?
 payapi.sourcetype.checker.scheduler=7 3/10 * * * ?
 citizencard.dolosstask.cron=-
+send.delay.notice.task.cron=29 0/1 * * * ?
 ################################################
 # user password
 auth.password.bcrypt.length=10
diff --git a/payapi/src/main/resources/data.sql b/payapi/src/main/resources/data.sql
index bcdf83c..3bd9798 100644
--- a/payapi/src/main/resources/data.sql
+++ b/payapi/src/main/resources/data.sql
@@ -77,6 +77,8 @@
 VALUES (37, NULL, 1, NULL, '', '/user/card', '市民卡查询', 1, 19, '{tenantid}');
 INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
 VALUES (38, NULL, 1, NULL, '', '/report/shoptodaybusiness', '商户当天统计表', 4, 20, '{tenantid}');
+INSERT INTO "tb_function" ("id", "createtime", "isleaf", "lastsaved", "menuicon", "menuurl", "name", "ordernum", "parentid", tenantid)
+VALUES (39, NULL, 1, NULL, '', '/notice/index', '手机通知公告', 6, 3, '{tenantid}');
 
 
 INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
@@ -145,6 +147,8 @@
 VALUES ('ff8080816db87e27016db88be41f0015', 37, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
 VALUES ('4028ee9f6e5d95d8016e5d99e8d50012', 38, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO "tb_role_function" ("id", "functionid", "roleid", tenantid)
+VALUES ('ff8080816f8d8258016f8d85e4d70005', 39, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 
 
 INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
@@ -311,6 +315,21 @@
 VALUES (94, '', 37, '修改跳转', '/user/load4modifycard', '{tenantid}');
 INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
 VALUES (95, '', 37, '修改', '/user/cardupdate', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (96, '', 39, '查询', '/notice/index', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (97, '', 39, '新增修改跳转', '/notice/load4add', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (98, '', 39, '新增修改', '/notice/edit', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (99, '', 39, '删除', '/notice/delete', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (100, '', 39, '发布跳转', '/notice/load4publish', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (101, '', 39, '发布', '/notice/publish', '{tenantid}');
+INSERT INTO "tb_resource" ("id", "code", "function_id", "name", "uri", tenantid)
+VALUES (102, '', 39, '发布明细', '/notice/load4detail', '{tenantid}');
+
 
 INSERT INTO "tb_permission" ("id", "resid", "role_func_id", "roleid", tenantid)
 VALUES ('ff8080816b7947ed016b795577300036', 16, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
@@ -476,7 +495,20 @@
 VALUES ('ff8080816ecaebf8016ecaeedcec0004', 94, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
 VALUES ('ff8080816ecafb4e016ecafd58400005', 95, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
-
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816f8d8258016f8d85e4e20006', 96, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780030', 97, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80978002e', 98, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780031', 99, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d809780032', 100, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80978002f', 101, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
+INSERT INTO  "tb_permission" ("id", "resid", "role_func_id", "roleid", "tenantid")
+VALUES ('ff8080816fa2c2b7016fa2d80977002d', 102, NULL, 'd1yctWs5+ks0iQN3m9bUvRHus6HbKbrs', '{tenantid}');
 
 
 INSERT INTO "tb_subject" ("subjid","subjno", "balflag", "displayflag", "endflag", "fsubjno", "opendate", "subjlevel", "subjname", "subjtype", "tenantid")
diff --git a/payapi/src/main/resources/static/libs/custom.js b/payapi/src/main/resources/static/libs/custom.js
index d034401..0307a2d 100644
--- a/payapi/src/main/resources/static/libs/custom.js
+++ b/payapi/src/main/resources/static/libs/custom.js
@@ -104,8 +104,9 @@
     }
 
     root.isempty = function (s) {
-        if (s == null || s.length == 0)
+        if (undefined == s || null == s || '' == s || s.length == 0) {
             return true;
-        return /\s/.test(s);
+        }
+        return false;
     }
 }(window));
\ No newline at end of file
diff --git a/payapi/src/main/resources/templates/system/notice/add.html b/payapi/src/main/resources/templates/system/notice/add.html
new file mode 100644
index 0000000..1247eda
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/add.html
@@ -0,0 +1,85 @@
+<form id="notice-add-form" lay-filter="notice-add-form" class="layui-form model-form">
+    <input name="noticeId" id="hidden-notice-add-msgid" type="hidden"/>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>推送终端</label>
+        <div class="layui-input-block">
+            <input type="radio" name="platform" value="all" title="全部" checked/>
+            <input type="radio" name="platform" value="android" title="安卓手机"/>
+            <input type="radio" name="platform" value="ios" title="苹果手机"/>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>公告标题</label>
+        <div class="layui-input-block">
+            <input name="title" placeholder="请填写标题,不能超过16个字" type="text" class="layui-input" maxlength="16"
+                   autocomplete="off" lay-verify="required" required/>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>公告内容</label>
+        <div class="layui-input-block">
+            <textarea name="content" placeholder="请输入内容,不能超过200个字" autocomplete="off" maxlength="200" class="layui-textarea" rows="6" lay-verify="required" required></textarea>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label">链接地址</label>
+        <div class="layui-input-block">
+            <textarea name="linkurl" placeholder="https://www.baidu.com" autocomplete="off" class="layui-textarea" lay-verify="linkurl"></textarea>
+        </div>
+    </div>
+
+    <div class="layui-form-item model-form-footer">
+        <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
+        <button class="layui-btn" lay-filter="notice-add-form-submit" lay-submit id="notice-add-submit-btn">保存</button>
+    </div>
+</form>
+
+<script>
+    layui.use(['layer', 'admin', 'form'], function () {
+        var layer = layui.layer;
+        var admin = layui.admin;
+        var form = layui.form;
+
+        form.render('radio');
+        form.verify({
+            linkurl: function (value, item) {
+                if ("" != value) {
+                    if (!/(^#)|(^http(s*):\/\/[^\s]+\.[^\s]+)/.test(value)) {
+                        return "链接格式不正确";
+                    }
+                }
+            }
+        });
+
+        var bean = admin.getTempData('t_noticeTmp');
+        if (bean) {
+            form.val('notice-add-form', bean);
+        }
+        // 表单提交事件
+        form.on('submit(notice-add-form-submit)', function (data) {
+            layer.load(2);
+            var token = $("meta[name='_csrf_token']").attr("value");
+            var param = data.field;
+            param["_csrf"] = token;
+            admin.go('[[@{/notice/edit}]]', param, function (result) {
+                console.log(result);
+                layer.closeAll('loading');
+                if (result.code == 200) {
+                    layer.msg(result.msg, {icon: 1});
+                    admin.finishPopupCenter();
+                } else if (result.code == 401) {
+                    layer.msg(result.msg, {icon: 2, time: 1500}, function () {
+                        location.replace('[[@{/login}]]');
+                    }, 1000);
+                    return;
+                } else {
+                    layer.msg(result.msg, {icon: 2});
+                }
+            }, function (ret) {
+                console.log(ret);
+                admin.errorBack(ret);
+            });
+            return false;
+        });
+    });
+</script>
\ No newline at end of file
diff --git a/payapi/src/main/resources/templates/system/notice/details.html b/payapi/src/main/resources/templates/system/notice/details.html
new file mode 100644
index 0000000..e2de243
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/details.html
@@ -0,0 +1,86 @@
+<form id="notice-publish-details" lay-filter="notice-publish-details" class="layui-form model-form" style="padding-top: 20px;">
+    <div class="layui-form-item">
+        <label class="layui-form-label">标题</label>
+        <div class="layui-input-block">
+            <input name="title" type="text" class="layui-input" readonly="readonly"/>
+        </div>
+    </div>
+    <div class="layui-form-item">
+        <label class="layui-form-label">内容</label>
+        <div class="layui-input-block">
+            <textarea name="content" class="layui-textarea" readonly="readonly" rows="5"></textarea>
+        </div>
+    </div>
+
+    <div class="layui-form-item">
+        <input type="hidden" name="noticeId" id="notice-publish-details-noticeId"/>
+        <div style="margin-left: 45px;" id="noticePublishDetailTableDiv">
+            <table class="layui-table" id="noticePublishDetailTable"
+                   lay-filter="noticePublishDetailTable-filter"></table>
+        </div>
+    </div>
+
+    <div class="layui-form-item model-form-footer">
+        <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">关闭</button>
+    </div>
+</form>
+
+<script>
+    layui.use(['layer', 'admin', 'form', 'table'], function () {
+        var layer = layui.layer;
+        var admin = layui.admin;
+        var form = layui.form;
+        var table = layui.table;
+
+        var bean = admin.getTempData('t_noticePublishDetail');
+        if (bean) {
+            if (isempty(bean.linkurl)) {
+                form.val('notice-publish-details', {
+                    noticeId: bean.id,
+                    title: bean.title,
+                    content: bean.content
+                });
+            } else {
+                form.val('notice-publish-details', {
+                    noticeId: bean.id,
+                    title: bean.title,
+                    content: bean.content + '\n\n链接:' + bean.linkurl
+                });
+            }
+        }
+
+        table.render({
+            elem: '#noticePublishDetailTable',
+            url: '[[@{/notice/detaillist}]]',
+            where: {
+                noticeId: $("#notice-publish-details-noticeId").val()
+            },
+            page: false,
+            size: 'sm',
+            height: 275,
+            cols: [
+                [
+                    {type: 'numbers', title: '编号', align: 'center', width: 65},
+                    {field: 'publisher', title: '发布者', align: 'center', width: 100},
+                    {
+                        field: 'createtime', title: '发布时间', align: 'right', width: 150, templet: function (e) {
+                            return admin.formatDate(e.createtime);
+                        }
+                    },
+                    {
+                        field: 'pushtime', title: '推送时间', align: 'right', width: 175, templet: function (e) {
+                            if (null != e.pushtime) {
+                                return admin.formatDate(e.pushtime);
+                            } else if (null != e.pushSettime) {
+                                return '<span style=\"color:red;\">预计</span> ' + admin.formatDate(e.pushSettime);
+                            } else {
+                                return '-';
+                            }
+                        }
+                    },
+                    {field: 'pushresult', title: '备注', align: 'center'},
+                ]
+            ]
+        });
+    });
+</script>
\ No newline at end of file
diff --git a/payapi/src/main/resources/templates/system/notice/index.html b/payapi/src/main/resources/templates/system/notice/index.html
new file mode 100644
index 0000000..02c000c
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/index.html
@@ -0,0 +1,250 @@
+<div class="layui-card">
+    <div class="layui-card-header">
+        <h2 class="header-title">手机通知公告</h2>
+        <span class="layui-breadcrumb pull-right">
+          <a href="#">系统中心</a>
+          <a><cite>手机通知公告</cite></a>
+        </span>
+    </div>
+    <div class="layui-card-body">
+        <div class="layui-form" lay-filter="notice-search-form">
+            <div class="layui-form-item" style="margin-bottom: 0;">
+                <div class="layui-inline">
+                    <label class="layui-form-label">创建日期</label>
+                    <div class="layui-input-inline">
+                        <input type="text" name="startdate" id="notice-search-startdate" placeholder="起始日期"
+                               autocomplete="off" class="layui-input"/>
+                    </div>
+                    <div class="layui-form-mid">-</div>
+                    <div class="layui-input-inline">
+                        <input type="text" name="enddate" id="notice-search-enddate" placeholder="截止日期"
+                               autocomplete="off" class="layui-input"/>
+                    </div>
+                </div>
+                <div class="layui-inline" style="margin-right: 20px;">
+                    <label class="layui-form-label">标题</label>
+                    <div class="layui-input-block" style="width: 265px;">
+                        <input type="text" name="searchkey" id="notice-search-searchkey" maxlength="20"
+                               autocomplete="off" class="layui-input"/>
+                    </div>
+                </div>
+                <div class="layui-inline" style="margin-right: 20px;margin-top: 10px;">
+                    <div class="layui-input-block" style="width: 200px;margin-left: 10px;">
+                        <button id="notice-search-btn" class="layui-btn icon-btn"><i class="layui-icon">&#xe615;</i>搜 索
+                        </button>
+                        <button id="notice-search-btn-reset" class="layui-btn layui-btn-primary">清 空</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="layui-card-body">
+        <div class="layui-form toolbar">
+            <button class="layui-btn layui-btn-sm" id="btn-notice-add"><i
+                    class="layui-icon">&#xe654;</i>新 增
+            </button>
+        </div>
+        <table class="layui-table" id="noticeSearchTable" lay-filter="noticeSearchTable-filter"></table>
+    </div>
+</div>
+
+<!-- 表格操作列 -->
+<script type="text/html" id="notice-table-bar">
+    <a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="publish">发布</a>
+    {{# if(d.sendcnt == 0){ }}
+    <a class="layui-btn layui-btn-xs" lay-event="modify">修改</a>
+    <a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete">删除</a>
+    {{# } else{ }}
+    <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="detail">发布详情</a>
+    {{# } }}
+</script>
+
+<script>
+    layui.use(['form', 'table', 'layer', 'admin', 'laydate'], function () {
+        var form = layui.form;
+        var table = layui.table;
+        var admin = layui.admin;
+        var laydate = layui.laydate;
+
+        laydate.render({
+            elem: '#notice-search-startdate',
+            trigger: 'click'
+        });
+        laydate.render({
+            elem: '#notice-search-enddate',
+            trigger: 'click'
+        });
+
+        table.render({
+            elem: '#noticeSearchTable',
+            url: '[[@{/notice/list}]]',
+            page: true,
+            cols: [
+                [
+                    {align: 'left', title: '操作', width: 170, toolbar: '#notice-table-bar', fixed: 'left'},
+                    {field: 'title', title: '消息标题', align: 'left', width: 160, fixed: 'left', sort: true},
+                    {field: 'content', title: '消息内容', align: 'left'},
+                    {
+                        field: 'platform',
+                        title: '推送终端',
+                        align: 'center',
+                        width: 120,
+                        sort: true,
+                        templet: function (d) {
+                            if ('all' == d.platform) {
+                                return '全部手机';
+                            } else if ('ios' == d.platform) {
+                                return '苹果手机';
+                            } else if ('android' == d.platform) {
+                                return '安卓手机';
+                            } else {
+                                return d.platform;
+                            }
+                        }
+                    },
+                    {field: 'sendcnt', title: '发布次数', align: 'center', width: 120, sort: true},
+                    {field: 'creator', title: '创建者', align: 'center', width: 140, sort: true},
+                    {
+                        field: 'createdate',
+                        title: '创建时间',
+                        align: 'center',
+                        width: 170,
+                        sort: true,
+                        templet: function (d) {
+                            return admin.formatDate(d.createdate + '' + d.createtime);
+                        }
+                    },
+                    {
+                        field: 'lastsaved',
+                        title: '最后更新时间',
+                        align: 'center',
+                        width: 170,
+                        sort: true,
+                        templet: function (d) {
+                            return admin.formatDate(d.lastsaved);
+                        }
+                    }
+                ]
+            ]
+        });
+
+        $("#notice-search-btn").click(function () {
+            table.reload('noticeSearchTable', {
+                where: {
+                    startdate: $("#notice-search-startdate").val(),
+                    enddate: $("#notice-search-enddate").val(),
+                    searchkey: $("#notice-search-searchkey").val()
+                }, page: {curr: 1}
+            });
+        });
+
+        $("#notice-search-btn-reset").click(function () {
+            $("#notice-search-startdate").val("");
+            $("#notice-search-enddate").val("");
+            $("#notice-search-searchkey").val("");
+        });
+
+        $("#btn-notice-add").click(function () {
+            showNoticeModel("新增公告", {
+                noticeId: '',
+                platform: 'all',
+                title: '',
+                content: '',
+                linkurl: ''
+            })
+        });
+
+        function showNoticeModel(title, data) {
+            admin.putTempData('t_noticeTmp', data);
+            admin.popupCenter({
+                title: title,
+                area: '650px',
+                path: '[[@{/notice/load4add}]]',
+                finish: function () {
+                    table.reload('noticeSearchTable', {});
+                }
+            });
+        }
+
+        function showNoticePublishModel(data) {
+            admin.putTempData('t_noticePubidTmp', {
+                noticeId: data.id,
+                pushmode: 'atonce',
+                settime: ''
+            });
+            admin.popupCenter({
+                title: '发布通知公告【' + data.title + '】',
+                area: '550px',
+                path: '[[@{/notice/load4publish}]]',
+                finish: function () {
+                    table.reload('noticeSearchTable', {});
+                }
+            });
+        }
+
+        function deleteNotice(data) {
+            layer.confirm('确定删除公告【' + data.title + '】吗?', function (i) {
+                layer.close(i);
+                layer.load(2);
+                admin.go('[[@{/notice/delete}]]', {
+                    noticeId: data.id,
+                    _csrf: $("meta[name='_csrf_token']").attr("value")
+                }, function (data) {
+                    layer.closeAll('loading');
+                    if (data.code == 200) {
+                        layer.msg(data.msg, {icon: 1});
+                    } else if (data.code == 401) {
+                        layer.msg(data.msg, {icon: 2, time: 1500}, function () {
+                            location.replace('[[@{/login}]]');
+                        }, 1000);
+                        return;
+                    } else {
+                        layer.msg(data.msg, {icon: 2});
+                    }
+                    table.reload('noticeSearchTable');
+                }, function (ret) {
+                    console.log(ret);
+                    admin.errorBack(ret);
+                });
+            })
+        }
+
+        function showNoticePublishDetailsModel(data) {
+            admin.putTempData('t_noticePublishDetail', data);
+            admin.popupCenter({
+                title: '通知公告【' + data.title + '】发布详情',
+                area: '750px',
+                path: '[[@{/notice/load4detail}]]',
+                finish: function () {
+                    table.reload('noticeSearchTable', {});
+                }
+            });
+        }
+
+
+        //监听单元格
+        table.on('tool(noticeSearchTable-filter)', function (obj) {
+            var data = obj.data;
+            switch (obj.event) {
+                case "publish":
+                    showNoticePublishModel(data);
+                    break;
+                case "modify":
+                    showNoticeModel("修改公告", {
+                        noticeId: data.id,
+                        platform: data.platform,
+                        title: data.title,
+                        content: data.content,
+                        linkurl: data.linkurl
+                    });
+                    break;
+                case "delete":
+                    deleteNotice(data);
+                    break;
+                case "detail":
+                    showNoticePublishDetailsModel(data);
+                    break;
+            }
+        });
+    });
+</script>
diff --git a/payapi/src/main/resources/templates/system/notice/publish.html b/payapi/src/main/resources/templates/system/notice/publish.html
new file mode 100644
index 0000000..621749b
--- /dev/null
+++ b/payapi/src/main/resources/templates/system/notice/publish.html
@@ -0,0 +1,85 @@
+<form id="notice-publish-form" lay-filter="notice-publish-form" class="layui-form model-form">
+    <input name="noticeId" id="hidden-notice-publish-noticeid" type="hidden"/>
+    <div class="layui-form-item">
+        <label class="layui-form-label"><span style="color: red;">*</span>发布时间</label>
+        <div class="layui-input-block">
+            <input type="radio" name="pushmode" value="atonce" title="立即发布" lay-filter="notice-publish-form-pushmode-filter"/>
+            <input type="radio" name="pushmode" value="delay" title="定时发布" lay-filter="notice-publish-form-pushmode-filter"/>
+        </div>
+    </div>
+    <div class="layui-form-item" id="hidden-div-set-delaytime" style="display: none;">
+        <label class="layui-form-label"><span style="color: red;">*</span>设定时间</label>
+        <div class="layui-input-block">
+            <input name="settime" type="text" id="notice-publish-form-settime" style="width: 195px;" class="layui-input" autocomplete="off" lay-verify="pushmode"/>
+        </div>
+    </div>
+
+    <div class="layui-form-item model-form-footer">
+        <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
+        <button class="layui-btn" lay-filter="notice-publish-form-submit" lay-submit id="notice-publish-submit-btn">保存</button>
+    </div>
+</form>
+
+<script>
+    layui.use(['layer', 'admin', 'form', 'laydate'], function () {
+        var layer = layui.layer;
+        var admin = layui.admin;
+        var form = layui.form;
+        var laydate = layui.laydate;
+
+        form.render('radio');
+        form.verify({
+            pushmode: function (value, item) {
+                var pushmode = $("#notice-publish-form").find("input[name='pushmode']:checked").val();
+                if('atonce' != pushmode && isempty(value)){
+                    return "定时发布请设定时间";
+                }
+            }
+        });
+        form.on('radio(notice-publish-form-pushmode-filter)', function (data) {
+            if('atonce'==data.value){
+                $("#notice-publish-form-settime").val("");
+                $("#hidden-div-set-delaytime").hide();
+            }else{
+                $("#hidden-div-set-delaytime").show();
+            }
+        });
+        laydate.render({
+            elem: '#notice-publish-form-settime',
+            type: 'datetime',
+            min: moment().locale('zh-cn').format('YYYY-MM-DD HH:mm:ss'),
+            trigger: 'click'
+        });
+        var bean = admin.getTempData('t_noticePubidTmp');
+        if (bean) {
+            form.val('notice-publish-form', bean);
+        }
+
+        // 表单提交事件
+        form.on('submit(notice-publish-form-submit)', function (data) {
+            layer.load(2);
+            var param = data.field;
+            var token = $("meta[name='_csrf_token']").attr("value");
+            param["_csrf"] = token;
+            admin.go('[[@{/notice/publish}]]', param, function (result) {
+                console.log(result);
+                layer.closeAll('loading');
+                if (result.code == 200) {
+                    layer.msg(result.msg, {icon: 1});
+                    admin.finishPopupCenter();
+                } else if (result.code == 401) {
+                    layer.msg(result.msg, {icon: 2, time: 1500}, function () {
+                        location.replace('[[@{/login}]]');
+                    }, 1000);
+                    return;
+                } else {
+                    layer.msg(result.msg, {icon: 2});
+                }
+            }, function (ret) {
+                console.log(ret);
+                admin.errorBack(ret);
+            });
+            return false;
+        });
+    });
+</script>
\ No newline at end of file