Browse Source

Merge remote-tracking branch 'origin/master' into 5.2

# Conflicts:
#	models/tables/admin/query.go
#	services/wechat/template_msg.go
Roc 2 years ago
parent
commit
f11ae90b6a

+ 321 - 0
controller/voice_broadcast/voice_broadcast.go

@@ -0,0 +1,321 @@
+package voice_broadcast
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"hongze/hongze_yb/controller/response"
+	"hongze/hongze_yb/global"
+	"hongze/hongze_yb/models/request"
+	voiceResp "hongze/hongze_yb/models/response"
+	"hongze/hongze_yb/models/tables/voice_broadcast"
+	"hongze/hongze_yb/models/tables/voice_section"
+	"hongze/hongze_yb/services"
+	"hongze/hongze_yb/services/company"
+	"hongze/hongze_yb/services/user"
+	"hongze/hongze_yb/services/wechat"
+	"hongze/hongze_yb/utils"
+	"io/ioutil"
+	"os"
+	"path"
+	"strconv"
+	"time"
+)
+
+// BroadcastList
+// @Description 语音播报列表
+// @Param page_index			query int false "页码"
+// @Param page_size				query int false "每页数量"
+// @Param broadcast_id			query int false "语音播报id"
+// @Param section_id			query int false "板块id"
+// @Success 200 {object} []voiceResp.BroadcastListResp
+// @failure 400 {string} string "获取失败"
+// @Router /list [Post]
+func BroadcastList(c *gin.Context) {
+	var req request.BroadcastListReq
+	if err := c.Bind(&req); err != nil {
+		response.Fail("参数有误", c)
+		return
+	}
+	if req.PageIndex == 0 {
+		req.PageIndex = 1
+	}
+	if req.PageSize == 0 {
+		req.PageSize = utils.PageSize20
+	}
+
+	userinfo := user.GetInfoByClaims(c)
+	ok, checkInfo, _, err := company.CheckBaseFiccPermission(userinfo.CompanyID, int(userinfo.UserID))
+	if err != nil {
+		response.FailMsg("用户权限验证失败", "CheckBaseAuth-用户权限验证失败" + err.Error(), c)
+		c.Abort()
+		return
+	}
+	if !ok {
+		response.AuthError(checkInfo, "暂无权限", c)
+		c.Abort()
+		return
+	}
+	list, err := services.GetVoiceBroadcastList(req.PageIndex, req.PageSize, req.SectionId, req.BroadcastId, userinfo)
+	if err != nil {
+
+		response.FailMsg("获取语音播报列表失败,"+err.Error(), "QuestionList ErrMsg:"+err.Error(), c)
+		return
+	}
+
+	isVoiceAdmin, _, err := services.GetVoiceAdminByUserInfo(userinfo)
+	if err != nil && err != utils.ErrNoRow {
+		response.FailMsg("获取语音管理员信息失败", "QuestionList ErrMsg:"+err.Error(), c)
+		return
+	}
+	var resp voiceResp.BroadcastListResp
+	resp.List = list
+	resp.IsVoiceAdmin = isVoiceAdmin
+	response.OkData("获取成功", resp, c)
+}
+
+// AddBroadcast
+// @Description 新建语音播报
+// @Param file  query  string  true  "音频文件"
+// @Success 200 {string} string "发布成功"
+// @failure 400 {string} string "发布失败"
+// @Router /add [post]
+func AddBroadcast(c *gin.Context) {
+	broadcastName := c.PostForm("broadcast_name")
+	fmt.Println("broadcastName:",broadcastName)
+	nsectionId := c.PostForm("section_id")
+	sectionId, _ := strconv.Atoi(nsectionId)
+	sectionName := c.PostForm("section_name")
+	nvarietyId := c.PostForm("variety_id")
+	varietyId, _ := strconv.Atoi(nvarietyId)
+	varietyName := c.PostForm("variety_name")
+	nauthorId := c.PostForm("author_id")
+	authorId, _ := strconv.Atoi(nauthorId)
+	author := c.PostForm("author")
+	imgUrl := c.PostForm("img_url")
+	file, err := c.FormFile("file")
+	if err != nil {
+		response.FailMsg("获取资源失败", "获取资源失败, Err:"+err.Error(), c)
+		return
+	}
+
+	ext := path.Ext(file.Filename)
+	if ext != ".mp3" {
+		response.Fail("暂仅支持mp3格式", c)
+		return
+	}
+	dateDir := time.Now().Format("20060102")
+	localDir := global.CONFIG.Serve.StaticDir + "hongze/" + dateDir
+	if err := os.MkdirAll(localDir, 0766); err != nil {
+		response.FailMsg("存储目录创建失败", "QuestionUploadAudio 存储目录创建失败, Err:"+err.Error(), c)
+		return
+	}
+	randStr := utils.GetRandStringNoSpecialChar(28)
+	filtName := randStr + ext
+	fpath := localDir + "/" + filtName
+	defer func() {
+		_ = os.Remove(fpath)
+	}()
+	// 生成文件至指定目录
+	if err := c.SaveUploadedFile(file, fpath); err != nil {
+		response.FailMsg("文件生成失败", "QuestionUploadAudio 文件生成失败, Err:"+err.Error(), c)
+		return
+	}
+	// 获取音频文件时长
+	fByte, err := ioutil.ReadFile(fpath)
+	if err != nil {
+		response.FailMsg("读取本地文件失败", "QuestionUploadAudio 读取本地文件失败", c)
+		return
+	}
+	if len(fByte) <= 0 {
+		response.FailMsg("文件大小有误", "QuestionUploadAudio 文件大小有误", c)
+		return
+	}
+	seconds, err := services.GetMP3PlayDuration(fByte)
+	if err != nil {
+		response.FailMsg("读取文件时长失败", "QuestionUploadAudio 读取文件时长失败", c)
+		return
+	}
+	// 音频大小MB
+	fi, err := os.Stat(fpath)
+	if err != nil {
+		response.FailMsg("读取文件大小失败", "QuestionUploadAudio 读取文件大小失败", c)
+		return
+	}
+	mb := utils.Bit2MB(fi.Size(), 2)
+	// 上传文件至阿里云
+	ossDir := "yb_wx/voice_broadcast/"
+	resourceUrl, err := services.UploadAliyunToDir(filtName, fpath, ossDir)
+	if err != nil {
+		response.FailMsg("文件上传失败", "QuestionUploadAudio 文件上传失败, Err:"+err.Error(), c)
+		return
+	}
+
+	voiceBroadcast := voice_broadcast.VoiceBroadcast{
+		BroadcastName:    broadcastName,
+		SectionId:        sectionId,
+		SectionName:      sectionName,
+		VarietyId:        varietyId,
+		VarietyName:      varietyName,
+		AuthorId:         authorId,
+		Author:           author,
+		ImgUrl:           imgUrl,
+		VoiceUrl:         resourceUrl,
+		VoicePlaySeconds: fmt.Sprint(seconds),
+		VoiceSize:        fmt.Sprint(mb),
+		CreateTime:       time.Now().Format(utils.FormatDateTime),
+	}
+	err = voiceBroadcast.AddVoiceBroadcast()
+	if err != nil {
+		fmt.Println("AddUserViewHistory err", err.Error())
+	}
+
+	// 推送回复消息给用户
+	go wechat.SendVoiceBroadcastWxMsg(voiceBroadcast.BroadcastId, voiceBroadcast.SectionName, voiceBroadcast.BroadcastName)
+
+	//同花顺客群
+	go services.SendVoiceBroadcastToThs(voiceBroadcast)
+	response.Ok("发布成功", c)
+}
+
+// SectionList
+// @Description 语音播报板块列表
+// @Success 200 {object} []voiceResp.VarietyList
+// @failure 400 {string} string "获取失败"
+// @Router /section/list [get]
+func SectionList(c *gin.Context) {
+	sList, err := voice_section.GetVoiceSection()
+	if err != nil {
+		response.FailMsg("查询语音播报板块失败", "GetVoiceSection, Err:"+err.Error(), c)
+	}
+	vList, err := voice_section.GetVoiceVariety()
+	if err != nil {
+		response.FailMsg("查询语音播报板块失败", "GetVoiceSection, Err:"+err.Error(), c)
+	}
+	var sectionList []voiceResp.SectionList
+	var varietyList []voiceResp.VarietyList
+	var resp []voiceResp.VarietyList
+	//var resp voiceResp.SectionListResp
+	//for _, s := range sList {
+	//	section := voiceResp.SectionList{
+	//		SectionId:   s.SectionId,
+	//		SectionName: s.SectionName,
+	//		Status:      s.Status,
+	//	}
+	//	sectionList = append(sectionList, section)
+	//}
+	var newsList []*voice_section.VoiceSection
+	//var bannedSectionList []*voice_section.VoiceSection
+
+	//查找被禁用的板块ids
+	var bannedIds []int
+	for _, section := range sList {
+		if section.Status == 0 {
+			//bannedSectionList = append(bannedSectionList, section)
+			bannedIds = append(bannedIds, section.SectionId)
+		} else {
+			newsList = append(newsList, section)
+		}
+	}
+
+	//如果有被禁用的板块,去语音列表查找被禁用板块有没有语音
+	var lists []*voice_broadcast.VoiceBroadcast
+	if len(bannedIds) > 0{
+		lists, err = voice_section.GetVoiceSectionFromBroadcast(bannedIds)
+		if err != nil {
+			response.FailMsg("查询语音播报禁用板块失败", "GetVoiceSectionFromBroadcast, Err:"+err.Error(), c)
+		}
+	}
+
+	//被禁用板块有语音,依然显示该板块
+	if len(lists) > 0 {
+		//清空切片,用新的
+		newsList = newsList[0:0]
+		bannedMap := make(map[int]int)
+		for _, broadcast := range lists {
+			bannedMap[broadcast.SectionId] = broadcast.SectionId
+		}
+		for _, section := range sList {
+			_,ok := bannedMap[section.SectionId]
+			if section.Status != 0 || ok {
+				newsList = append(newsList, section)
+			}
+		}
+	}
+
+	for _, v := range vList {
+		variety := voiceResp.VarietyList{
+			VarietyId:   v.VarietyId,
+			VarietyName: v.VarietyName,
+		}
+		varietyList = append(varietyList, variety)
+	}
+
+	for _, v := range varietyList {
+		for _, s := range newsList {
+			if v.VarietyId == s.VarietyId {
+				section := voiceResp.SectionList{
+					ImgUrl:      s.ImgUrl,
+					SectionId:   s.SectionId,
+					SectionName: s.SectionName,
+					Status:      s.Status,
+				}
+				sectionList = append(sectionList, section)
+			}
+		}
+		if len(sectionList) == 0{
+			continue
+		}
+		v.Children = sectionList
+		resp = append(resp, v)
+		sectionList = []voiceResp.SectionList{}
+	}
+	response.OkData("上传成功", resp, c)
+}
+
+// DelBroadcast
+// @Description 删除语音播报
+// @Param broadcast_id			query int false "语音播报id"
+// @Success 200 {string} string "删除成功"
+// @failure 400 {string} string "删除失败"
+// @Router /delete [get]
+func DelBroadcast(c *gin.Context) {
+	sbroadcastId := c.DefaultQuery("broadcast_id", "0")
+	broadcastId, err := strconv.Atoi(sbroadcastId)
+	if err != nil {
+		response.FailMsg("转换id失败,请输入正确的id", "strconv.Atoi, Err:"+err.Error(), c)
+	}
+	if broadcastId <= 0 {
+		response.FailMsg("参数错误","参数有误", c)
+		return
+	}
+	var item voice_broadcast.VoiceBroadcast
+	item.BroadcastId = broadcastId
+	err = item.DelVoiceBroadcast()
+	if err != nil {
+		response.FailMsg("删除语音播报失败", "DelVoiceBroadcast, Err:"+err.Error(), c)
+	}
+	response.Ok("删除成功", c)
+}
+
+// AddStatistics
+// @Description 新增语音播报记录
+// @Param file  query  string  true  "音频文件"
+// @Success 200 {string} string "新增成功"
+// @failure 400 {string} string "新增失败"
+// @Router /statistics/add [post]
+func AddStatistics(c *gin.Context) {
+	var req request.AddBroadcastStatisticsReq
+	if err := c.Bind(&req); err != nil {
+		response.Fail("参数有误", c)
+		return
+	}
+
+	if req.BroadcastId <= 0{
+		response.Fail("参数有误", c)
+	}
+	userinfo := user.GetInfoByClaims(c)
+	
+	go services.AddBroadcastRecord(userinfo, req.Source, req.BroadcastId)
+
+	response.Ok("新增记录成功", c)
+}

+ 1 - 1
go.mod

@@ -26,9 +26,9 @@ require (
 	github.com/swaggo/gin-swagger v1.3.3
 	github.com/swaggo/swag v1.7.4
 	github.com/tosone/minimp3 v1.0.1
+	github.com/wenzhenxi/gorsa v0.0.0-20220418014903-15feec0f05a6
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
 	golang.org/x/image v0.0.0-20190802002840-cff245a6509b
-	golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42 // indirect
 	golang.org/x/text v0.3.7 // indirect
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 	gorm.io/driver/mysql v1.1.3

+ 2 - 0
init_serve/router.go

@@ -69,5 +69,7 @@ func InitRouter() (r *gin.Engine) {
 	routers.InitPriceDriven(r)
 	//沙盘逻辑推演路由
 	routers.InitSandbox(r)
+	//语音播报
+	routers.InitVoiceBroadcast(r)
 	return
 }

+ 2 - 2
logic/user/user.go

@@ -418,7 +418,7 @@ func GetUserTabBar(userInfo user.UserInfo, version string) (list []string, err e
 		err = errors.New("获取客户信息失败, Err: " + e.Error())
 		return
 	}
-	if companyProduct != nil {
+	if companyProduct != nil && companyProduct.Status != "" {
 		if strings.Contains("永续,正式", companyProduct.Status) {
 			authOk = true
 		}
@@ -525,7 +525,7 @@ func GetTopTab(userInfo user.UserInfo, version string) (list []*TopTab, err erro
 		err = errors.New("获取客户信息失败, Err: " + e.Error())
 		return
 	}
-	if companyProduct != nil {
+	if companyProduct != nil && companyProduct.Status != "" {
 		if strings.Contains("永续,正式", companyProduct.Status) {
 			authOk = true
 		}

+ 1 - 1
middleware/recover.go

@@ -18,7 +18,7 @@ func Recover() gin.HandlerFunc {
 		contentType := c.ContentType()
 		// 为 multipart forms 设置较低的内存限制(50M) (默认是 32 MiB)
 		if contentType == "multipart/form-data" {
-			err := c.Request.ParseMultipartForm(-50 << 20)
+			err := c.Request.ParseMultipartForm(10 << 20)
 			if err != nil {
 				response.Custom(http.StatusRequestEntityTooLarge, "上传文件太大,err:"+err.Error(), c)
 				c.Abort()

+ 1 - 1
models/request/user/user.go

@@ -13,7 +13,7 @@ type ApplyReq struct {
 	CompanyName     string `description:"公司名称" json:"company_name"`
 	RealName        string `description:"用户真实姓名" json:"real_name"`
 	Permission      string `description:"用户关注品种,多个品种之间用英文,隔开" json:"permission"`
-	Source			int	   `description:"申请来源:1-我的 2-活动 3-图库"`
+	Source			int	   `description:"申请来源:1-我的 2-活动 3-图库 4-报告详情 5-问答社区 6-价格驱动 7-沙盘推演 8-语音播报"`
 	SourceAgent     int    `description:"申请入口来源,1:小程序,2:pc" json:"source_agent"`
 	FromPage        string `description:"申请来源页面" json:"from_page"`
 }

+ 40 - 0
models/request/voice_broadcast.go

@@ -0,0 +1,40 @@
+package request
+
+import "mime/multipart"
+
+type BroadcastListReq struct {
+	PageIndex   int `json:"page_index" form:"page_index"`
+	PageSize    int `json:"page_size" form:"page_size"`
+	BroadcastId int `json:"broadcast_id" form:"broadcast_id"`
+	SectionId   int `json:"section_id" form:"section_id"`
+}
+
+type AddBroadcastReq struct {
+	BroadcastName string                `json:"broadcast_name" `
+	SectionId     int                   `json:"section_id" `
+	SectionName   string                `json:"section_name"`
+	VarietyId     int                   `json:"variety_id"`
+	VarietyName   string                `json:"variety_name"`
+	AuthorId      int                   `json:"author_id"`
+	Author        string                `json:"author"`
+	File          *multipart.FileHeader `json:"file"`
+}
+
+//type AddBroadcastReq struct {
+//	BroadcastName    string `json:"page_index" `
+//	SectionId        int    `json:"section_id" `
+//	SectionName      string `json:"section_name"`
+//	VarietyId        int    `json:"variety_id"`
+//	VarietyName      string `json:"variety_name"`
+//	AuthorId         int    `json:"author_id"`
+//	Author           string `json:"author"`
+//	VoiceUrl         string `json:"voice_url"`
+//	VoicePlaySeconds string `json:"voice_play_seconds"`
+//	VoiceSize        string `json:"voice_size"`
+//	CreateTime       string `json:"create_time" `
+//}
+
+type AddBroadcastStatisticsReq struct {
+	Source      int    `json:"source" description:"点击来源,1手机小程序,2pc小程序,3web端"`
+	BroadcastId int `json:"broadcast_id" `
+}

+ 39 - 0
models/response/voice_broadcast.go

@@ -0,0 +1,39 @@
+package response
+
+// BroadcastListResp 语音播报列表resp
+type BroadcastListResp struct {
+	List         []Broadcast
+	IsVoiceAdmin bool `description:"是否为语音管理员"`
+}
+
+type Broadcast struct {
+	BroadcastId      int    `description:"语音ID"`
+	BroadcastName    string `description:"语音名称"`
+	SectionId        int    `description:"语音分类ID"`
+	SectionName      string `description:"语音分类名称"`
+	VarietyId        int    `description:"品种id"`
+	VarietyName      string `description:"品种名称"`
+	AuthorId         int    `description:"作者id"`
+	Author           string `description:"作者"`
+	ImgUrl           string `description:"背景图url"`
+	VoiceUrl         string `description:"音频url"`
+	VoicePlaySeconds string `description:"音频时长"`
+	VoiceSize        string `description:"音频大小"`
+	CreateTime       string `description:"创建时间"`
+	IsAuthor         bool   `description:"是否为作者"`
+}
+
+//type SectionListResp struct {
+//	List []VarietyList
+//}
+type VarietyList struct {
+	VarietyId   int
+	VarietyName string
+	Children    []SectionList
+}
+type SectionList struct {
+	ImgUrl      string
+	SectionId   int
+	SectionName string
+	Status      int
+}

+ 6 - 0
models/tables/admin/query.go

@@ -16,6 +16,12 @@ func GetAdminByMobile(mobile string) (item *Admin, err error) {
 	return
 }
 
+// GetAdminByEmail 通过邮箱获取系统用户信息
+func GetAdminByEmail(mobile string) (item *Admin, err error) {
+	err = global.DEFAULT_MYSQL.Model(Admin{}).Where("email = ? AND enabled = 1", mobile).First(&item).Error
+	return
+}
+
 // GetVWangInfo 获取沛总的账户信息
 func GetVWangInfo() (item *Admin, err error) {
 	return GetByAdminId(66)

+ 11 - 0
models/tables/report_send_ths_detail/create.go

@@ -0,0 +1,11 @@
+package models
+
+import "hongze/hongze_yb/global"
+
+//新增报告发送给同花顺的记录
+func AddReportSendThsDetail(item *ReportSendThsDetail) (lastId int, err error) {
+	err = global.DEFAULT_MYSQL.Create(item).Error
+	lastId = item.SendId
+	return
+}
+

+ 16 - 0
models/tables/report_send_ths_detail/query.go

@@ -0,0 +1,16 @@
+package models
+
+import "hongze/hongze_yb/global"
+
+//根据报告id获取发送记录
+func GetVoiceSendThsDetailById(voiceId int, reportType string) (item *ReportSendThsDetail, err error) {
+	err = global.DEFAULT_MYSQL.Model(ReportSendThsDetail{}).Where("report_id = ? AND report_type=?", voiceId,reportType).Order("send_id DESC").Scan(&item).Error
+	return
+}
+
+// GetLatelyReportSendThsDetail 获取发送中/发送成功的 距离现在最近的一条记录
+func GetLatelyReportSendThsDetail() (item *ReportSendThsDetail, err error) {
+	err = global.DEFAULT_MYSQL.Model(ReportSendThsDetail{}).Where(" status >=0").Order("push_time desc,send_id desc").Scan(&item).Error
+	return
+}
+

+ 20 - 0
models/tables/report_send_ths_detail/report_send_ths_detail.go

@@ -0,0 +1,20 @@
+package models
+
+import (
+	"time"
+)
+
+//报告推送给同花顺的表结构体
+type ReportSendThsDetail struct {
+	SendId     int       `orm:"column(send_id);pk" description:"发送给同花顺的Id"`
+	ReportId   int       `description:"报告id"`
+	ReportType string    `description:"报告类型"`
+	Status     int8      `description:"发送结果,0:待发送,-1发送失败,1发送成功"`
+	Remark     string    `description:"失败原因"`
+	PushTime   time.Time `description:"实际开始推送时间/预推送时间"`
+	CreateTime time.Time `description:"发送时间"`
+}
+
+func (r *ReportSendThsDetail) TableName() string {
+	return "report_send_ths_detail"
+}

+ 14 - 0
models/tables/report_send_ths_detail/update.go

@@ -0,0 +1,14 @@
+package models
+
+import (
+	"hongze/hongze_yb/global"
+)
+
+//修改报告发送给同花顺的记录状态
+func ModifyReportSendThsDetailStatus(sendId int, status int8, remark string) (err error) {
+	err = global.DEFAULT_MYSQL.Model(ReportSendThsDetail{}).Where("send_id = ? ", sendId).Updates(ReportSendThsDetail{
+		Status:     status,
+		Remark:     remark,
+	}).Error
+	return
+}

+ 17 - 0
models/tables/sys_role_admin/query.go

@@ -0,0 +1,17 @@
+package sys_role_admin
+
+import "hongze/hongze_yb/global"
+
+func GetVoiceAdmin(adminId int) (item *SysRoleAdmin, err error) {
+	if global.CONFIG.Serve.RunMode == "release" {
+		err = global.DEFAULT_MYSQL.Model(SysRoleAdmin{}).Where("role_id=32 AND admin_id=?",adminId).First(&item).Error
+	}else {
+		err = global.DEFAULT_MYSQL.Model(SysRoleAdmin{}).Where("role_id=33 AND admin_id=?",adminId).First(&item).Error
+	}
+	return
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *SysRoleAdmin) TableName() string {
+	return "sys_role_admin"
+}

+ 8 - 0
models/tables/sys_role_admin/sys_role_admin.go

@@ -0,0 +1,8 @@
+package sys_role_admin
+
+type SysRoleAdmin struct {
+	Id         int
+	RoleId     int
+	AdminId    int
+	CreateTime string
+}

+ 9 - 0
models/tables/voice_broadcast/create.go

@@ -0,0 +1,9 @@
+package voice_broadcast
+
+import "hongze/hongze_yb/global"
+
+// AddVoiceBroadcast 新增记录
+func (voiceBroadcast *VoiceBroadcast) AddVoiceBroadcast() (err error) {
+	err = global.DEFAULT_MYSQL.Create(voiceBroadcast).Error
+	return
+}

+ 9 - 0
models/tables/voice_broadcast/delete.go

@@ -0,0 +1,9 @@
+package voice_broadcast
+
+import "hongze/hongze_yb/global"
+
+// DelVoiceBroadcast 删除记录
+func (voiceBroadcast *VoiceBroadcast) DelVoiceBroadcast() (err error) {
+	err = global.DEFAULT_MYSQL.Delete(&voiceBroadcast).Error
+	return
+}

+ 26 - 0
models/tables/voice_broadcast/query.go

@@ -0,0 +1,26 @@
+package voice_broadcast
+
+import "hongze/hongze_yb/global"
+
+func GetBroadcastByCondition(pageIndex, pageSize, sectionId int) (list []*VoiceBroadcast, err error) {
+	offset := (pageIndex - 1) * pageSize
+	err = global.DEFAULT_MYSQL.Model(VoiceBroadcast{}).Where("section_id=?", sectionId).Offset(offset).Limit(pageSize).Order("create_time DESC").Scan(&list).Error
+	return
+}
+
+func GetBroadcastByIdAndPage(pageIndex, pageSize, broadcastId int) (list []*VoiceBroadcast, err error) {
+	offset := (pageIndex - 1) * pageSize
+	err = global.DEFAULT_MYSQL.Model(VoiceBroadcast{}).Where("broadcast_id=?", broadcastId).Offset(offset).Limit(pageSize).Order("create_time DESC").Scan(&list).Error
+	return
+}
+
+func GetBroadcast(pageIndex, pageSize int) (list []*VoiceBroadcast, err error) {
+	offset := (pageIndex - 1) * pageSize
+	err = global.DEFAULT_MYSQL.Model(VoiceBroadcast{}).Offset(offset).Limit(pageSize).Order("create_time DESC").Scan(&list).Error
+	return
+}
+
+func GetBroadcastById(broadcastId int) (list *VoiceBroadcast, err error) {
+	err = global.DEFAULT_MYSQL.Model(VoiceBroadcast{}).Where("broadcast_id=?", broadcastId).First(&list).Error
+	return
+}

+ 22 - 0
models/tables/voice_broadcast/voice_broadcast.go

@@ -0,0 +1,22 @@
+package voice_broadcast
+
+type VoiceBroadcast struct {
+	BroadcastId      int    `gorm:"primaryKey;column:broadcast_id;type:int(11)" description:"语音ID"`
+	BroadcastName    string `description:"语音名称"`
+	SectionId        int    `description:"语音分类ID"`
+	SectionName      string `description:"语音分类名称"`
+	VarietyId        int    `description:"品种id"`
+	VarietyName      string `description:"品种名称"`
+	AuthorId         int    `description:"作者id"`
+	Author           string `description:"作者"`
+	ImgUrl           string `description:"背景图url"`
+	VoiceUrl         string `description:"音频url"`
+	VoicePlaySeconds string `description:"音频时长"`
+	VoiceSize        string `description:"音频大小"`
+	CreateTime       string `description:"创建时间"`
+}
+
+// TableName get sql table name.获取数据库表名
+func (m *VoiceBroadcast) TableName() string {
+	return "yb_voice_broadcast"
+}

+ 9 - 0
models/tables/voice_broadcast_statistics/create.go

@@ -0,0 +1,9 @@
+package voice_broadcast_statistics
+
+import "hongze/hongze_yb/global"
+
+// AddBroadcastStatistics 新增记录
+func (voiceBroadcastStatistics *VoiceBroadcastStatistics) AddBroadcastStatistics() (err error) {
+	err = global.DEFAULT_MYSQL.Create(voiceBroadcastStatistics).Error
+	return
+}

+ 28 - 0
models/tables/voice_broadcast_statistics/voice_broadcast_statistics.go

@@ -0,0 +1,28 @@
+package voice_broadcast_statistics
+
+type VoiceBroadcastStatistics struct {
+	Id            int    `orm:"column(id);pk"`
+	CompanyId     int    `description:"客户ID"`
+	CompanyName   string `description:"客户名称"`
+	UserId        int    `description:"用户ID"`
+	RealName      string `description:"用户名称"`
+	Mobile        string `description:"用户手机号"`
+	Email         string `description:"电子邮箱"`
+	CompanyStatus string `description:"客户状态""`
+	Source        int    `description:"点击来源,1手机小程序,2pc小程序,3web端""`
+	BroadcastId   int    `description:"语音ID"`
+	BroadcastName string `description:"语音名称"`
+	SectionId     int    `description:"语音分类ID"`
+	SectionName   string `description:"语音分类名称"`
+	VarietyId     int    `description:"品种id"`
+	VarietyName   string `description:"品种名称"`
+	AuthorId      int    `description:"作者id"`
+	Author        string `description:"语音管理员"`
+	PublishTime   string `description:"发布时间"`
+	CreateTime    string `description:"访问时间"`
+}
+
+// TableName 表名变更
+func (voiceBroadcastStatistics *VoiceBroadcastStatistics) TableName() string {
+	return "yb_voice_broadcast_statistics"
+}

+ 24 - 0
models/tables/voice_section/query.go

@@ -0,0 +1,24 @@
+package voice_section
+
+import (
+	"hongze/hongze_yb/global"
+	"hongze/hongze_yb/models/tables/voice_broadcast"
+)
+
+// GetVoiceSection 查询所有语音播报章节
+func GetVoiceSection() (list []*VoiceSection, err error) {
+	err = global.DEFAULT_MYSQL.Order("create_time").Find(&list).Error
+	return
+}
+
+// GetVoiceSection 查询所有语音播报章节
+func GetVoiceVariety() (list []*VoiceSection, err error) {
+	err = global.DEFAULT_MYSQL.Group("variety_id").Order("create_time").Find(&list).Error
+	return
+}
+
+// GetVoiceSectionFromBroadcast 查询所有语音播报章节
+func GetVoiceSectionFromBroadcast(bannedIds []int) (list []*voice_broadcast.VoiceBroadcast, err error) {
+	err = global.DEFAULT_MYSQL.Where("section_id IN (?)", bannedIds).Group("section_id").Find(&list).Error
+	return
+}

+ 16 - 0
models/tables/voice_section/voice_section.go

@@ -0,0 +1,16 @@
+package voice_section
+
+type VoiceSection struct {
+	SectionId   int    `orm:"column(section_id);pk" description:"板块id"`
+	SectionName string `description:"板块名称"`
+	VarietyId   int    `description:"品种id"`
+	VarietyName string `description:"品种名称"`
+	Status      int    `description:"角色状态"`
+	ImgUrl      string `description:"背景图url"`
+	CreateTime  string `description:"创建时间"`
+}
+
+// TableName 表名变更
+func (voiceSection *VoiceSection) TableName() string {
+	return "yb_voice_section"
+}

+ 16 - 0
models/tables/wx_user/query.go

@@ -39,4 +39,20 @@ func GetByUserId(userId int) (wxUser *WxUser, err error) {
 func GetByUserIds(userIds []uint64) (list []*WxUser, err error) {
 	err = global.DEFAULT_MYSQL.Model(WxUser{}).Where("user_id in  ? ", userIds).Scan(&list).Error
 	return
+}
+
+type OpenIdList struct {
+	OpenID string
+	UserID int
+}
+
+func GetOpenIdList() (items []*OpenIdList, err error) {
+	sql := `SELECT DISTINCT ur.open_id,wu.user_id FROM wx_user AS wu 
+          INNER JOIN company AS c ON c.company_id = wu.company_id 
+          INNER JOIN company_product AS d ON c.company_id=d.company_id
+		INNER join user_record  as ur on wu.user_id=ur.user_id
+          WHERE ur.open_id != "" AND ur.subscribe=1 and ur.create_platform=1 AND  d.status IN('正式','试用','永续') `
+
+	err = global.DEFAULT_MYSQL.Raw(sql).Scan(&items).Error
+	return
 }

+ 17 - 0
routers/voice_broadcast.go

@@ -0,0 +1,17 @@
+package routers
+
+import (
+	"github.com/gin-gonic/gin"
+	"hongze/hongze_yb/controller/voice_broadcast"
+	"hongze/hongze_yb/middleware"
+)
+
+func InitVoiceBroadcast(r *gin.Engine)  {
+	rGroup := r.Group("api/voice/broadcast").Use(middleware.Token())
+	rGroup.POST("/list", voice_broadcast.BroadcastList)
+	rGroup.POST("/add", voice_broadcast.AddBroadcast)
+	rGroup.GET("/section/list", voice_broadcast.SectionList)
+	rGroup.GET("/delete", voice_broadcast.DelBroadcast)
+	rGroup.POST("/statistics/add", voice_broadcast.AddStatistics)
+}
+

+ 4 - 0
services/company/permission.go

@@ -485,6 +485,7 @@ type ChartPermissionCheckInfo struct {
 	Mobile       string       `json:"mobile" description:"手机号"`
 	Type         string       `json:"type" description:"无权限,需要前端处理的类型,枚举值:expired, apply, contact"`
 	CustomerInfo CustomerInfo `json:"customer_info" description:"客户信息"`
+	Jump         string       `json:"jump" description:"需要跳转的页面,sandbox_list"`
 }
 
 // CheckUserChartPermission 验证用户/联系人的图库权限
@@ -888,6 +889,9 @@ func CheckUserSandboxPermission(companyId int64, userId, permissionId int) (ok b
 					ok = true
 				}
 			}
+			if ok == false {
+				permissionCheckInfo.Jump = `sandbox_list` //如果是没有单个品种的权限,那么提示后并跳转到沙盘列表页吧
+			}
 		} else {
 			// 不校验品种权限的话,那么就是认为有权限的
 			if len(companyPermissionList) > 0 {

+ 239 - 0
services/report_push.go

@@ -0,0 +1,239 @@
+package services
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"github.com/wenzhenxi/gorsa"
+	"hongze/hongze_yb/global"
+	models "hongze/hongze_yb/models/tables/report_send_ths_detail"
+	"hongze/hongze_yb/models/tables/voice_broadcast"
+	"hongze/hongze_yb/services/alarm_msg"
+	"hongze/hongze_yb/services/wechat"
+	"hongze/hongze_yb/utils"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+//func init() {
+//	report, _ := models.GetReportById(572)
+//	SendReportToThs(report)
+//}
+
+var permissionMap map[string]string = map[string]string{
+	"化里化外日评":    "原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱",
+	"股债日评":      "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"贵金属复盘":     "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"每日经济数据备忘录": "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"宏观商品复盘":    "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"知白守黑日评":    "钢材,铁矿,双焦(焦煤、焦炭)",
+	"有声有色日度闲篇":  "有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"EIA原油库存点评": "原油",
+	"苯乙烯数据点评":   "苯乙烯",
+	"API原油库存点评": "原油",
+	"铁矿航运数据点评":  "铁矿",
+	"中观需求点评":    "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"聚酯数据点评":    "PTA,MEG",
+	"钢材周度数据点评":  "钢材",
+	"寻根知本":      "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"国际宏观":      "宏观,利率债,原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱,钢材,铁矿,双焦(焦煤、焦炭),有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"能化百家谈":     "原油,PTA,MEG,织造终端,甲醇,聚烯烃,沥青,橡胶,苯乙烯,玻璃纯碱",
+	"有色百家谈":     "有色(铜、铝),有色(锌、铅),镍+不锈钢",
+	"黑色百家谈":     "钢材,铁矿,双焦(焦煤、焦炭)",
+}
+
+// TshResult 同花顺返回信息
+type TshResult struct {
+	ErrorCode int    `json:"error" description:"错误状态码"`
+	Message   string `json:"message" description:"提示信息"`
+}
+
+var (
+	THS_SendUrl string //同花顺地址url
+	THS_PubKey  string //同花顺公钥
+)
+
+func init() {
+	THS_PubKey = `-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqugglfCboOEfWtHlGBOW
+40a4Y3xOs0MPBwjTOzHgcaWzx5XCc20VftGVXkWlpjs8u4dza/Bp1SV7SJ5Y7U95
+jgUOP8Js9Qgp6UVqBJDJf3i1KpjHzlk3ma8zxAYUAdieEUE+SKSxSY+BD9A6lpf5
+n+igXLmzR5GeVGFeLzoMhB1+pXgGhW30ao9wPwuRF7DBl+FKa/ACi7iXLiwXVgqT
+FFi29TKeerEENu3EpMXvPml7tNUiVmVW6d83hlascfbAlkShwuHLSGpLqK7brtg6
+jRS9hreKFKb0BUQ4TB26e7IDCstbMRvUp4+OGezexzic5NYPQ8uLo5OTaS7f7PrW
+ZwIDAQAB
+-----END PUBLIC KEY-----`
+	if global.CONFIG.Serve.RunMode == "release" {
+		//同花顺正式地址
+		THS_SendUrl = `https://board.10jqka.com.cn/gateway/ps/syncNews`
+	} else {
+		//同花顺测试地址
+		THS_SendUrl = `https://mtest.10jqka.com.cn/gateway/ps/syncNews`
+	}
+}
+// SendThs 发送消息到同花顺
+func SendThs(title, labelStr, abstract, jumpBaseUrl, logoUrl, dataType string) (err error) {
+	defer func() {
+		if err != nil {
+			//fmt.Println(utils.APPNAME+"【"+utils.RunMode+"】"+"失败提醒", "发送消息至同花顺失败 ErrMsg:"+err.Error(), utils.EmailSendToUsers)
+			go alarm_msg.SendAlarmMsg("发送消息至同花顺失败 ErrMsg:"+err.Error(), 3)
+			//go utils.SendEmail(utils.APPNAME+"【"+utils.RunMode+"】"+"失败提醒", "发送报告至同花顺失败 ErrMsg:"+err.Error(), utils.EmailSendToUsers)
+		}
+	}()
+	pubKey := THS_PubKey
+	sendUrl := THS_SendUrl
+
+	//标题字符长度截取,最多50位字符
+	title = utils.SubStr(title, 50)
+	global.LOG.Info(fmt.Sprintf("title:%s", title))
+	title, err = gorsa.PublicEncrypt(title, pubKey)
+	if err != nil {
+		return
+	}
+
+	//简介字符长度截取,最多50位字符
+	abstract = utils.SubStr(abstract, 50)
+	global.LOG.Info(fmt.Sprintf("abstract:%s", abstract))
+	abstract, err = gorsa.PublicEncrypt(abstract, pubKey)
+	if err != nil {
+		return
+	}
+
+	global.LOG.Info(fmt.Sprintf("labelStr:%s", labelStr))
+	label, err := gorsa.PublicEncrypt(labelStr, pubKey)
+	if err != nil {
+		return
+	}
+
+	jumpUrl, err := gorsa.PublicEncrypt(jumpBaseUrl, pubKey)
+	if err != nil {
+		return
+	}
+
+	picUrl, err := gorsa.PublicEncrypt(logoUrl, pubKey)
+	if err != nil {
+		return
+	}
+
+	dataTypeEncript, err := gorsa.PublicEncrypt(dataType, pubKey)
+	if err != nil {
+		return
+	}
+
+	//开始发送
+	client := http.Client{}
+	form := url.Values{}
+	form.Add("title", title)
+	form.Add("description", abstract)
+	form.Add("label", label)
+	form.Add("url", jumpUrl)
+	form.Add("icon", picUrl)
+	form.Add("dataType", dataTypeEncript)
+
+	global.LOG.Info(fmt.Sprintf("SendThs parms:%s", form.Encode()))
+	resp, err := client.PostForm(sendUrl, form)
+	if err != nil {
+		return
+	}
+	defer resp.Body.Close()
+
+	body, _ := ioutil.ReadAll(resp.Body)
+
+	//fmt.Println(string(body))
+	global.LOG.Info(fmt.Sprintf("ThsResult parms:%s", string(body)))
+
+	//同花顺接口返回数据
+	var tshResult TshResult
+	err = json.Unmarshal(body, &tshResult)
+	if err != nil {
+		err = errors.New(fmt.Sprint("同花顺接口返回数据转换成结构体异常,Err:", err))
+		return
+	}
+	if tshResult.ErrorCode != 1 {
+		err = errors.New(fmt.Sprint("发送数据到同花顺接口异常,result:", string(body)))
+		return
+	}
+	return
+}
+
+// SendVoiceBroadcastToThs 发送语音播报到同花顺
+func SendVoiceBroadcastToThs(voice voice_broadcast.VoiceBroadcast) (err error) {
+	defer func() {
+		if err != nil {
+			go alarm_msg.SendAlarmMsg("发送语音播报至同花顺失败 ErrMsg:"+err.Error(), 3)
+			//go utils.SendEmail(utils.APPNAME+"【"+utils.RunMode+"】"+"失败提醒", "SendReportMiniToThs发送报告至同花顺失败, ReportId:" + strconv.Itoa(report.Id) + ", ErrMsg:" + err.Error(), utils.EmailSendToUsers)
+		}
+	}()
+	permissionName := "宏观" //写死宏观,默认所有群都推
+	//小程序跳转地址
+	jumpBaseUrl := wechat.WxYbAppId + `/pages/voice/voice?voiceId=`
+
+	logoUrl := `https://hongze.oss-cn-shanghai.aliyuncs.com/hzyj.png`
+
+	sendDetail, err := models.GetVoiceSendThsDetailById(voice.BroadcastId, "语音播报")
+	if err != nil && err != utils.ErrNoRow {
+		return
+	} else if err == nil && sendDetail != nil {
+		if sendDetail.Status >= 0 {
+			fmt.Println("重复发送")
+			return
+		}
+	}
+
+	pushTime := time.Now()      //预发送时间
+	isPrePush := false          //是否预发布
+	addTime := time.Minute * 40 //短时间内多次发布报告,每篇报告的间隔时间(目前暂定40分钟,只有日度点评的报告需要限制)
+
+	//获取距离现在最近的一条发送成功失败记录
+	latelySendDetail, err := models.GetLatelyReportSendThsDetail()
+	//如果存在最近一条发送记录,那么去校验时间
+	if (err == nil && latelySendDetail != nil) || err == utils.ErrNoRow {
+		pushTime = latelySendDetail.PushTime.Add(addTime)
+		//如果最近一条的发送记录 的 (发送时间 + 每篇报告的间隔时间)  晚于 当前时间
+		if pushTime.After(time.Now()) {
+			isPrePush = true
+		} else {
+			pushTime = time.Now()
+		}
+	}
+
+	if isPrePush { //预发布,只添加预发布记录,不立马发送报告,等待定时任务推送消息给同花顺
+		newSendDetail := &models.ReportSendThsDetail{
+			ReportId:   voice.BroadcastId,
+			ReportType: "语音播报",
+			Status:     2,
+			PushTime:   pushTime,
+			CreateTime: time.Now(),
+		}
+		_, tmpErr := models.AddReportSendThsDetail(newSendDetail)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+	} else {
+		newSendDetail := &models.ReportSendThsDetail{
+			ReportId:   voice.BroadcastId,
+			ReportType: "语音播报",
+			Status:     0,
+			PushTime:   pushTime,
+			CreateTime: time.Now(),
+		}
+		sendDetailId, tmpErr := models.AddReportSendThsDetail(newSendDetail)
+		if tmpErr != nil {
+			err = tmpErr
+			return
+		}
+		//及时发送
+		dataType := "2" //内容类型:1文字 2小程序
+		err = SendThs(voice.BroadcastName, permissionName, voice.BroadcastName, fmt.Sprint(jumpBaseUrl, voice.BroadcastId), logoUrl, dataType)
+		if err != nil {
+			_ = models.ModifyReportSendThsDetailStatus(int(sendDetailId), -1, err.Error())
+			return
+		}
+		_ = models.ModifyReportSendThsDetailStatus(int(sendDetailId), 1, "")
+	}
+
+	return
+}

+ 209 - 0
services/voice_broadcast.go

@@ -0,0 +1,209 @@
+package services
+
+import (
+	"errors"
+	"fmt"
+	"hongze/hongze_yb/global"
+	"hongze/hongze_yb/models/response"
+	admin2 "hongze/hongze_yb/models/tables/admin"
+	"hongze/hongze_yb/models/tables/company_product"
+	"hongze/hongze_yb/models/tables/sys_role_admin"
+	"hongze/hongze_yb/models/tables/voice_broadcast"
+	"hongze/hongze_yb/models/tables/voice_broadcast_statistics"
+	"hongze/hongze_yb/services/user"
+	"hongze/hongze_yb/utils"
+	"time"
+)
+
+func GetVoiceBroadcastList(pageindex, pagesize, sectionId, broadcastId int, userInfo user.UserInfo)  (list []response.Broadcast, err error){
+	if broadcastId == 0 {
+		if sectionId == 0 {
+			broadList, e := voice_broadcast.GetBroadcast(pageindex, pagesize)
+			if e != nil {
+				e = errors.New("获取语音播报列表失败 Err:" + e.Error())
+			}
+			for _, item := range broadList {
+				var respItem response.Broadcast
+				respItem = response.Broadcast{
+					BroadcastId:      item.BroadcastId,
+					BroadcastName:    item.BroadcastName,
+					SectionId:        item.SectionId,
+					SectionName:      item.SectionName,
+					VarietyId:        item.VarietyId,
+					VarietyName:      item.VarietyName,
+					AuthorId:         item.AuthorId,
+					Author:           item.Author,
+					ImgUrl:           item.ImgUrl,
+					VoiceUrl:         item.VoiceUrl,
+					VoicePlaySeconds: item.VoicePlaySeconds,
+					VoiceSize:        item.VoiceSize,
+					CreateTime:       item.CreateTime,
+					IsAuthor:         false,
+				}
+				if int(userInfo.UserID) == item.AuthorId{
+					respItem.IsAuthor = true
+				}
+				list = append(list, respItem)
+
+			}
+			err = e
+			return
+		}
+		broadList, e := voice_broadcast.GetBroadcastByCondition(pageindex, pagesize, sectionId)
+		if e != nil {
+			e = errors.New("获取语音播报列表失败 Err:" + e.Error())
+		}
+		for _, item := range broadList {
+			var respItem response.Broadcast
+			respItem = response.Broadcast{
+				BroadcastId:      item.BroadcastId,
+				BroadcastName:    item.BroadcastName,
+				SectionId:        item.SectionId,
+				SectionName:      item.SectionName,
+				VarietyId:        item.VarietyId,
+				VarietyName:      item.VarietyName,
+				AuthorId:         item.AuthorId,
+				Author:           item.Author,
+				ImgUrl:           item.ImgUrl,
+				VoiceUrl:         item.VoiceUrl,
+				VoicePlaySeconds: item.VoicePlaySeconds,
+				VoiceSize:        item.VoiceSize,
+				CreateTime:       item.CreateTime,
+				IsAuthor:         false,
+			}
+			if int(userInfo.UserID) == item.AuthorId{
+				respItem.IsAuthor = true
+			}
+			list = append(list, respItem)
+
+		}
+		err = e
+		return
+	}else {
+		broadList, e := voice_broadcast.GetBroadcastByIdAndPage(pageindex, pagesize, broadcastId)
+		if e != nil {
+			e = errors.New("获取语音播报列表失败 Err:" + e.Error())
+		}
+		for _, item := range broadList {
+			var respItem response.Broadcast
+			respItem = response.Broadcast{
+				BroadcastId:      item.BroadcastId,
+				BroadcastName:    item.BroadcastName,
+				SectionId:        item.SectionId,
+				SectionName:      item.SectionName,
+				VarietyId:        item.VarietyId,
+				VarietyName:      item.VarietyName,
+				AuthorId:         item.AuthorId,
+				Author:           item.Author,
+				ImgUrl:           item.ImgUrl,
+				VoiceUrl:         item.VoiceUrl,
+				VoicePlaySeconds: item.VoicePlaySeconds,
+				VoiceSize:        item.VoiceSize,
+				CreateTime:       item.CreateTime,
+				IsAuthor:         false,
+			}
+			if int(userInfo.UserID) == item.AuthorId{
+				respItem.IsAuthor = true
+			}
+			list = append(list, respItem)
+
+		}
+		err = e
+		return
+	}
+}
+
+// GetVoiceAdminByUserInfo 判断当前用户是否为语音管理员
+func GetVoiceAdminByUserInfo(userInfo user.UserInfo) (ok bool, adminInfo *admin2.Admin, err error) {
+	mobile := userInfo.Mobile
+	var email string
+	if mobile == "" {
+		// 用户有可能是通过邮箱登录
+		email = userInfo.Email
+		if email == "" {
+			return
+		}
+	}
+
+	if userInfo.CompanyID != 16 {
+		return
+	}
+	if mobile != "" {
+		adminInfo, err = admin2.GetAdminByMobile(mobile)
+		if err != nil {
+			if err == utils.ErrNoRow {
+				err = nil
+				return
+			}
+			return
+		}
+	} else {
+		adminInfo, err = admin2.GetAdminByEmail(email)
+		if err != nil {
+			if err == utils.ErrNoRow {
+				err = nil
+				return
+			}
+			return
+		}
+	}
+
+	if adminInfo.Enabled != 1 {
+		return
+	}
+	_,err = sys_role_admin.GetVoiceAdmin(int(adminInfo.AdminID))
+	if err != nil && err != utils.ErrNoRow{
+		return
+	}
+
+	if err == utils.ErrNoRow {
+		ok = false
+		return
+	}
+	ok = true
+
+
+	return
+}
+
+func AddBroadcastRecord(userinfo user.UserInfo, source, broadcastId int) {
+	var err error
+	defer func() {
+		if err != nil {
+			global.LOG.Critical(fmt.Sprintf("AddBroadcastLog: userId=%d, err:%s", userinfo.UserID, err.Error()))
+		}
+	}()
+	companyProduct, err := company_product.GetByCompany2ProductId(userinfo.CompanyID,1)
+	if err != nil {
+		return
+	}
+	broadcast, err := voice_broadcast.GetBroadcastById(broadcastId)
+	if err != nil {
+		return
+	}
+	voiceBroadcastStatistics := voice_broadcast_statistics.VoiceBroadcastStatistics{
+		CompanyId:     companyProduct.CompanyID,
+		CompanyName:   companyProduct.CompanyName,
+		UserId:        int(userinfo.UserID),
+		RealName:      userinfo.RealName,
+		Mobile:        userinfo.Mobile,
+		Email:         userinfo.Email,
+		CompanyStatus: companyProduct.Status,
+		Source:        source,
+		BroadcastId:   broadcastId,
+		BroadcastName: broadcast.BroadcastName,
+		SectionId:     broadcast.SectionId,
+		SectionName:   broadcast.SectionName,
+		VarietyId:     broadcast.VarietyId,
+		VarietyName:   broadcast.VarietyName,
+		AuthorId:      broadcast.AuthorId,
+		Author:        broadcast.Author,
+		PublishTime:   broadcast.CreateTime,
+		CreateTime:    time.Now().Format(utils.FormatDateTime),
+	}
+	err = voiceBroadcastStatistics.AddBroadcastStatistics()
+	if err != nil {
+		return
+	}
+	return
+}

+ 94 - 0
services/wechat/template_msg.go

@@ -9,6 +9,7 @@ import (
 	"hongze/hongze_yb/global"
 	"hongze/hongze_yb/models/tables/user_record"
 	"hongze/hongze_yb/models/tables/user_template_record"
+	"hongze/hongze_yb/models/tables/wx_user"
 	"hongze/hongze_yb/services/alarm_msg"
 	"hongze/hongze_yb/utils"
 	"io/ioutil"
@@ -235,6 +236,99 @@ func SendQuestionReplyWxMsg(questionId, userId int, questionTitle string) (err e
 	return
 }
 
+// SendVoiceBroadcastWxMsg 推送研报小程序模板消息-语音播报
+func SendVoiceBroadcastWxMsg(broadcastId int, sectionName, broadcastName string) (err error) {
+	var errMsg string
+	defer func() {
+		if err != nil {
+			alarmMsg := fmt.Sprintf("SendVoiceBroadcastWxMsg-推送语音播报模版消息失败; broadcastId: %d; Err: %s; Msg: %s", broadcastId, err.Error(), errMsg)
+			go alarm_msg.SendAlarmMsg(alarmMsg, 3)
+		}
+	}()
+
+	List, err := wx_user.GetOpenIdList()
+	if err != nil {
+		return
+	}
+	openIdList := make([]*OpenIdList, 0)
+	for _, item := range List {
+		openIdList = append(openIdList, &OpenIdList{
+			OpenId: item.OpenID,
+			UserId: item.UserID,
+		})
+	}
+	//openIdList = append(openIdList, &OpenIdList{
+	//			OpenId: "oN0jD1cuiBLxV1IRpu74_oHnoOjk",
+	//			UserId: 52709,
+	//		})
+	sendMap := make(map[string]interface{})
+	sendData := make(map[string]interface{})
+
+	first := "您好,有新的语音播报待查看"
+	keyword1 := sectionName + ":" + broadcastName
+	keyword2 := "待查看"
+	remark := "请点击详情查看"
+
+	sendData["first"] = map[string]interface{}{"value": first, "color": "#173177"}
+	sendData["keyword1"] = map[string]interface{}{"value": keyword1, "color": "#173177"}
+	sendData["keyword2"] = map[string]interface{}{"value": keyword2, "color": "#173177"}
+	sendData["remark"] = map[string]interface{}{"value": remark, "color": "#173177"}
+
+	sendMap["template_id"] = TemplateIdWithCommunityQuestion
+	sendMap["data"] = sendData
+
+	wxAppPath := fmt.Sprintf("pages/voice/voice?voiceId=%d", broadcastId)
+	if global.CONFIG.Serve.RunMode == "debug" {
+		// 仅测试环境测试用
+		wxAppPath = "pages-report/reportDetail?reportId=3800"
+	}
+	if wxAppPath != "" {
+		sendMap["miniprogram"] = map[string]interface{}{"appid": WxYbAppId, "pagepath": wxAppPath}
+	}
+	err = SendMultiTemplateMsgNoReturn(sendMap, openIdList, wxAppPath, utils.TEMPLATE_MSG_YB_VOICE_BROADCAST)
+	return
+}
+
+// SendMultiTemplateMsg 推送模板消息至多个用户中间出错不返回
+func SendMultiTemplateMsgNoReturn(sendMap map[string]interface{}, items []*OpenIdList, resource string, sendType int) (err error) {
+	ws := GetWxChat()
+	accessToken, err := ws.GetAccessToken()
+	if err != nil {
+		return
+	}
+	for _, item := range items {
+		sendMap["touser"] = item.OpenId
+		data, e := json.Marshal(sendMap)
+		if e != nil {
+			err = e
+			return
+		}
+		ts := &TemplateMsgSendClient{
+			AccessToken: accessToken,
+			Data:        data,
+		}
+		result, e := ts.SendMsg()
+		if result == nil {
+			return
+		}
+		// 推送消息记录
+		{
+			go func(v *OpenIdList) {
+				sendStatus := 1
+				if e != nil {
+					sendStatus = 0
+				}
+				resultJson, _ := json.Marshal(result)
+				_ = AddUserTemplateRecord(v.UserId, sendStatus, sendType, v.OpenId, resource, string(data), string(resultJson))
+			}(item)
+		}
+		if e != nil {
+			err = e
+		}
+	}
+	return
+}
+
 // SendQuestionToAdmin 推送研报小程序模板消息-用户提问时,通知到管理员
 func SendQuestionToAdmin(questionId, userId int, questionTitle string) (err error) {
 	if userId == 0 {

+ 11 - 0
utils/common.go

@@ -966,4 +966,15 @@ func Bit2MB(bitSize int64, prec int) (size float64) {
 	mb := float64(bitSize)/float64(1024*1024)
 	size, _ = strconv.ParseFloat(strconv.FormatFloat(mb,'f',prec,64), 64)
 	return
+}
+
+// SubStr 截取字符串(中文)
+func SubStr(str string, subLen int) string {
+	strRune := []rune(str)
+	bodyRuneLen := len(strRune)
+	if bodyRuneLen > subLen {
+		bodyRuneLen = subLen
+	}
+	str = string(strRune[:bodyRuneLen])
+	return str
 }

+ 1 - 0
utils/constants.go

@@ -136,6 +136,7 @@ const (
 	TEMPLATE_MSG_ACTIVITY_APPOINTMENT            //活动预约/报名时间通知
 	_
 	TEMPLATE_MSG_YB_COMMUNITY_QUESTION // 研报小程序-问答社区通知
+	TEMPLATE_MSG_YB_VOICE_BROADCAST // 研报小程序-语音播报
 )
 
 // 微信用户user_record注册平台