瀏覽代碼

feature(报告提醒): 1、计划回款到期提醒需求开发
2、售后运维到期提醒统计表开发、售后运维到期提醒功能实现
3、消息提醒类型可配置功能实现

lk 2 年之前
父節點
當前提交
4b65c796c4

+ 11 - 11
opms_admin/app/model/sys_message.go

@@ -23,21 +23,21 @@ type SysMessageSearchReq struct {
 }
 
 type CreateSysMessageReq struct {
-	MsgTitle    string `json:"msgTitle"      v:"required#消息标题不能为空"`                             // 消息标题
-	MsgContent  string `json:"msgContent"    v:"required#消息内容不能为空"`                             // 消息内容
-	MsgType     string `json:"msgType"       v:"required|in:10,20,30#消息类别不能为空|消息类别只能为10、20、30"` // 消息类别(10公告20消息30审批)
-	MsgStatus   string `json:"msgStatus"     v:"required|in:10,20#消息状态不能为空|消息状态只能为10或20"`       // 消息状态(10正常20关闭)
-	RecvUserIds string `json:"recvUserIds"`                                                     // 接收用户
-	SendType    string `json:"sendType"`                                                        // 发送方式
-	OpnUrl      string `json:"opnUrl"`                                                          // 操作链接
-	IsRead      string `json:"isRead"`                                                          // 是否已读(10否20是)
-	Remark      string `json:"remark"`                                                          // 备注
+	MsgTitle    string `json:"msgTitle"      v:"required#消息标题不能为空"`                                          // 消息标题
+	MsgContent  string `json:"msgContent"    v:"required#消息内容不能为空"`                                          // 消息内容
+	MsgType     string `json:"msgType"       v:"required|in:10,20,30,40#消息类别不能为空|消息类别只能为10、20、30、40"` // 消息类别(10公告20消息30审批40文件
+	MsgStatus   string `json:"msgStatus"     v:"required|in:10,20#消息状态不能为空|消息状态只能为10或20"`            // 消息状态(10正常20关闭)
+	RecvUserIds string `json:"recvUserIds"`                                                                          // 接收用户
+	SendType    string `json:"sendType"`                                                                             // 发送方式
+	OpnUrl      string `json:"opnUrl"`                                                                               // 操作链接
+	IsRead      string `json:"isRead"`                                                                               // 是否已读(10否20是)
+	Remark      string `json:"remark"`                                                                               // 备注
 }
 type SendMessageReq struct {
 	MsgTitle    string `json:"msgTitle"      v:"required#消息标题不能为空"` // 消息标题
 	MsgContent  string `json:"msgContent"    v:"required#消息内容不能为空"` // 消息内容
-	RecvUserIds string `json:"recvUserIds"`                         // 接收用户
-	OpnUrl      string `json:"opnUrl"`                              //附件地址
+	RecvUserIds string `json:"recvUserIds"`                                 // 接收用户
+	OpnUrl      string `json:"opnUrl"`                                      //附件地址
 }
 
 type UpdateSysMessageReq struct {

+ 16 - 2
opms_admin/app/service/sys_message.go

@@ -134,13 +134,15 @@ func (s *MessageService) Create(req *model.CreateSysMessageReq) (err error) {
 	for _, v := range gset.NewStrSetFrom(sendMsgType).Slice() {
 		switch v {
 		case "10": // 10:websocket
-			go BatchSendMessageNotify(userIds, *data)
+			//go BatchSendMessageNotify(userIds, *data)
+			s.handleMsgBySetting(userIds, data)
 			fmt.Println(v, "10")
 		case "20": // 20:邮件
 			go s.BatchSendUserEmailMsg(userIds, data.MsgTitle, data.MsgContent)
 			fmt.Println(v, "20")
 		case "30": // 30:钉钉
-			go s.BatchSendUserDingTalkTextMsg(userIds, data.MsgTitle, data.MsgContent)
+			//go s.BatchSendUserDingTalkTextMsg(userIds, data.MsgTitle, data.MsgContent)
+			s.handleMsgBySetting(userIds, data)
 			fmt.Println(v, "30")
 		case "40": // 40:微信小程序订阅消息
 
@@ -151,6 +153,18 @@ func (s *MessageService) Create(req *model.CreateSysMessageReq) (err error) {
 	return
 }
 
+// 按照配置处理消息
+func (s *MessageService) handleMsgBySetting(userIds []string, data *model.SysMessage) {
+	msgType := g.Config().GetString("message.type")
+	if msgType == "websocket" {
+		go BatchSendMessageNotify(userIds, *data)
+	} else if msgType == "dingding" {
+		go s.BatchSendUserDingTalkTextMsg(userIds, data.MsgType, data.MsgContent)
+	} else {
+		fmt.Println("非法的消息类型:" + msgType)
+	}
+}
+
 // SendMail 发送邮件
 func (s *MessageService) SendMail(req *model.SendMessageReq) (err error) {
 	data := new(model.SysMessage)

+ 23 - 8
opms_admin/app/service/sys_send_message.go

@@ -94,7 +94,7 @@ func (c *contextService) SendUserDingTalkTextMsg(userId, msgTitle, msgContent st
 }
 
 // 批量发送钉钉
-func (c *contextService) BatchSendUserDingTalkTextMsg(userIds []string, msgTitle, msgContent string) error {
+func (c *contextService) BatchSendUserDingTalkTextMsg(userIds []string, msgType, msgContent string) error {
 	userInfos, err := dao.NewSysUserDao(c.Tenant).Fields(dao.SysUser.C.NickName, dao.SysUser.C.DingtalkUid).WherePri(userIds).All()
 	if err != nil {
 		g.Log().Error(err)
@@ -111,15 +111,30 @@ func (c *contextService) BatchSendUserDingTalkTextMsg(userIds []string, msgTitle
 		dingtalkUids = append(dingtalkUids, userInfo.DingtalkUid)
 		userNickNameList = append(userNickNameList, userInfo.NickName)
 	}
-	dingtalkSendMsgReq := &corpconversation.SendCorpConversationRequest{
-		UseridList: strings.Join(dingtalkUids, ","),
-		Msg: corpconversation.Msg{
-			Msgtype: "text",
-			Text: corpconversation.TextMsg{
-				Content: msgContent,
+	dingtalkSendMsgReq := new(corpconversation.SendCorpConversationRequest)
+	// 文件处理
+	if msgType != "40" {
+		dingtalkSendMsgReq = &corpconversation.SendCorpConversationRequest{
+			UseridList: strings.Join(dingtalkUids, ","),
+			Msg: corpconversation.Msg{
+				Msgtype: "text",
+				Text: corpconversation.TextMsg{
+					Content: msgContent,
+				},
 			},
-		},
+		}
+	} else {
+		dingtalkSendMsgReq = &corpconversation.SendCorpConversationRequest{
+			UseridList: strings.Join(dingtalkUids, ","),
+			Msg: corpconversation.Msg{
+				Msgtype: "file",
+				File: corpconversation.FileMsg{
+					MediaId: msgContent,
+				},
+			},
+		}
 	}
+
 	err = c.BaseSendUserDingTalkMsg(dingtalkSendMsgReq, userNickNameList)
 	if err != nil {
 		return err

+ 4 - 1
opms_admin/config/config.toml

@@ -60,4 +60,7 @@
     host="hwsmtp.exmail.qq.com"
     port="465"
     user="likai@dashoo.cn"
-    password="LLLkkk0210"
+    password="LLLkkk0210"
+
+[message]
+    type="dingding" # dingding / websocket

+ 97 - 0
opms_libary/plugin/dingtalk/base/base.go

@@ -2,11 +2,16 @@ package base
 
 import (
 	"bytes"
+	"dashoo.cn/opms_libary/myerrors"
 	"dashoo.cn/opms_libary/plugin/dingtalk/context"
 	"encoding/json"
 	"fmt"
+	"io"
 	"io/ioutil"
+	"mime/multipart"
 	"net/http"
+	"os"
+	"path/filepath"
 	"strings"
 )
 
@@ -127,3 +132,95 @@ func PostJSON(url string, obj interface{}, token string) ([]byte, error) {
 	}
 	return ioutil.ReadAll(response.Body)
 }
+
+// HTTPPostFormDataWithAccessToken 使用multipart/form-data POST上传文件
+func (c *Base) HTTPPostFormDataWithAccessToken(url string, obj map[string]string) (resp []byte, err error) {
+	retry := 1
+Do:
+	var accessToken string
+	accessToken, err = c.GetAccessToken()
+	if err != nil {
+		return
+	}
+
+	var target = ""
+	if strings.Contains(url, "http") {
+		target = fmt.Sprintf("%s?access_token=%s", url, accessToken)
+	} else {
+		if strings.Contains(url, "?") {
+			target = fmt.Sprintf("%s%s", BaseApiUrl, url)
+		} else {
+			target = fmt.Sprintf("%s%s", BaseApiUrl, url)
+		}
+	}
+	resp, err = PostFormData(target, obj, accessToken)
+
+	if err != nil {
+		if retry > 0 {
+			retry--
+			c.CleanAccessTokenCache()
+			goto Do
+		}
+		return
+	}
+	return
+}
+
+// PostFormData  form-data 提交数据
+func PostFormData(url string, bodyData map[string]string, token string) ([]byte, error) {
+	// Create a new buffer to store the form data
+	body := new(bytes.Buffer)
+	writer := multipart.NewWriter(body)
+	// Create a new form file field
+	fileField, err := writer.CreateFormFile(bodyData["____fileData"], filepath.Base(bodyData["____filePath"]))
+	if err != nil {
+		return nil, myerrors.New(500, err, err.Error())
+	}
+
+	// 读取文件
+	file, err := os.Open(bodyData["____filePath"])
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	// Copy the file contents to the form file field
+	_, err = io.Copy(fileField, file)
+	if err != nil {
+		return nil, myerrors.New(500, err, err.Error())
+	}
+
+	for key, val := range bodyData {
+		if !strings.Contains(key, "----") {
+			_ = writer.WriteField(key, val)
+		}
+	}
+
+	// Close the writer to finalize the form data
+	err = writer.Close()
+	if err != nil {
+		return nil, myerrors.New(500, err, err.Error())
+	}
+
+	// Create a new HTTP request with the form data
+	request, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, myerrors.New(500, err, err.Error())
+	}
+
+	// Set the Content-Type header to multipart/form-data
+	request.Header.Set("Content-Type", writer.FormDataContentType())
+
+	// Send the request and get the response
+	response, err := (&http.Client{}).Do(request)
+	if err != nil {
+		return nil, err
+	}
+	defer response.Body.Close()
+
+	fmt.Println(response.StatusCode, response.Status)
+	if response.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", url, response.StatusCode)
+	}
+	return ioutil.ReadAll(response.Body)
+}

+ 18 - 2
opms_libary/plugin/dingtalk/message/corpconversation/corpconversation.go

@@ -10,8 +10,9 @@ import (
 
 const (
 	//POST https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token=ACCESS_TOKEN
-	SendCorpConversationUrl = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"  //添加权限
-	GetSendResultUrl        = "https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult" //添加权限
+	SendCorpConversationUrl   = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"  //添加权限
+	GetSendResultUrl          = "https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult" //添加权限
+	UploadConversationFileUrl = "https://oapi.dingtalk.com/media/upload"                                  //上传文件
 
 )
 
@@ -48,3 +49,18 @@ func (c *CorpConversation) GetSendResult(request *GetSendResultRequest) (respons
 	err = json.Unmarshal(resp, &response)
 	return response, err
 }
+
+// UploadConversationFile 发送工作通知--上传文件
+func (c *CorpConversation) UploadConversationFile(req *UploadConversationFileRequest) (response UploadConversationFileResponse, err error) {
+	request := make(map[string]string, 0)
+	request["type"] = req.Type
+	request["____fileData"] = req.FileData
+	request["____filePath"] = req.FilePath
+
+	resp, _ := c.HTTPPostFormDataWithAccessToken(UploadConversationFileUrl, request)
+	if err != nil {
+		return
+	}
+	err = json.Unmarshal(resp, &response)
+	return response, err
+}

+ 18 - 0
opms_libary/plugin/dingtalk/message/corpconversation/entity.go

@@ -47,6 +47,24 @@ type SendCorpConversationResponse struct {
 	Errmsg    string `json:"errmsg"`
 }
 
+// 发送工作通知--上传文件请求
+type UploadConversationFileRequest struct {
+	Type string `json:"type"` // file:普通文件,最大20MB。支持上传doc、docx、xls、xlsx、ppt、pptx、zip、pdf、rar格式。
+	//Media    string `json:"media"`        // 示例:C:/Users/Desktop/222.png
+	FileData string `json:"____fileData"` // 文件匹配字段
+	FilePath string `json:"____filePath"` // 文件路径
+}
+
+// 发送工作通知--上传文件响应
+type UploadConversationFileResponse struct {
+	RequestId string `json:"request_id"` // 请求ID。
+	Type      string `json:"type"`       // file:普通文件,最大20MB。支持上传doc、docx、xls、xlsx、ppt、pptx、zip、pdf、rar格式。
+	MediaId   string `json:"media_id"`   // 媒体文件上传后获取的唯一标识。
+	CreatedAt int64  `json:"created_at"` // 媒体文件上传时间戳。
+	Errcode   int64  `json:"errcode"`
+	Errmsg    string `json:"errmsg"`
+}
+
 type Msg struct {
 	Msgtype    string        `json:"msgtype"` // text,image,voice,file,link,oa,markdown,action_card
 	Text       TextMsg       `json:"text,omitempty"`

+ 16 - 0
opms_parent/app/handler/contract/report.go

@@ -42,3 +42,19 @@ func (c *ContractReportHandler) QueryContractNum(ctx context.Context, req *model
 	rsp.Data = data
 	return nil
 }
+
+// QueryContractExpireNum 售后运维到期统计表
+func (c *ContractReportHandler) QueryContractExpireNum(ctx context.Context, req *model.QueryNumReq, rsp *comm_def.CommonMsg) error {
+	g.Log().Infof("ContractReport.QueryContractExpireNum request %#v ", *req)
+	s, err := service.NewContractReportService(ctx)
+	if err != nil {
+		return err
+	}
+	data, err := s.QueryContractExpireNum(req.Date)
+	if err != nil {
+		return err
+	}
+
+	rsp.Data = data
+	return nil
+}

+ 2 - 2
opms_parent/app/handler/dingtalk/ding_event.go

@@ -36,8 +36,8 @@ func (h *DingHandler) CallBack(ctx context.Context, req *message.SubsMessage, rs
 		switch msg.EventType {
 		case message.EventCheckUrl:
 			return h.handleCheckUrl(msg)
-		case message.EventCalendarChange:
-			return h.handleCalendarChange(msg, handler.Context)
+		case message.EventCalendarChange: // 钉钉日程不再同步到本地系统
+			//return h.handleCalendarChange(msg, handler.Context)
 		case message.BpmsInstanceChange:
 			return h.handleBpmsInstanceChange(msg, handler.Context)
 		case message.BpmsTaskChange:

+ 9 - 9
opms_parent/app/model/work/deliver_order_imp_progress.go

@@ -27,15 +27,15 @@ type DeliverOrderProgressListReq struct {
 }
 
 type DeliverOrderProgressAddReq struct {
-	PlanId          int         `json:"planId" v:"required#请输入工单计划ID"`        // 关联实施计划
-	ProgressTitle   string      `json:"progressTitle" v:"required#请输入任务标题"`   // 任务标题
-	ProgressContext string      `json:"progressContext" v:"required#请输入任务内容"` // 任务内容
-	StartDate       *gtime.Time `json:"startDate" v:"required#请输入开始时间"`       // 开始时间
-	EndDate         *gtime.Time `json:"endDate" v:"required#请输入结束时间"`         // 结束时间
-	ReaStartDate    *gtime.Time `json:"reaStartDate"`                         // 实际开始时间
-	ReaEndDate      *gtime.Time `json:"reaEndDate"`                           // 实际结束时间
-	ProgressLevel   string      `json:"progressLevel"`                        // 优先级(10最高 20普通 30较低 )
-	Remark          string      `json:"remark"`                               // 备注
+	PlanId          int         `json:"planId" v:"required#请输入工单计划ID"`      // 关联实施计划
+	ProgressTitle   string      `json:"progressTitle" v:"required#请输入任务标题"` // 任务标题
+	ProgressContext string      `json:"progressContext"`                           // 任务内容
+	StartDate       *gtime.Time `json:"startDate" v:"required#请输入开始时间"`     // 开始时间
+	EndDate         *gtime.Time `json:"endDate" v:"required#请输入结束时间"`       // 结束时间
+	ReaStartDate    *gtime.Time `json:"reaStartDate"`                              // 实际开始时间
+	ReaEndDate      *gtime.Time `json:"reaEndDate"`                                // 实际结束时间
+	ProgressLevel   string      `json:"progressLevel"`                             // 优先级(10最高 20普通 30较低 )
+	Remark          string      `json:"remark"`                                    // 备注
 
 }
 

+ 1 - 1
opms_parent/app/model/work/work_order.go

@@ -32,7 +32,7 @@ type WorkOrderSearchReq struct {
 	SaleName         string      `json:"saleName"`         //销售工程师
 	UpdatedTimeStart *gtime.Time `json:"updatedTimeStart"` // 更新时间
 	UpdatedTimeEnd   *gtime.Time `json:"updatedTimeEnd"`   // 更新时间
-	SupportTime      string      `json:"supportTime"`      // 支持时间
+	SupportTime      *[]string   `json:"supportTime"`      // 支持时间
 	ProductLine      string      `json:"productLine"`      // 产品线
 	request.PageReq
 }

+ 2 - 1
opms_parent/app/service/contract/ctr_contract.go

@@ -799,7 +799,8 @@ func ContractApplyApproval(ctx context.Context, flow *workflowModel.PlatWorkflow
 	}
 
 	return contractDao.DB.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
-		_, err = worksrv.DeliverOrderAdd(tx, contractId, request.UserInfo{}, nil)
+		// 交付状态(0发起 10项目立项 15进行中 20 完成 30审批拒绝40关闭)
+		_, err = worksrv.DeliverOrderAdd(tx, contractId, request.UserInfo{}, nil, "10")
 		return err
 	})
 }

+ 309 - 0
opms_parent/app/service/contract/ctr_contract_cron.go

@@ -0,0 +1,309 @@
+package service
+
+import (
+	"dashoo.cn/micro/app/service"
+	"dashoo.cn/opms_libary/plugin/dingtalk"
+	"dashoo.cn/opms_libary/plugin/dingtalk/message/corpconversation"
+	"database/sql"
+	"fmt"
+	"github.com/360EntSecGroup-Skylar/excelize"
+	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/os/glog"
+	"github.com/gogf/gf/os/gtime"
+	"github.com/gogf/gf/util/gconv"
+	"github.com/robfig/cron"
+	"os"
+)
+
+// 初始化,创建每10分钟执行一次的定时任务
+func init() {
+	// 定时任务
+	c := cron.New()
+	spec1 := "0 0 20 * * 0" // 每周日晚八点
+	spec2 := "0 0 20 1 * *" // 每月1号晚八点
+
+	if err := c.AddJob(spec1, CollectionCron{}); err != nil {
+		glog.Error(err)
+	}
+	if err := c.AddJob(spec2, ContractCron{}); err != nil {
+		glog.Error(err)
+	}
+
+	c.Start()
+}
+
+// CollectionCron 回款定义
+type CollectionCron struct {
+}
+
+// ContractCron 合同任务定义
+type ContractCron struct {
+}
+
+// Run 计划回款到期提醒
+func (c CollectionCron) Run() {
+	tenant := g.Config().GetString("micro_srv.tenant")
+	if tenant == "" {
+		glog.Error("交接任务单定时任务租户码未设置,请前往配置")
+		return
+	}
+	// 当前时间
+	now := gtime.Now()
+	collections, err := g.DB(tenant).Model("ctr_contract_collection_plan p").InnerJoin("ctr_contract c", "c.id=p.contract_id").Where(fmt.Sprintf("(p.contract_status='10' OR p.contract_status='20') AND p.plan_datetime>='%v'", now.Format("Y-m-d 00:00:00"))).Fields("p.*,c.incharge_id,c.incharge_name").FindAll()
+	if err != nil && err != sql.ErrNoRows {
+		glog.Error(err)
+		return
+	}
+
+	data := make(map[int][]map[string]string)
+
+	for _, collection := range collections {
+		// 校验当前时间
+		remindTime := collection["cashed_datetime"].GTime()
+		if isMatchDate(remindTime, 5) || isMatchDate(remindTime, 15) || isMatchDate(remindTime, 30) || isMatchDate(remindTime, 60) || isMatchDate(remindTime, 90) {
+			id := gconv.Int(collection["incharge_id"])
+			if _, ok := data[id]; !ok {
+				data[id] = make([]map[string]string, 0)
+			}
+			d := map[string]string{
+				"contract_code":   gconv.String(collection["contract_code"]),
+				"cashed_datetime": collection["cashed_datetime"].GTime().Format("Y-m-d"),
+				"cust_name":       gconv.String(collection["cust_name"]),
+				"plan_amount":     fmt.Sprintf("%.2f", collection["plan_amount"].Float64()),
+			}
+			data[id] = append(data[id], d)
+		}
+	}
+	if len(data) > 0 {
+		for id, collections := range data {
+			f := excelize.NewFile()
+			// Create a new sheet.
+			sheet := f.NewSheet("Sheet1")
+
+			// 表头
+			f.SetCellValue("Sheet1", Div(1)+"1", "合同编号")
+			f.SetCellValue("Sheet1", Div(2)+"1", "计划回款时间")
+			f.SetCellValue("Sheet1", Div(3)+"1", "客户名称")
+			f.SetCellValue("Sheet1", Div(4)+"1", "回款金额")
+
+			for index, collection := range collections {
+				f.SetCellValue("Sheet1", Div(1)+gconv.String(index+2), collection["contract_code"])
+				f.SetCellValue("Sheet1", Div(2)+gconv.String(index+2), collection["cashed_datetime"])
+				f.SetCellValue("Sheet1", Div(3)+gconv.String(index+2), collection["cust_name"])
+				f.SetCellValue("Sheet1", Div(4)+gconv.String(index+2), collection["plan_amount"])
+			}
+
+			// Set active sheet of the workbook.
+			f.SetActiveSheet(sheet)
+			// Save xlsx file by the given path.
+			file := "./temp/计划回款到期合同.xlsx"
+			_, err = createPath("./temp")
+			if err != nil {
+				glog.Error(err)
+				return
+			}
+			if err = f.SaveAs(file); err != nil {
+				glog.Error(err)
+				return
+			}
+
+			fileId, err := uploadFile(file)
+			if err != nil {
+				glog.Error(err)
+				return
+			}
+
+			notifyMessage("计划回款到期提醒", gconv.String(id), fileId)
+			// 删除已存在的文件
+			err = os.Remove(file)
+			if err != nil {
+				glog.Error(err)
+				return
+			}
+		}
+	}
+}
+
+// Run 售后运维到期提醒
+func (c ContractCron) Run() {
+	tenant := g.Config().GetString("micro_srv.tenant")
+	if tenant == "" {
+		glog.Error("交接任务单定时任务租户码未设置,请前往配置")
+		return
+	}
+	// 当前时间
+	date := gtime.Now().AddDate(1, 0, 0)
+	// 获取数据
+	contracts, err := g.DB(tenant).Model("ctr_contract").Where(fmt.Sprintf("appro_status='30' AND contract_end_time LIKE '%v%%'", date.Format("Y-m"))).Order("contract_end_time ASC").FindAll()
+	if err != nil && err != sql.ErrNoRows {
+		glog.Error(err)
+		return
+	}
+
+	data := make(map[int][]map[string]string)
+
+	for _, contract := range contracts {
+		id := gconv.Int(contract["incharge_id"])
+		if _, ok := data[id]; !ok {
+			data[id] = make([]map[string]string, 0)
+		}
+		d := map[string]string{
+			"contract_code":     gconv.String(contract["contract_code"]),
+			"contract_end_time": contract["contract_end_time"].GTime().Format("Y-m-d"),
+			"cust_name":         gconv.String(contract["cust_name"]),
+			"contract_amount":   fmt.Sprintf("%.2f", contract["contract_amount"].Float64()),
+		}
+		data[id] = append(data[id], d)
+	}
+	if len(data) > 0 {
+		for id, collections := range data {
+			f := excelize.NewFile()
+			// Create a new sheet.
+			sheet := f.NewSheet("Sheet1")
+
+			// 表头
+			f.SetCellValue("Sheet1", Div(1)+"1", "合同编号")
+			f.SetCellValue("Sheet1", Div(2)+"1", "合同结束时间")
+			f.SetCellValue("Sheet1", Div(3)+"1", "客户名称")
+			f.SetCellValue("Sheet1", Div(4)+"1", "合同总金额")
+
+			for index, collection := range collections {
+				f.SetCellValue("Sheet1", Div(1)+gconv.String(index+2), collection["contract_code"])
+				f.SetCellValue("Sheet1", Div(2)+gconv.String(index+2), collection["contract_end_time"])
+				f.SetCellValue("Sheet1", Div(3)+gconv.String(index+2), collection["cust_name"])
+				f.SetCellValue("Sheet1", Div(4)+gconv.String(index+2), collection["contract_amount"])
+			}
+
+			// Set active sheet of the workbook.
+			f.SetActiveSheet(sheet)
+			// Save xlsx file by the given path.
+			file := "./temp/售后运维到期提醒.xlsx"
+			_, err = createPath("./temp")
+			if err != nil {
+				glog.Error(err)
+				return
+			}
+			if err = f.SaveAs(file); err != nil {
+				glog.Error(err)
+				return
+			}
+
+			fileId, err := uploadFile(file)
+			if err != nil {
+				glog.Error(err)
+				return
+			}
+
+			notifyMessage("售后运维到期提醒", gconv.String(id), fileId)
+			// 删除已存在的文件
+			err = os.Remove(file)
+			if err != nil {
+				glog.Error(err)
+				return
+			}
+		}
+	}
+}
+
+func uploadFile(file string) (string, error) {
+	var request corpconversation.UploadConversationFileRequest
+	request.Type = "file"
+	request.FileData = "media"
+	request.FilePath = file
+
+	c := dingtalk.Client.GetCorpConversation()
+	resp, err := c.UploadConversationFile(&request)
+	if err != nil {
+		fmt.Println(err)
+		return "", err
+	}
+
+	return resp.MediaId, nil
+}
+
+// notifyMessage 发送消息通知
+func notifyMessage(title, ids, message string) {
+	msg := g.MapStrStr{
+		"msgTitle":    title,
+		"msgContent":  message,
+		"msgType":     "40", // 文件处理
+		"recvUserIds": ids,
+		"msgStatus":   "10",
+		"sendType":    "30",
+	}
+	if err := service.CreateSystemMessage(msg); err != nil {
+		glog.Error("消息提醒异常:", err)
+	}
+}
+
+func isMatchDate(date *gtime.Time, days int) bool {
+	begin := gtime.Now().AddDate(0, 0, days)
+	end := gtime.Now().AddDate(0, 0, days+7)
+	return begin.Format("Y-m-d 00:00:00") <= date.Format("Y-m-d H:i:s") && date.Format("Y-m-d H:i:s") <= end.Format("Y-m-d 23:59:59")
+}
+
+// 判断文件夹是否存在
+func pathExists(path string) (bool, error) {
+	_, err := os.Stat(path)
+	if err == nil {
+		return true, nil
+	}
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return false, err
+}
+
+func createPath(path string) (bool, error) {
+	exist, err := pathExists(path)
+	if err != nil {
+		return false, err
+	}
+
+	if exist {
+		return true, nil
+	} else {
+		// 创建文件夹
+		err := os.MkdirAll(path, os.ModePerm)
+		if err != nil {
+			return false, err
+		} else {
+			return true, err
+		}
+	}
+}
+
+// Div 数字转字母
+func Div(Num int) string {
+	var (
+		Str  string = ""
+		k    int
+		temp []int //保存转化后每一位数据的值,然后通过索引的方式匹配A-Z
+	)
+	//用来匹配的字符A-Z
+	Slice := []string{"", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O",
+		"P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}
+
+	if Num > 26 { //数据大于26需要进行拆分
+		for {
+			k = Num % 26 //从个位开始拆分,如果求余为0,说明末尾为26,也就是Z,如果是转化为26进制数,则末尾是可以为0的,这里必须为A-Z中的一个
+			if k == 0 {
+				temp = append(temp, 26)
+				k = 26
+			} else {
+				temp = append(temp, k)
+			}
+			Num = (Num - k) / 26 //减去Num最后一位数的值,因为已经记录在temp中
+			if Num <= 26 {       //小于等于26直接进行匹配,不需要进行数据拆分
+				temp = append(temp, Num)
+				break
+			}
+		}
+	} else {
+		return Slice[Num]
+	}
+	for _, value := range temp {
+		Str = Slice[value] + Str //因为数据切分后存储顺序是反的,所以Str要放在后面
+	}
+	return Str
+}

+ 44 - 0
opms_parent/app/service/contract/report.go

@@ -9,6 +9,7 @@ import (
 	"dashoo.cn/opms_libary/myerrors"
 	"fmt"
 	"github.com/gogf/gf/frame/g"
+	"github.com/gogf/gf/os/gtime"
 )
 
 type ContractReportService struct {
@@ -198,3 +199,46 @@ func (s *ContractReportService) QueryContractNum(date string) (interface{}, erro
 
 	return g.Map{"header": header, "data": data}, nil
 }
+
+// QueryContractExpireNum 售后运维到期统计表
+func (s *ContractReportService) QueryContractExpireNum(date string) (interface{}, error) {
+	if date != "" {
+		date += "-01 00:00:00"
+		date = gtime.NewFromStr(date).AddDate(1, 0, 0).Format("Y-m")
+	}
+	where := ""
+	// 权限限制(销售工程师看自己的)
+	if service.StringsContains(s.CxtUser.Roles, "SalesEngineer") {
+		where = fmt.Sprintf("incharge_id='%v'", s.CxtUser.Id)
+	}
+	// 获取数据
+	var contractInfos []*contract.CtrContract
+	err := s.Dao.Where(fmt.Sprintf("contract_end_time LIKE '%v%%' AND appro_status='30'", date)).Where(where).Order("incharge_id ASC, contract_end_time ASC").Scan(&contractInfos)
+	if err != nil {
+		return nil, err
+	}
+
+	header, data := make([]g.Map, 0), make([]g.Map, 0)
+	header = append(header, g.Map{"prop": "userName", "label": "销售工程师"}, g.Map{"prop": "code", "label": "合同编号"}, g.Map{"prop": "endTime", "label": "合同结束时间"}, g.Map{"prop": "cusName", "label": "客户名称"}, g.Map{"prop": "amount", "label": "合同总金额"})
+
+	total := 0
+	for _, info := range contractInfos {
+		data = append(data, g.Map{
+			"userName": info.InchargeName,
+			"code":     info.ContractCode,
+			"endTime":  info.ContractEndTime.Format("Y-m-d"),
+			"cusName":  info.CustName,
+			"amount":   fmt.Sprintf("总计:%.2f元", info.ContractAmount),
+		})
+		total += 1
+	}
+	data = append(data, g.Map{
+		"userName": "",
+		"code":     "",
+		"endTime":  "",
+		"cusName":  "",
+		"amount":   fmt.Sprintf("总计:%v", total),
+	})
+
+	return g.Map{"header": header, "data": data}, nil
+}

+ 1 - 1
opms_parent/app/service/plat/plat_task_cron.go

@@ -18,7 +18,7 @@ import (
 func init() {
 	// 定时任务
 	c := cron.New()
-	spec := "1 0/10 * * * ?" // 每天10分钟执行一次
+	spec := "1 */10 * * * ?" // 每天10分钟执行一次
 
 	if err := c.AddJob(spec, taskCron{}); err != nil {
 		glog.Error(err)

+ 4 - 3
opms_parent/app/service/work/deliver_order.go

@@ -308,7 +308,8 @@ func (s DeliverOrderService) Add(ctx context.Context, req *work.DeliverOrderAddR
 	var id []int
 	txerr := s.Dao.DB.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
 		var err error
-		id, err = DeliverOrderAdd(tx, req.ContractId, s.userInfo, &productInfo)
+		// 交付状态(0发起 10项目立项 15进行中 20 完成 30审批拒绝40关闭)
+		id, err = DeliverOrderAdd(tx, req.ContractId, s.userInfo, &productInfo, "0")
 		if err != nil {
 			return err
 		}
@@ -336,7 +337,7 @@ func (s DeliverOrderService) Add(ctx context.Context, req *work.DeliverOrderAddR
 	return id, txerr
 }
 
-func DeliverOrderAdd(tx *gdb.TX, contractId int, userInfo request.UserInfo, productInfo *work.ProductInfo) ([]int, error) {
+func DeliverOrderAdd(tx *gdb.TX, contractId int, userInfo request.UserInfo, productInfo *work.ProductInfo, state string) ([]int, error) {
 	var c contractmodel.CtrContract
 	err := tx.GetStruct(&c, "select * from ctr_contract where id = ?", contractId)
 	if err == sql.ErrNoRows {
@@ -392,7 +393,7 @@ func DeliverOrderAdd(tx *gdb.TX, contractId int, userInfo request.UserInfo, prod
 		// 交付状态(0发起 10项目立项 15进行中 20 完成 30审批拒绝40关闭)
 		o := work.DeliverOrder{
 			OrderCode:      fmt.Sprintf("%s%s", c.ContractCode, orderType),
-			OrderStatus:    "0",
+			OrderStatus:    state,
 			OrderType:      orderType,
 			CustId:         c.CustId,
 			CustName:       c.CustName,

+ 1 - 1
opms_parent/app/service/work/deliver_order_cron.go

@@ -15,7 +15,7 @@ import (
 func init() {
 	// 定时任务
 	c := cron.New()
-	spec := "1 0/1 * * * ?" // 每天10分钟执行一次
+	spec := "1 */10 * * * ?" // 每天10分钟执行一次
 
 	if err := c.AddJob(spec, deliverOrderProgressCron{}); err != nil {
 		glog.Error(err)

+ 3 - 2
opms_parent/app/service/work/work_order.go

@@ -125,8 +125,9 @@ func (s *OrderService) GetList(req *model.WorkOrderSearchReq) (total int, orderL
 	if req.SaleName != "" {
 		db = db.WhereLike("a."+s.Dao.C.SaleName, "%"+req.SaleName+"%")
 	}
-	if req.SupportTime != "" {
-		db = db.Where("a."+s.Dao.C.SupportTime, req.SupportTime)
+	if req.SupportTime != nil && len(*req.SupportTime) == 2 {
+		db = db.Where("a.support_time >= ?", (*req.SupportTime)[0]+" 00:00:00")
+		db = db.Where("a.support_time <= ?", (*req.SupportTime)[1]+" 23:59:59")
 	}
 	if req.ProductLine != "" {
 		db = db.Where("b.product_line", req.ProductLine)