浏览代码

test: 智力共享接口

hsun 6 月之前
父节点
当前提交
435b558c27
共有 8 个文件被更改,包括 1675 次插入0 次删除
  1. 150 0
      controllers/base_open.go
  2. 420 0
      controllers/report_open.go
  3. 112 0
      models/classify.go
  4. 121 0
      models/ppt_v2.go
  5. 115 0
      models/report_grant.go
  6. 40 0
      models/report_open.go
  7. 89 0
      models/sys_admin.go
  8. 628 0
      services/report_open.go

+ 150 - 0
controllers/base_open.go

@@ -0,0 +1,150 @@
+package controllers
+
+import (
+	"encoding/json"
+	"eta_gn/eta_report/models"
+	"eta_gn/eta_report/utils"
+	"fmt"
+	"github.com/beego/beego/v2/server/web"
+	"net/http"
+	"net/url"
+)
+
+type BaseOpenController struct {
+	web.Controller
+}
+
+func (this *BaseOpenController) Prepare() {
+	fmt.Println("enter prepare")
+	method := this.Ctx.Input.Method()
+	uri := this.Ctx.Input.URI()
+	fmt.Println("Url:", uri)
+	if method != "HEAD" {
+		//校验签名
+		nonce := this.Ctx.Input.Header("nonce")
+		timestamp := this.Ctx.Input.Header("timestamp")
+		appid := this.Ctx.Input.Header("appid")
+		signature := this.Ctx.Input.Header("signature")
+
+		if nonce == "" {
+			errMsg := "随机字符串不能为空"
+			this.JSON(models.BaseResponse{Ret: 400, Msg: "", ErrMsg: errMsg}, false, false)
+			this.StopRun()
+			return
+		}
+
+		if timestamp == "" {
+			errMsg := "时间戳不能为空"
+			this.JSON(models.BaseResponse{Ret: 400, Msg: "", ErrMsg: errMsg}, false, false)
+			this.StopRun()
+			return
+		}
+
+		if appid != utils.AppId {
+			errMsg := "AppId错误,请核查"
+			this.JSON(models.BaseResponse{Ret: 400, Msg: "", ErrMsg: errMsg}, false, false)
+			this.StopRun()
+			return
+		}
+
+		checkSign := utils.GetSign(nonce, timestamp)
+		if signature != checkSign {
+			utils.FileLog.Info(fmt.Sprintf("用户提交签名:%s;\n系统生成签名:%s\n", signature, checkSign))
+			errMsg := "签名错误"
+			this.JSON(models.BaseResponse{Ret: 401, Msg: errMsg, ErrMsg: errMsg}, false, false)
+			this.StopRun()
+			return
+		}
+		if method != "GET" && method != "POST" {
+			errMsg := "无效的请求方式"
+			this.JSON(models.BaseResponse{Ret: 501, Msg: errMsg, ErrMsg: errMsg}, false, false)
+			this.StopRun()
+			return
+		}
+	} else {
+		this.JSON(models.BaseResponse{Ret: 500, Msg: "系统异常,请联系客服!", ErrMsg: "method:" + method}, false, false)
+		this.StopRun()
+		return
+	}
+}
+
+func (c *BaseOpenController) ServeJSON(encoding ...bool) {
+	//所有请求都做这么个处理吧,目前这边都是做编辑、刷新逻辑处理(新增的话,并没有指标id,不会有影响)
+	var (
+		hasIndent   = false
+		hasEncoding = false
+	)
+	if web.BConfig.RunMode == web.PROD {
+		hasIndent = false
+	}
+	if len(encoding) > 0 && encoding[0] == true {
+		hasEncoding = true
+	}
+	if c.Data["json"] == nil {
+		//go utils.SendEmail("异常提醒:", "接口:"+"URI:"+c.Ctx.Input.URI()+";无返回值", utils.EmailSendToUsers)
+		//body := "接口:" + "URI:" + c.Ctx.Input.URI() + ";无返回值"
+		//go alarm_msg.SendAlarmMsg(body, 1)
+		return
+	}
+
+	baseRes := c.Data["json"].(*models.BaseResponse)
+	if baseRes != nil && baseRes.Ret != 408 {
+		//body, _ := json.Marshal(baseRes)
+		//var requestBody string
+		//method := c.Ctx.Input.Method()
+		//if method == "GET" {
+		//	requestBody = c.Ctx.Request.RequestURI
+		//} else {
+		//	requestBody, _ = url.QueryUnescape(string(c.Ctx.Input.RequestBody))
+		//}
+		//if baseRes.Ret != 200 && baseRes.IsSendEmail {
+		//	go utils.SendEmail(utils.APP_NAME_CN+"【"+utils.RunMode+"】"+"失败提醒", "URI:"+c.Ctx.Input.URI()+"<br/> "+"Params"+requestBody+" <br/>"+"ErrMsg:"+baseRes.ErrMsg+";<br/>Msg:"+baseRes.Msg+";<br/> Body:"+string(body)+"<br/>", utils.EmailSendToUsers)
+		//}
+
+		// 记录错误日志, 并清掉错误信息避免暴露给外部
+		if baseRes.ErrMsg != "" {
+			utils.FileLog.Info(baseRes.ErrMsg)
+			baseRes.ErrMsg = ""
+		}
+	}
+	c.JSON(c.Data["json"], hasIndent, hasEncoding)
+}
+
+func (c *BaseOpenController) JSON(data interface{}, hasIndent bool, coding bool) error {
+	c.Ctx.Output.Header("Content-Type", "application/json; charset=utf-8")
+	var content []byte
+	var err error
+	if hasIndent {
+		content, err = json.MarshalIndent(data, "", "  ")
+	} else {
+		content, err = json.Marshal(data)
+	}
+	if err != nil {
+		http.Error(c.Ctx.Output.Context.ResponseWriter, err.Error(), http.StatusInternalServerError)
+		return err
+	}
+	ip := c.Ctx.Input.IP()
+	requestBody, err := url.QueryUnescape(string(c.Ctx.Input.RequestBody))
+	if err != nil {
+		requestBody = string(c.Ctx.Input.RequestBody)
+	}
+	if requestBody == "" {
+		requestBody = c.Ctx.Input.URI()
+	}
+	c.logUri(content, requestBody, ip)
+	if coding {
+		content = []byte(utils.StringsToJSON(string(content)))
+	}
+	return c.Ctx.Output.Body(content)
+}
+
+func (c *BaseOpenController) logUri(respContent []byte, requestBody, ip string) {
+	var reqData interface{}
+	err := json.Unmarshal([]byte(requestBody), &reqData)
+	if err != nil {
+		utils.ApiLog.Info("uri:%s, requestBody:%s, ip:%s", c.Ctx.Input.URI(), requestBody, ip)
+	} else {
+		utils.ApiLog.Info("uri:%s, requestBody:%s, ip:%s", c.Ctx.Input.URI(), requestBody, ip)
+	}
+	return
+}

+ 420 - 0
controllers/report_open.go

@@ -0,0 +1,420 @@
+package controllers
+
+import (
+	"encoding/json"
+	"eta_gn/eta_report/models"
+	"eta_gn/eta_report/services"
+	"eta_gn/eta_report/utils"
+	"fmt"
+	"strings"
+	"time"
+)
+
+// ReportOpenController 报告开放接口
+type ReportOpenController struct {
+	BaseOpenController
+}
+
+// ClassifyTree
+// @Title 报告分类
+// @Description 报告分类接口
+// @Param   ClassifyType   query   int   false   "分类类型:0-全部(不传默认为0);1-研报;2-PPT"
+// @Success 200 {object} models.ClassifyTreeItem
+// @router /report/classify [get]
+func (this *ReportOpenController) ClassifyTree() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	classifyType, _ := this.GetInt("ClassifyType", 0)
+	if classifyType < 0 || classifyType > 2 {
+		classifyType = 0
+	}
+
+	// 获取分类列表, 格式化分类树
+	classifyOb := new(models.Classify)
+	cond := ``
+	pars := make([]interface{}, 0)
+	if classifyType > 0 {
+		cond += ` AND classify_type = ?`
+		pars = append(pars, classifyType)
+	}
+	list, e := classifyOb.GetItemsByCondition(cond, pars, []string{}, "parent_id ASC, sort ASC, create_time ASC")
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取分类列表失败,%v", e)
+		return
+	}
+	resp := services.GetReportClassifyTreeRecursive(list, 0)
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// ReportCreate
+// @Title 生成报告
+// @Description 生成报告接口
+// @Param	request	body models.ReportCreateReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /report/create [post]
+func (this *ReportOpenController) ReportCreate() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req models.ReportCreateReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析失败,%v", e)
+		return
+	}
+	if req.ReportId <= 0 {
+		br.Msg = "报告ID有误"
+		br.ErrMsg = fmt.Sprintf("智力共享报告ID有误, %d", req.ReportId)
+		return
+	}
+	if req.ClassifyId <= 0 {
+		br.Msg = "请选择分类"
+		return
+	}
+	req.Title = strings.TrimSpace(req.Title)
+	if req.Title == "" {
+		br.Msg = "请输入报告标题"
+		return
+	}
+	req.EndTime = strings.TrimSpace(req.EndTime)
+	if req.EndTime == "" {
+		br.Msg = "请输入课题结束时间"
+		return
+	}
+	topicTime, e := time.ParseInLocation(utils.FormatDateTime, req.EndTime, time.Local)
+	if e != nil {
+		br.Msg = "课题结束时间格式有误"
+		return
+	}
+	req.Creator = strings.TrimSpace(req.Creator)
+	if req.Creator == "" {
+		br.Msg = "请输入创建人工号"
+		return
+	}
+
+	// 查询分类,根据分类创建对应类型的研报
+	classifyOb := new(models.Classify)
+	classify, e := classifyOb.GetItemById(req.ClassifyId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Msg = "分类不存在"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取分类失败, %v", e)
+		return
+	}
+
+	// 校验报告ID是否已存在, 已存在返回已有的报告ID
+	reportExist, pptExist := services.GetReportByOutReportId(req.ReportId)
+	if reportExist != nil && reportExist.Id > 0 {
+		resp := new(models.ReportCreateResp)
+		resp.ReportId = reportExist.Id
+		br.Data = resp
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "操作成功"
+		return
+	}
+	if pptExist != nil && pptExist.PptId > 0 {
+		resp := new(models.ReportCreateResp)
+		resp.ReportId = pptExist.PptId
+		br.Data = resp
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "操作成功"
+		return
+	}
+
+	// 研报类型
+	var reportId int
+	if classify.ClassifyType == utils.ReportTypeDefault {
+		reportId, e = services.CreateReport(req.ReportId, req.ClassifyId, req.Title, topicTime, req.Creator, req.Authors)
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("新增报告失败, %v", e)
+			return
+		}
+	}
+
+	// PPT类型
+	if classify.ClassifyType == utils.ReportTypePPT {
+		reportId, e = services.CreatePptReport(req.ReportId, req.ClassifyId, req.Title, topicTime, req.Creator, req.Authors)
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("新增PPT报告失败, %v", e)
+			return
+		}
+	}
+
+	resp := new(models.ReportCreateResp)
+	resp.ReportId = reportId
+	//resp.ReportCode = reportCode
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// ReportModify
+// @Title 编辑报告
+// @Description 编辑报告接口
+// @Param	request	body models.ReportModifyReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /report/modify [post]
+func (this *ReportOpenController) ReportModify() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req models.ReportModifyReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析失败,%v", e)
+		return
+	}
+	if req.ReportId <= 0 {
+		br.Msg = "报告ID有误"
+		br.ErrMsg = fmt.Sprintf("智力共享报告ID有误, %d", req.ReportId)
+		return
+	}
+	req.Title = strings.TrimSpace(req.Title)
+	if req.Title == "" {
+		br.Msg = "请输入报告标题"
+		return
+	}
+	req.EndTime = strings.TrimSpace(req.EndTime)
+	if req.EndTime == "" {
+		br.Msg = "请输入课题结束时间"
+		return
+	}
+	topicTime, e := time.ParseInLocation(utils.FormatDateTime, req.EndTime, time.Local)
+	if e != nil {
+		br.Msg = "课题结束时间格式有误"
+		return
+	}
+	reportExist, pptExist := services.GetReportByOutReportId(req.ReportId)
+	if reportExist == nil && pptExist == nil {
+		br.Msg = "报告不存在"
+		br.ErrMsg = fmt.Sprintf("报告不存在, OutReportId: %d", req.ReportId)
+		return
+	}
+
+	// 研报类型
+	if reportExist != nil && reportExist.Id > 0 {
+		if reportExist.State == models.ReportStatePass {
+			br.Msg = "报告已发布,不可编辑"
+			return
+		}
+		if e = services.EditReport(reportExist, req.Title, topicTime, req.Authors); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("编辑报告失败, %v", e)
+			return
+		}
+	}
+
+	// PPT类型
+	if pptExist != nil && pptExist.PptId > 0 {
+		if pptExist.State == models.ReportStatePass {
+			br.Msg = "报告已发布,不可编辑"
+			return
+		}
+		if e = services.EditPptReport(pptExist, req.Title, topicTime, req.Authors); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("编辑PPT报告失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// ReportApprove
+// @Title 审批报告
+// @Description 审批报告接口
+// @Param	request	body models.ReportApproveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /report/approve [post]
+func (this *ReportOpenController) ReportApprove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req models.ReportApproveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析失败,%v", e)
+		return
+	}
+	if len(req.ReportIds) == 0 {
+		br.Msg = "报告ID有误"
+		br.ErrMsg = fmt.Sprintf("智力共享报告ID有误, %v", req.ReportIds)
+		return
+	}
+	if req.ApproveType != 1 && req.ApproveType != 2 && req.ApproveType != 3 {
+		br.Msg = "审批类型异常"
+		br.ErrMsg = fmt.Sprintf("审批类型异常, %d", req.ApproveType)
+		return
+	}
+
+	// 先做校验后执行
+	updateReports := make([]*models.Report, 0)
+	updatePpts := make([]*models.PptV2, 0)
+	for _, v := range req.ReportIds {
+		reportExist, pptExist := services.GetReportByOutReportId(v)
+		if reportExist == nil && pptExist == nil {
+			br.Msg = fmt.Sprintf("报告ID: %d不存在", v)
+			br.ErrMsg = fmt.Sprintf("报告ID: %d不存在", v)
+			return
+		}
+
+		// 通过和驳回需要审批中状态,撤回暂不做限制
+		if reportExist != nil && reportExist.Id > 0 {
+			if (req.ApproveType == 1 || req.ApproveType == 2) && reportExist.State != models.ReportStateWaitApprove {
+				br.Msg = "报告当前状态不允许审批"
+				br.ErrMsg = fmt.Sprintf("报告当前状态不允许审批, %d", reportExist.State)
+				return
+			}
+			updateReports = append(updateReports, reportExist)
+		}
+		if pptExist != nil && pptExist.PptId > 0 {
+			if (req.ApproveType == 1 || req.ApproveType == 2) && pptExist.State != models.ReportStateWaitApprove {
+				br.Msg = "报告当前状态不允许审批"
+				br.ErrMsg = fmt.Sprintf("报告当前状态不允许审批, %d", reportExist.State)
+				return
+			}
+			updatePpts = append(updatePpts, pptExist)
+		}
+	}
+
+	afterApproveMap := map[int]int{
+		1: models.ReportStatePass,
+		2: models.ReportStateRefused,
+		3: models.ReportStateWaitSubmit,
+	}
+	updateCols := []string{"State", "ApproveTime", "ModifyTime"}
+	if len(updateReports) > 0 {
+		for _, v := range updateReports {
+			v.State = afterApproveMap[req.ApproveType]
+			if v.State == models.ReportStateRefused || v.State == models.ReportStatePass {
+				v.ApproveTime = time.Now()
+			} else {
+				v.ApproveTime = time.Time{}
+			}
+			v.ModifyTime = time.Now()
+			if e := v.Update(updateCols); e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("更新报告状态失败, %v", e)
+				return
+			}
+		}
+	}
+	if len(updatePpts) > 0 {
+		for _, v := range updatePpts {
+			v.State = afterApproveMap[req.ApproveType]
+			if v.State == models.ReportStateRefused || v.State == models.ReportStatePass {
+				v.ApproveTime = time.Now()
+			} else {
+				v.ApproveTime = time.Time{}
+			}
+			v.ModifyTime = time.Now()
+			if e := v.Update(updateCols); e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("更新报告状态失败, %v", e)
+				return
+			}
+		}
+	}
+
+	// TODO:消息通知
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// ReportRemove
+// @Title 删除报告
+// @Description 删除报告接口
+// @Param	request	body models.ReportRemoveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /report/remove [post]
+func (this *ReportOpenController) ReportRemove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	var req models.ReportRemoveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数解析异常"
+		br.ErrMsg = fmt.Sprintf("参数解析失败,%v", e)
+		return
+	}
+	if len(req.ReportIds) == 0 {
+		br.Msg = "报告ID有误"
+		br.ErrMsg = fmt.Sprintf("智力共享报告ID有误, %v", req.ReportIds)
+		return
+	}
+
+	// TODO:若报告的状态是已审批状态,那么把OutReportId置空,不做物理删除
+	for _, v := range req.ReportIds {
+		reportExist, pptExist := services.GetReportByOutReportId(v)
+		// 不存在忽略即可
+		if reportExist == nil && pptExist == nil {
+			continue
+		}
+
+		// 研报类型
+		if reportExist != nil && reportExist.Id > 0 {
+			if e := reportExist.Remove(); e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("删除报告失败, %v", e)
+				return
+			}
+		}
+
+		// PPT类型
+		if pptExist != nil && pptExist.PptId > 0 {
+			if e := pptExist.Remove(); e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("删除PPT报告失败, %v", e)
+				return
+			}
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}

+ 112 - 0
models/classify.go

@@ -0,0 +1,112 @@
+package models
+
+import (
+	"eta_gn/eta_report/global"
+	"fmt"
+	"strings"
+	"time"
+)
+
+type Classify struct {
+	Id                   int       `gorm:"column:id;primaryKey" json:"id"`                                //`orm:"column(id);pk" gorm:"primaryKey" `
+	ClassifyName         string    `gorm:"column:classify_name" json:"classify_name"`                     //`description:"分类名称"`
+	Sort                 int       `gorm:"column:sort" json:"sort"`                                       //`json:"-"`
+	ParentId             int       `gorm:"column:parent_id" json:"parent_id"`                             //`description:"父级分类id"`
+	CreateTime           time.Time `gorm:"column:create_time" json:"create_time"`                         //`description:"创建时间"`
+	ModifyTime           time.Time `gorm:"column:modify_time" json:"modify_time"`                         //`description:"修改时间"`
+	Abstract             string    `gorm:"column:abstract" json:"abstract"`                               //`description:"栏目简介"`
+	Descript             string    `gorm:"column:descript" json:"descript"`                               //`description:"分享描述"`
+	ReportAuthor         string    `gorm:"column:report_author" json:"report_author"`                     //`description:"栏目作者"`
+	AuthorDescript       string    `gorm:"column:author_descript" json:"author_descript"`                 //`description:"作者简介"`
+	ColumnImgUrl         string    `gorm:"column:column_img_url" json:"column_img_url"`                   //`description:"栏目配图"`
+	HeadImgUrl           string    `gorm:"column:head_img_url" json:"head_img_url"`                       //`description:"头部banner"`
+	AvatarImgUrl         string    `gorm:"column:avatar_img_url" json:"avatar_img_url"`                   //`description:"头像"`
+	ReportImgUrl         string    `gorm:"column:report_img_url" json:"report_img_url"`                   //`description:"报告配图"`
+	HomeImgUrl           string    `gorm:"column:home_img_url" json:"home_img_url"`                       //`description:"首页配图"`
+	ClassifyLabel        string    `gorm:"column:classify_label" json:"classify_label"`                   //`description:"分类标签"`
+	ShowType             int       `gorm:"column:show_type" json:"show_type"`                             //`description:"展示类型:1-列表 2-专栏"`
+	HasTeleconference    int       `gorm:"column:has_teleconference" json:"has_teleconference"`           //`description:"是否有电话会:0-否 1-是"`
+	VipTitle             string    `gorm:"column:vip_title" json:"vip_title"`                             //`description:"研究员头衔"`
+	IsShow               int       `gorm:"column:is_show" json:"is_show"`                                 //`description:"是否在小程序显示:1-显示 0-隐藏"`
+	YbFiccSort           int       `gorm:"column:yb_ficc_sort" json:"yb_ficc_sort"`                       //`description:"小程序FICC页排序"`
+	YbFiccIcon           string    `gorm:"column:yb_ficc_icon" json:"yb_ficc_icon"`                       // `description:"小程序FICC页icon"`
+	YbFiccPcIcon         string    `gorm:"column:yb_ficc_pc_icon" json:"yb_ficc_pc_icon"`                 //`description:"小程序PC端FICC页背景图"`
+	YbIconUrl            string    `gorm:"column:yb_icon_url" json:"yb_icon_url"`                         //`description:"小程序已购页icon"`
+	YbBgUrl              string    `gorm:"column:yb_bg_url" json:"yb_bg_url"`                             //`description:"小程序已购详情背景图"`
+	YbListImg            string    `gorm:"column:yb_list_img" json:"yb_list_img"`                         //`description:"小程序研报列表封面图"`
+	YbShareBgImg         string    `gorm:"column:yb_share_bg_img" json:"yb_share_bg_img"`                 //`description:"小程序研报详情分享背景图"`
+	YbRightBanner        string    `gorm:"column:yb_right_banner" json:"yb_right_banner"`                 //`description:"Pc端详情页,右侧,报告合集背景图"`
+	RelateTel            int       `gorm:"column:relate_tel" json:"relate_tel"`                           //`description:"是否在电话会中可选: 0-否; 1-是"`
+	RelateVideo          int       `gorm:"column:relate_video" json:"relate_video"`                       //`description:"是否在路演视频中可选: 0-否; 1-是"`
+	IsMassSend           int       `gorm:"column:is_mass_send" json:"is_mass_send"`                       //`description:"1:群发,0:非群发"`
+	Enabled              int       `gorm:"column:enabled" json:"enabled"`                                 //`description:"是否可用,1可用,0禁用"`
+	Level                int       `gorm:"column:level" json:"level"`                                     //`description:"层级"`
+	HasChild             int       `gorm:"column:has_child" json:"has_child"`                             //`description:"是否有子级别,0:下面没有子分类,1:下面有子分类;默认:0"`
+	ReportDetailShowType int       `gorm:"column:report_detail_show_type" json:"report_detail_show_type"` //`description:"报告详情的展示类型:1-拼接;2:目录"`
+	ClassifyType         int       `gorm:"column:classify_type" json:"classify_type"`                     //`description:"报告详情的展示类型:1-拼接;2:目录"`
+	IsRemind             int       `gorm:"column:is_remind" json:"is_remind"`                             //`description:"报告详情的展示类型:1-拼接;2:目录"`
+	RemindTime           string    `gorm:"column:remind_time" json:"remind_time"`                         //`description:"报告详情的展示类型:1-拼接;2:目录"`
+	ReportNum            int       `gorm:"column:report_num" json:"report_num"`                           //`description:"分类下的报告数"`
+	LevelPath            string    `gorm:"column:level_path" json:"level_path"`                           //`description:"分类的层级路径,英文逗号分隔"`
+}
+
+func (m *Classify) TableName() string {
+	return "classify"
+}
+
+func (m *Classify) GetItemById(id int) (item *Classify, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), "id")
+	err = global.DEFAULT_DmSQL.Raw(sql, id).First(&item).Error
+	return
+}
+
+func (m *Classify) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*Classify, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := `ORDER BY create_time DESC`
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+type ClassifyTreeItem struct {
+	ClassifyId   int                 `description:"分类ID"`
+	ClassifyName string              `description:"分类名称"`
+	ParentId     int                 `description:"父级ID"`
+	Sort         int                 `description:"排序"`
+	Level        int                 `description:"层级"`
+	ClassifyType int                 `description:"分类类型:1-研报;2-PPT"`
+	Children     []*ClassifyTreeItem `description:"子目录"`
+}
+
+func (m *Classify) Format2TreeItem(origin *Classify) (item *ClassifyTreeItem) {
+	if origin == nil {
+		return
+	}
+	item = new(ClassifyTreeItem)
+	item.ClassifyId = origin.Id
+	item.ClassifyName = origin.ClassifyName
+	item.ParentId = origin.ParentId
+	item.Sort = origin.Sort
+	item.Level = origin.Level
+	item.ClassifyType = origin.ClassifyType
+	item.Children = make([]*ClassifyTreeItem, 0)
+	return
+}
+
+func GetClassifyById(classifyId int) (item *Classify, err error) {
+	sql := `SELECT * FROM classify WHERE id=? `
+	err = global.DEFAULT_DmSQL.Raw(sql, classifyId).First(&item).Error
+	return
+}
+
+// UpdateClassify 更新分类
+func (classifyInfo *Classify) UpdateClassify(cols []string) (err error) {
+	err = global.DEFAULT_DmSQL.Select(cols).Updates(classifyInfo).Error
+	return
+}

+ 121 - 0
models/ppt_v2.go

@@ -0,0 +1,121 @@
+package models
+
+import (
+	"eta_gn/eta_report/global"
+	"fmt"
+	"strings"
+	"time"
+)
+
+// PptV2 表
+type PptV2 struct {
+	PptId            int       `gorm:"column:ppt_id;primaryKey;autoIncrement:true" description:"ppt的Id"`
+	TemplateType     int       `gorm:"column:template_type" description:"模版类型"`
+	BackgroundImg    string    `gorm:"column:background_img" description:"背景图片"`
+	Title            string    `gorm:"column:title" description:"标题"`
+	ReportType       string    `gorm:"column:report_type" description:"报告类型"`
+	PptDate          string    `gorm:"column:ppt_date" description:"选择日期"`
+	Content          string    `gorm:"column:content" description:"ppt内容"`
+	PptUrl           string    `gorm:"column:ppt_url" description:"ppt下载地址"`
+	PptxUrl          string    `gorm:"column:pptx_url" description:"pptx下载地址"`
+	CreateTime       time.Time `gorm:"column:create_time" description:"创建时间"`
+	ModifyTime       time.Time `gorm:"column:modify_time" description:"修改时间"`
+	AdminId          int       `gorm:"column:admin_id" description:"系统用户id"`
+	AdminRealName    string    `gorm:"column:admin_real_name" description:"系统用户名称"`
+	PptVersion       int8      `gorm:"column:ppt_version" description:"是否ppt的旧版本;1:旧的,2:新的"`
+	ReportId         int       `gorm:"column:report_id" description:"关联的报告ID"`
+	ReportCode       string    `gorm:"column:report_code" description:"关联的报告code"`
+	IsShare          int8      `gorm:"column:is_share" description:"是否分享,0:不分享,1:分享"`
+	PublishTime      time.Time `gorm:"column:publish_time" description:"发布时间"`
+	CoverContent     string    `gorm:"column:cover_content" description:"PPT内容-JSON"`
+	PptPage          int       `gorm:"column:ppt_page" description:"PPT页数"`
+	TitleSetting     string    `gorm:"column:title_setting" description:"PPT标题设置"`
+	ClassifyId       int       `gorm:"column:classify_id" description:"报告分类ID"`
+	AddType          int       `gorm:"column:add_type" description:"新增方式:1-新增报告;2-继承报告"`
+	InheritReportId  int       `gorm:"column:inherit_report_id" description:"继承的报告ID"`
+	CollaborateType  int       `gorm:"column:collaborate_type" description:"协作方式:1-个人;2-多人协作"`
+	CollaborateUsers string    `gorm:"column:collaborate_users" description:"协作人IDs, 英文逗号分隔"`
+	IsPublicPublish  int       `gorm:"column:is_public_publish" description:"是否公开发布:1-是;2-否"`
+	SubmitTime       time.Time `gorm:"column:submit_time" description:"提交时间"`
+	ApproveTime      time.Time `gorm:"column:approve_time" description:"审批时间"`
+	ReportSource     int       `gorm:"column:report_source" description:"报告来源:1-系统内;2-智力共享"`
+	OutReportId      string    `gorm:"column:out_report_id" description:"外部报告ID(或编码)"`
+	State            int       `gorm:"column:state" description:"报告状态:1-未发布;2-已发布;3-待提交;4-待审批;5-已驳回;6-已通过"`
+	TopicEndTime     time.Time `gorm:"column:topic_end_time" description:"课题结束时间"`
+}
+
+func (m *PptV2) TableName() string {
+	return "ppt_v2"
+}
+
+func (m *PptV2) Create() (err error) {
+	err = global.DEFAULT_DmSQL.Create(m).Error
+	return
+}
+
+func (m *PptV2) Update(cols []string) (err error) {
+	err = global.DEFAULT_DmSQL.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *PptV2) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), "ppt_id")
+	err = global.DEFAULT_DmSQL.Exec(sql, m.PptId).Error
+	return
+}
+
+func (m *PptV2) GetItemById(id int) (item *PptV2, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), "ppt_id")
+	err = global.DEFAULT_DmSQL.Raw(sql, id).First(&item).Error
+	return
+}
+
+func (m *PptV2) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *PptV2, err error) {
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s %s LIMIT 1`, m.TableName(), condition, order)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *PptV2) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*PptV2, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := `ORDER BY create_time DESC`
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+func (m *PptV2) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*PptV2, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := `ORDER BY create_time DESC`
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s LIMIT ?,?`, fields, m.TableName(), condition, order)
+	pars = append(pars, startSize, pageSize)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+func (m *PptV2) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+// PptReportQueryFields 除富文本的常用查询字段
+var PptReportQueryFields = []string{
+	"ppt_id", "title", "classify_id", "ppt_version", "pptx_url", "ppt_page", "title_setting", "state", "report_source", "publish_time", "submit_time", "approve_time", "create_time", "modify_time", "admin_id", "collaborate_type", "collaborate_users",
+}

+ 115 - 0
models/report_grant.go

@@ -0,0 +1,115 @@
+package models
+
+import (
+	"eta_gn/eta_report/global"
+	"eta_gn/eta_report/utils"
+	"fmt"
+	"strings"
+	"time"
+)
+
+// ReportGrant 报告授权表
+type ReportGrant struct {
+	GrantId    int       `gorm:"primaryKey;column:grant_id;type:int(9) unsigned;not null"`    // 授权id
+	ReportId   int       `gorm:"column:report_id;type:int(9) unsigned;not null;default:0"`    // 报告id
+	AdminId    int       `gorm:"column:admin_id;type:int(9) unsigned;default:0"`              // 授权的用户id
+	CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:CURRENT_TIMESTAMP"` // 授权时间
+}
+
+func (m *ReportGrant) TableName() string {
+	return "report_grant"
+}
+
+type ReportGrantCols struct {
+	PrimaryId  string
+	ReportId   string
+	AdminId    string
+	CreateTime string
+}
+
+func (m *ReportGrant) Cols() ReportGrantCols {
+	return ReportGrantCols{
+		PrimaryId:  "grant_id",
+		ReportId:   "report_id",
+		AdminId:    "admin_id",
+		CreateTime: "create_time",
+	}
+}
+
+func (m *ReportGrant) Create() (err error) {
+	err = global.DEFAULT_DmSQL.Create(m).Error
+	return
+}
+
+func (m *ReportGrant) CreateMulti(items []*ReportGrant) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DmSQL.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *ReportGrant) Update(cols []string) (err error) {
+	err = global.DEFAULT_DmSQL.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *ReportGrant) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DmSQL.Exec(sql, m.GrantId).Error
+	return
+}
+
+func (m *ReportGrant) MultiRemove(ids []int) (err error) {
+	if len(ids) == 0 {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s IN (%s)`, m.TableName(), m.Cols().PrimaryId, utils.GetOrmInReplace(len(ids)))
+	err = global.DEFAULT_DmSQL.Exec(sql, ids).Error
+	return
+}
+
+func (m *ReportGrant) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DmSQL.Exec(sql, pars...).Error
+	return
+}
+
+func (m *ReportGrant) GetItemById(id int) (item *ReportGrant, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DmSQL.Raw(sql, id).First(&item).Error
+	return
+}
+
+func (m *ReportGrant) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *ReportGrant, err error) {
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE 1=1 %s %s LIMIT 1`, m.TableName(), condition, order)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *ReportGrant) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM %s WHERE 1=1 %s`, m.TableName(), condition)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+func (m *ReportGrant) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*ReportGrant, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := fmt.Sprintf(`ORDER BY %s DESC`, m.Cols().CreateTime)
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM %s WHERE 1=1 %s %s`, fields, m.TableName(), condition, order)
+	err = global.DEFAULT_DmSQL.Raw(sql, pars...).Find(&items).Error
+	return
+}

+ 40 - 0
models/report_open.go

@@ -0,0 +1,40 @@
+package models
+
+// ReportCreateReq 创建报告
+type ReportCreateReq struct {
+	ReportId   int      `description:"智力共享报告ID"`
+	ClassifyId int      `description:"分类ID"`
+	Title      string   `description:"报告标题"`
+	EndTime    string   `description:"课题结束时间"`
+	Creator    string   `description:"创建人工号"`
+	Authors    []string `description:"作者工号"`
+}
+
+// ReportCreateResp 创建报告响应
+type ReportCreateResp struct {
+	ReportId int `description:"智能研报ID"`
+	//ReportCode string `description:"报告唯一编码"`
+}
+
+// ReportModifyReq 编辑报告
+type ReportModifyReq struct {
+	ReportId int      `description:"智力共享报告ID"`
+	Title    string   `description:"报告标题"`
+	EndTime  string   `description:"课题结束时间"`
+	Authors  []string `description:"作者工号"`
+	Operator string   `description:"操作人工号"`
+}
+
+// ReportApproveReq 审批报告
+type ReportApproveReq struct {
+	ReportIds   []int  `description:"智力共享报告ID"`
+	ApproveType int    `description:"审批类型:1-通过;2-驳回;3-撤回"`
+	ApproveUser string `description:"审批人"`
+	Suggestion  string `description:"审批意见"`
+}
+
+// ReportRemoveReq 删除报告
+type ReportRemoveReq struct {
+	ReportIds []int  `description:"智力共享报告ID"`
+	Operator  string `description:"操作人工号"`
+}

+ 89 - 0
models/sys_admin.go

@@ -0,0 +1,89 @@
+package models
+
+import (
+	"eta_gn/eta_report/global"
+	"fmt"
+	"strings"
+	"time"
+)
+
+type Admin struct {
+	AdminId                   int    `gorm:"primaryKey;autoIncrement;column:admin_id"`
+	AdminName                 string `description:"系统用户名称"`
+	AdminAvatar               string `description:"用户头像"`
+	RealName                  string `description:"系统用户姓名"`
+	Password                  string `json:"-"`
+	LastUpdatedPasswordTime   string `json:"-"`
+	Enabled                   int
+	Email                     string `description:"系统用户邮箱"`
+	LastLoginTime             string
+	CreatedTime               time.Time
+	LastUpdatedTime           string
+	Role                      string    `description:"系统用户角色"`
+	Mobile                    string    `description:"手机号"`
+	RoleType                  int       `description:"角色类型:1需要录入指标,0:不需要"`
+	RoleId                    int       `description:"角色ID"`
+	RoleName                  string    `description:"角色名称"`
+	RoleTypeCode              string    `description:"角色类型编码"`
+	DepartmentId              int       `description:"部门id"`
+	DepartmentName            string    `description:"部门名称"`
+	GroupId                   int       `description:"分组id"`
+	GroupName                 string    `description:"分组名称"`
+	Authority                 int       `description:"管理权限,0:无,1:部门负责人,2:小组负责人,或者ficc销售主管,3:超级管理员,4:ficc销售组长"`
+	Position                  string    `description:"职位"`
+	DisableTime               time.Time `description:"禁用时间"`
+	ChartPermission           int8      `description:"图表权限id"`
+	OpenId                    string    `description:"国能部门公众号的openid"`
+	UnionId                   string    `description:"微信公众平台唯一标识"`
+	EdbPermission             int8      `description:"指标库操作权限,0:只能操作 自己的,1:所有指标可操作"`
+	MysteelChemicalPermission int8      `description:"钢联化工指标操作权限,0:只能操作 自己的,1:所有指标可操作"`
+	PredictEdbPermission      int8      `description:"预测指标库操作权限,0:只能操作 自己的,1:所有预测指标可操作"`
+	Province                  string    `description:"省"`
+	ProvinceCode              string    `description:"省编码"`
+	City                      string    `description:"市"`
+	CityCode                  string    `description:"市编码"`
+	EmployeeId                string    `description:"员工工号(钉钉/每刻报销)"`
+	TelAreaCode               string    `description:"手机区号"`
+	IsLdap                    int       `description:"是否为域用户:0-系统账户;1-域用户"`
+	OutId                     string    `description:"外部ID"`
+}
+
+func (m *Admin) TableName() string {
+	return "admin"
+}
+
+func (m *Admin) GetItemById(id int) (item *Admin, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM "admin" WHERE %s = ? LIMIT 1`, "admin_id")
+	err = global.DmSQL["eta"].Raw(sql, id).First(&item).Error
+	return
+}
+
+func (m *Admin) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *Admin, err error) {
+	order := ``
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT * FROM "admin" WHERE 1=1 %s %s LIMIT 1`, condition, order)
+	err = global.DmSQL["eta"].Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *Admin) GetCountByCondition(condition string, pars []interface{}) (count int, err error) {
+	sql := fmt.Sprintf(`SELECT COUNT(1) FROM "admin" WHERE 1=1 %s`, condition)
+	err = global.DmSQL["eta"].Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+func (m *Admin) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*Admin, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := `ORDER BY created_time DESC`
+	if orderRule != "" {
+		order = ` ORDER BY ` + orderRule
+	}
+	sql := fmt.Sprintf(`SELECT %s FROM "admin" WHERE 1=1 %s %s`, fields, condition, order)
+	err = global.DmSQL["eta"].Raw(sql, pars...).Find(&items).Error
+	return
+}

+ 628 - 0
services/report_open.go

@@ -0,0 +1,628 @@
+package services
+
+import (
+	"eta_gn/eta_report/models"
+	"eta_gn/eta_report/utils"
+	"fmt"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+)
+
+// GetReportClassifyTreeRecursive 递归获取目录树
+func GetReportClassifyTreeRecursive(list []*models.Classify, parentId int) []*models.ClassifyTreeItem {
+	res := make([]*models.ClassifyTreeItem, 0)
+	for _, v := range list {
+		if v.ParentId == parentId {
+			t := new(models.ClassifyTreeItem)
+			t.ClassifyId = v.Id
+			t.ClassifyName = v.ClassifyName
+			t.ParentId = v.ParentId
+			t.Sort = v.Sort
+			t.Level = v.Level
+			t.ClassifyType = v.ClassifyType
+			t.Children = GetReportClassifyTreeRecursive(list, v.Id)
+			res = append(res, t)
+		}
+	}
+	return res
+}
+
+// GetReportByOutReportId 根据外部ID获取报告
+func GetReportByOutReportId(outReportId int) (reportItem *models.Report, pptItem *models.PptV2) {
+	reportOb := new(models.Report)
+	cond := ` AND out_report_id = ?`
+	pars := make([]interface{}, 0)
+	pars = append(pars, outReportId)
+	reportExist, _ := reportOb.GetItemByCondition(cond, pars, "")
+	if reportExist != nil && reportExist.Id > 0 {
+		reportItem = reportExist
+	}
+
+	pptOb := new(models.PptV2)
+	pptExist, _ := pptOb.GetItemByCondition(cond, pars, "")
+	if pptExist != nil && pptExist.PptId > 0 {
+		pptItem = pptExist
+	}
+	return
+}
+
+// CreatePptReport 创建PPT报告
+func CreatePptReport(outReportId, classifyId int, title string, topicEndTime time.Time, creator string, authors []string) (reportId int, err error) {
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info(fmt.Sprintf("创建外部PPT报告失败, OutReportId: %d, %v", outReportId, err))
+		}
+	}()
+
+	// 获取用户
+	outIdAdmin := make(map[string]*models.Admin)
+	{
+		ob := new(models.Admin)
+		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取用户列表失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			if v.OutId == "" {
+				continue
+			}
+			outIdAdmin[v.OutId] = v
+		}
+	}
+	creatorAdmin := outIdAdmin[creator]
+	if creatorAdmin == nil {
+		err = fmt.Errorf("创建人不存在, OutId: %s", creator)
+		return
+	}
+
+	// 新建PPT基础信息
+	newItem := new(models.PptV2)
+	newItem.Title = title
+	newItem.AddType = utils.ReportAddTypeInherit
+	newItem.ClassifyId = classifyId
+	newItem.CollaborateType = utils.ReportWriteTypeGroup
+	if len(authors) > 0 {
+		var partnerArr []string
+		for _, v := range authors {
+			ad := outIdAdmin[v]
+			// 忽略无法识别的工号
+			if ad == nil {
+				continue
+			}
+			partnerArr = append(partnerArr, strconv.Itoa(ad.AdminId))
+		}
+		newItem.CollaborateUsers = strings.Trim(strings.Join(partnerArr, ","), `"`)
+	}
+	newItem.PptVersion = 2
+	newItem.AdminId = creatorAdmin.AdminId
+	newItem.AdminRealName = creatorAdmin.RealName
+	newItem.ReportSource = utils.ReportSourceOuter // 固定外部PPT
+	newItem.State = models.ReportStateWaitSubmit   // 默认待提交
+	newItem.CreateTime = time.Now()
+	newItem.ModifyTime = time.Now()
+	newItem.OutReportId = strconv.Itoa(outReportId)
+	newItem.TopicEndTime = topicEndTime
+	newItem.TemplateType = 1 // 模板类型默认1,有继承那么就继承
+
+	// 自动继承该分类上一篇外部报告(审批通过的)
+	inheritItem := new(models.PptV2)
+	{
+		ob := new(models.PptV2)
+		cond := ` AND report_source = ? AND classify_id = ? AND state = ?`
+		pars := make([]interface{}, 0)
+		pars = append(pars, utils.ReportSourceOuter, classifyId, models.ReportStatePass)
+		inheritItem, _ = ob.GetItemByCondition(cond, pars, "")
+	}
+	if inheritItem != nil && inheritItem.PptId > 0 {
+		newItem.TemplateType = inheritItem.TemplateType
+		newItem.BackgroundImg = inheritItem.BackgroundImg
+		newItem.ReportType = inheritItem.ReportType
+		newItem.PptDate = inheritItem.PptDate
+		newItem.Content = inheritItem.Content
+		newItem.CoverContent = inheritItem.CoverContent
+		newItem.TitleSetting = inheritItem.TitleSetting
+	}
+	if e := newItem.Create(); e != nil {
+		err = fmt.Errorf("新增PPT报告失败, %v", e)
+		return
+	}
+	reportId = newItem.PptId
+	//reportCode = utils.MD5(fmt.Sprintf("PPT%d", newItem.PptId))
+
+	// 更新报告分类计数
+	go func() {
+		_ = UpdateClassifyReportNum(classifyId)
+	}()
+	return
+}
+
+// 更新分类报告计数加个锁
+var classifyReportNumLock sync.Mutex
+
+// UpdateClassifyReportNum 更新分类报告计数
+func UpdateClassifyReportNum(classifyId int) (err error) {
+	classifyReportNumLock.Lock()
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info(fmt.Sprintf("更新分类报告计数失败, %v", err))
+		}
+		classifyReportNumLock.Unlock()
+	}()
+
+	classifyItem, e := models.GetClassifyById(classifyId)
+	if e != nil {
+		err = fmt.Errorf("获取分类失败, %v", e)
+		return
+	}
+
+	// 更新分类报告数
+	var total int
+	{
+		reportOb := new(models.Report)
+		var cond string
+		switch classifyItem.Level {
+		case 1:
+			cond += ` AND classify_id_first = ?`
+		case 2:
+			cond += ` AND classify_id_second = ?`
+		case 3:
+			cond += ` AND classify_id_third = ?`
+		}
+		pars := make([]interface{}, 0)
+		pars = append(pars, classifyId, classifyId)
+		count, e := reportOb.GetCountByCondition(cond, pars)
+		if e != nil {
+			err = fmt.Errorf("获取报告计数失败, %v", e)
+			return
+		}
+		total += count
+	}
+	{
+		pptOb := new(models.PptV2)
+		cond := ` AND classify_id = ?`
+		pars := make([]interface{}, 0)
+		pars = append(pars, classifyId)
+		count, e := pptOb.GetCountByCondition(cond, pars)
+		if e != nil {
+			err = fmt.Errorf("获取PPT报告计数失败, %v", e)
+			return
+		}
+		total += count
+	}
+	classifyItem.ReportNum = total
+	if e = classifyItem.UpdateClassify([]string{"ReportNum"}); e != nil {
+		err = fmt.Errorf("更新分类报告计数失败, %v", e)
+		return
+	}
+
+	// 获取所有分类, 更新父级, 无父级忽略
+	if classifyItem.ParentId <= 0 {
+		return
+	}
+	classifyOb := new(models.Classify)
+	classifies, e := classifyOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+	if e != nil {
+		err = fmt.Errorf("获取报告列表失败, %v", e)
+		return
+	}
+	classifyIdMap := make(map[int]*models.Classify)
+	classifyParentChildMap := make(map[int][]*models.Classify)
+	for _, v := range classifies {
+		classifyIdMap[v.Id] = v
+		if v.ParentId <= 0 {
+			continue
+		}
+		if classifyParentChildMap[v.ParentId] == nil {
+			classifyParentChildMap[v.ParentId] = make([]*models.Classify, 0)
+		}
+		classifyParentChildMap[v.ParentId] = append(classifyParentChildMap[v.ParentId], v)
+	}
+
+	// 递归获取需要更新的父级报告数
+	updateClassifies := CountParentClassifyReportNumRecursive(classifies, classifyItem.ParentId, classifyIdMap, classifyParentChildMap)
+	if len(updateClassifies) == 0 {
+		return
+	}
+	for _, v := range updateClassifies {
+		if e = v.UpdateClassify([]string{"ReportNum"}); e != nil {
+			err = fmt.Errorf("更新父级分类报告计数失败, %v", e)
+			return
+		}
+	}
+	return
+}
+
+// CountParentClassifyReportNumRecursive 递归统计父级分类报告数
+func CountParentClassifyReportNumRecursive(list []*models.Classify, parentId int, classifyIdMap map[int]*models.Classify, classifyParentChildMap map[int][]*models.Classify) []*models.Classify {
+	res := make([]*models.Classify, 0)
+	for _, v := range list {
+		// 找父级
+		if v.Id != parentId {
+			continue
+		}
+		parentItem := classifyIdMap[v.Id]
+		if parentItem == nil {
+			break
+		}
+
+		// 合计所有直系子分类报告数
+		children := classifyParentChildMap[v.Id]
+		if len(children) == 0 {
+			break
+		}
+		var t int
+		for _, child := range children {
+			t += child.ReportNum
+		}
+		parentItem.ReportNum = t
+		res = append(res, parentItem)
+
+		// 继续向上统计父级
+		if v.ParentId <= 0 {
+			break
+		}
+		parents := CountParentClassifyReportNumRecursive(list, v.ParentId, classifyIdMap, classifyParentChildMap)
+		if len(parents) == 0 {
+			break
+		}
+		res = append(res, parents...)
+	}
+	return res
+}
+
+// CreateReport 创建报告
+func CreateReport(outReportId, classifyId int, title string, topicEndTime time.Time, creator string, authors []string) (reportId int, err error) {
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info(fmt.Sprintf("创建外部报告失败, OutReportId: %d, %v", outReportId, err))
+		}
+	}()
+
+	// 获取用户
+	outIdAdmin := make(map[string]*models.Admin)
+	{
+		ob := new(models.Admin)
+		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取用户列表失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			if v.OutId == "" {
+				continue
+			}
+			outIdAdmin[v.OutId] = v
+		}
+	}
+	creatorAdmin := outIdAdmin[creator]
+	if creatorAdmin == nil {
+		err = fmt.Errorf("创建人不存在, OutId: %s", creator)
+		return
+	}
+
+	// 各级分类信息
+	var classifyIdFirst, classifyIdSecond, classifyIdThird int
+	var classifyNameFirst, classifyNameSecond, classifyNameThird string
+	{
+		classifyOb := new(models.Classify)
+		classifies, e := classifyOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取报告列表失败, %v", e)
+			return
+		}
+		classifyIdMap := make(map[int]*models.Classify)
+		for _, v := range classifies {
+			classifyIdMap[v.Id] = v
+		}
+		thisClassify := classifyIdMap[classifyId]
+		if thisClassify == nil {
+			err = fmt.Errorf("分类不存在, ID: %d", classifyId)
+			return
+		}
+
+		// 根据levelPath确认各级分类信息
+		levelArr := strings.Split(thisClassify.LevelPath, ",")
+		levelLen := len(levelArr)
+		if levelLen <= 0 {
+			err = fmt.Errorf("分类层级信息有误, ID: %d, LevelPath: %s", classifyId, thisClassify.LevelPath)
+			return
+		}
+		if levelLen > 2 {
+			strThird := levelArr[2]
+			classifyIdThird, _ = strconv.Atoi(strThird)
+			classifyThird := classifyIdMap[classifyIdThird]
+			if classifyThird != nil {
+				classifyNameThird = classifyThird.ClassifyName
+			}
+		}
+		if levelLen > 1 {
+			strSecond := levelArr[1]
+			classifyIdSecond, _ = strconv.Atoi(strSecond)
+			classifySecond := classifyIdMap[classifyIdSecond]
+			if classifySecond != nil {
+				classifyNameSecond = classifySecond.ClassifyName
+			}
+		}
+		if levelLen > 0 {
+			strFirst := levelArr[0]
+			classifyIdFirst, _ = strconv.Atoi(strFirst)
+			classifyFirst := classifyIdMap[classifyIdFirst]
+			if classifyFirst != nil {
+				classifyNameFirst = classifyFirst.ClassifyName
+			}
+		}
+	}
+
+	// 新报告信息
+	newItem := new(models.Report)
+	newItem.AddType = utils.ReportAddTypeInherit // 固定继承
+	newItem.ReportVersion = 2                    // 固定新版报告
+	newItem.ClassifyIdFirst = classifyIdFirst
+	newItem.ClassifyNameFirst = classifyNameFirst
+	newItem.ClassifyIdSecond = classifyIdSecond
+	newItem.ClassifyNameSecond = classifyNameSecond
+	newItem.ClassifyIdThird = classifyIdThird
+	newItem.ClassifyNameThird = classifyNameThird
+	newItem.Title = title
+	newItem.Frequency = "日度"                     // 默认日度吧,这个也没啥用
+	newItem.State = models.ReportStateWaitSubmit // 固定待提交
+	newItem.Stage = 0                            // 期数去掉了
+	newItem.NeedSplice = 1                       // TODO:是否默认拼接版头
+	newItem.CollaborateType = 2                  // 固定协作报告
+	newItem.HasChapter = 1                       // 协作报告固定有章节
+	newItem.ReportLayout = 2                     // 固定智能布局
+	newItem.IsPublicPublish = 1                  // 固定公开发布
+	newItem.ReportCreateTime = time.Now()
+	newItem.ReportSource = utils.ReportSourceOuter // 固定外部报告
+	newItem.OutReportId = strconv.Itoa(outReportId)
+	newItem.TopicEndTime = topicEndTime // 课题结束时间
+	newItem.AdminId = creatorAdmin.AdminId
+	newItem.AdminRealName = creatorAdmin.RealName
+	newItem.ContentModifyTime = time.Now()
+	newItem.CreateTime = time.Now()
+	newItem.ModifyTime = time.Now()
+	newItem.LastModifyAdminId = creatorAdmin.AdminId
+	newItem.LastModifyAdminName = creatorAdmin.RealName
+
+	// 协作人信息
+	newGrants := make([]*models.ReportGrant, 0)
+	var partnerIds []int
+	if len(authors) > 0 {
+		for _, v := range authors {
+			ad := outIdAdmin[v]
+			// 忽略无法识别的工号
+			if ad == nil {
+				continue
+			}
+			partnerIds = append(partnerIds, ad.AdminId)
+		}
+	}
+	for _, v := range partnerIds {
+		newGrants = append(newGrants, &models.ReportGrant{
+			AdminId:    v,
+			CreateTime: time.Now(),
+		})
+	}
+
+	// 自动继承该分类上一篇外部报告(审批通过的)
+	newChapters := make([]*models.ReportChapter, 0)
+	inheritItem := new(models.Report)
+	{
+		ob := new(models.Report)
+		cond := ` AND report_source = ? AND state = ? AND classify_id_first = ? AND classify_id_second = ? AND classify_id_third = ?`
+		pars := make([]interface{}, 0)
+		pars = append(pars, utils.ReportSourceOuter, models.ReportStatePass, classifyIdFirst, classifyIdSecond, classifyIdThird)
+		inheritItem, _ = ob.GetItemByCondition(cond, pars, "")
+	}
+	if inheritItem != nil && inheritItem.Id > 0 {
+		newItem.InheritReportId = inheritItem.Id
+		newItem.Abstract = inheritItem.Abstract
+		newItem.Content = inheritItem.Content
+		newItem.ContentSub = inheritItem.ContentSub
+		newItem.ContentStruct = inheritItem.ContentStruct
+		newItem.HeadImg = inheritItem.HeadImg
+		newItem.EndImg = inheritItem.EndImg
+		newItem.CanvasColor = inheritItem.CanvasColor
+		newItem.HeadResourceId = inheritItem.HeadResourceId
+		newItem.EndResourceId = inheritItem.EndResourceId
+
+		// 继承报告章节内容
+		chapterOb := new(models.ReportChapter)
+		chapterCond := ` AND report_id = ?`
+		chapterPars := make([]interface{}, 0)
+		chapterPars = append(chapterPars, inheritItem.Id)
+		inheritChapters, e := chapterOb.GetItemsByCondition(chapterCond, chapterPars, []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取继承报告章节失败, %v", e)
+			return
+		}
+		for _, v := range inheritChapters {
+			v.ReportChapterId = 0
+			v.ReportId = 0
+			v.ClassifyIdFirst = classifyIdFirst
+			v.ClassifyNameFirst = classifyNameFirst
+			v.PublishState = 1 // 未发布
+			v.CreateTime = time.Now()
+			v.ModifyTime = time.Now()
+			v.ContentModifyTime = time.Now()
+			v.LastModifyAdminId = creatorAdmin.AdminId
+			v.LastModifyAdminName = creatorAdmin.RealName
+			newChapters = append(newChapters, v)
+		}
+
+		// TODO:继承的协作人
+	}
+
+	// 新增报告、章节以及协作人权限
+	newId, e := newItem.CreateReportAndChapters(newItem, newChapters, newGrants)
+	if e != nil {
+		err = fmt.Errorf("新增报告、章节及授权失败, %v", e)
+		return
+	}
+	if newId <= 0 {
+		err = fmt.Errorf("新增报告ID为0")
+		return
+	}
+	reportId = newId
+
+	// 更新唯一编码、更新报告分类计数
+	go func() {
+		newItem.ReportCode = utils.MD5(strconv.Itoa(newItem.Id))
+		if e = newItem.Update([]string{"ReportCode"}); e != nil {
+			utils.FileLog.Info(fmt.Sprintf("报告唯一编码更想失败, ID: %d", newItem.Id))
+		}
+		_ = UpdateClassifyReportNum(classifyId)
+	}()
+	return
+}
+
+// EditReport 编辑报告
+func EditReport(reportItem *models.Report, title string, topicEndTime time.Time, authors []string) (err error) {
+	if reportItem == nil {
+		err = fmt.Errorf("报告信息有误")
+		return
+	}
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info(fmt.Sprintf("编辑外部报告失败, OutReportId: %s, %v", reportItem.OutReportId, err))
+		}
+	}()
+
+	// 获取用户
+	outIdAdmin := make(map[string]*models.Admin)
+	{
+		ob := new(models.Admin)
+		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取用户列表失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			if v.OutId == "" {
+				continue
+			}
+			outIdAdmin[v.OutId] = v
+		}
+	}
+
+	// 比对协作人,移除被删除协作人的权限
+	var removePartner []int
+	{
+		var originPartner []int
+		var partnerIds []int
+		if len(authors) > 0 {
+			for _, v := range authors {
+				ad := outIdAdmin[v]
+				// 忽略无法识别的工号
+				if ad == nil {
+					continue
+				}
+				partnerIds = append(partnerIds, ad.AdminId)
+			}
+		}
+		grantOb := new(models.ReportGrant)
+		grantCond := ` AND report_id = ?`
+		grantPars := make([]interface{}, 0)
+		grantPars = append(grantPars, reportItem.Id)
+		grants, e := grantOb.GetItemsByCondition(grantCond, grantPars, []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取报告授权失败, %v", e)
+			return
+		}
+		for _, v := range grants {
+			originPartner = append(originPartner, v.AdminId)
+		}
+		if len(partnerIds) == 0 {
+			removePartner = originPartner
+		}
+		// 原协作人不在新协作人中则移除
+		for _, v := range originPartner {
+			if !utils.InArrayByInt(partnerIds, v) {
+				removePartner = append(removePartner, v)
+			}
+		}
+	}
+
+	// 获取章节IDs,已有章节清除授权
+	var chapterIds []int
+	if len(removePartner) > 0 {
+		chapterOb := new(models.ReportChapter)
+		chapterCond := ` AND report_id = ?`
+		chapterPars := make([]interface{}, 0)
+		chapterPars = append(chapterPars, reportItem.Id)
+		chapters, e := chapterOb.GetItemsByCondition(chapterCond, chapterPars, []string{"report_chapter_id"}, "")
+		if e != nil {
+			err = fmt.Errorf("获取报告章节失败, %v", e)
+			return
+		}
+		for _, v := range chapters {
+			chapterIds = append(chapterIds, v.ReportChapterId)
+		}
+	}
+
+	// 更新报告和移除授权
+	reportItem.Title = title
+	reportItem.TopicEndTime = topicEndTime
+	reportItem.ModifyTime = time.Now()
+	updateCols := []string{"Title", "TopicEndTime", "ModifyTime"}
+	if e := reportItem.EditReportAndClearGrant(reportItem, updateCols, chapterIds, removePartner); e != nil {
+		err = fmt.Errorf("更新报告失败, %v", e)
+		return
+	}
+	return
+}
+
+// EditPptReport 编辑PPT报告
+func EditPptReport(pptItem *models.PptV2, title string, topicEndTime time.Time, authors []string) (err error) {
+	if pptItem == nil {
+		err = fmt.Errorf("PPT报告信息有误")
+		return
+	}
+	defer func() {
+		if err != nil {
+			utils.FileLog.Info(fmt.Sprintf("编辑外部PPT报告失败, OutReportId: %s, %v", pptItem.OutReportId, err))
+		}
+	}()
+
+	// 获取用户
+	outIdAdmin := make(map[string]*models.Admin)
+	{
+		ob := new(models.Admin)
+		list, e := ob.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取用户列表失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			if v.OutId == "" {
+				continue
+			}
+			outIdAdmin[v.OutId] = v
+		}
+	}
+
+	// 新建PPT基础信息
+	pptItem.Title = title
+	pptItem.TopicEndTime = topicEndTime
+	if len(authors) > 0 {
+		var partnerArr []string
+		for _, v := range authors {
+			ad := outIdAdmin[v]
+			// 忽略无法识别的工号
+			if ad == nil {
+				continue
+			}
+			partnerArr = append(partnerArr, strconv.Itoa(ad.AdminId))
+		}
+		pptItem.CollaborateUsers = strings.Trim(strings.Join(partnerArr, ","), `"`)
+	}
+	updateCols := []string{"Title", "TopicEndTime", "CollaborateUsers"}
+	if e := pptItem.Update(updateCols); e != nil {
+		err = fmt.Errorf("更新PPT报告失败, %v", e)
+		return
+	}
+	return
+}