Bläddra i källkod

Merge branch 'feature/eta_2.6.6' into debug

hsun 15 timmar sedan
förälder
incheckning
aa46157c09

+ 1074 - 0
controllers/assessment/assessment_form.go

@@ -0,0 +1,1074 @@
+package assessment
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/services"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// AssessmentFormController 考核填报
+type AssessmentFormController struct {
+	controllers.BaseAuthController
+}
+
+// CheckAdd
+// @Title 新建填报权限
+// @Description 新建填报权限
+// @Success 200 string "获取成功"
+// @router /form/check_add [get]
+func (this *AssessmentFormController) CheckAdd() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	resp := new(models.AssessmentFormCheckAddResp)
+
+	// 校验研究员身份,非研究员无权新增
+	var researcherId int
+	{
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s = ? AND %s = ?`, researcherOb.Cols().AdminId, researcherOb.Cols().Enabled)
+		pars := make([]interface{}, 0)
+		pars = append(pars, sysUser.AdminId, models.AssessmentResearcherEnabled)
+		item, e := researcherOb.GetItemByCondition(cond, pars, "")
+		if e != nil && !utils.IsErrNoRow(e) {
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取研究员失败"
+			return
+		}
+		if item == nil || item.AssessmentResearcherId <= 0 {
+			br.Data = resp
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "获取成功"
+			return
+		}
+		researcherId = item.AssessmentResearcherId
+		resp.IsResearcher = true
+	}
+
+	// 获取关联品种,无关联品种无权新增
+	{
+		mappingOb := new(models.AssessmentResearcherVarietyMapping)
+		cond := fmt.Sprintf(` AND %s = ?`, mappingOb.Cols().AssessmentResearcherId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcherId)
+		mappings, e := mappingOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员品种关联失败, %v", e)
+			return
+		}
+		if len(mappings) == 0 {
+			br.Data = resp
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "获取成功"
+			return
+		}
+		resp.HasVariety = true
+	}
+
+	// 校验本周是否已填报过
+	formOb := new(models.AssessmentForm)
+	{
+		weekStart, weekEnd := utils.GetWeekRange(time.Now().Local())
+		cond := fmt.Sprintf(` AND %s = ? AND (%s BETWEEN ? AND ?)`, formOb.Cols().ResearcherId, formOb.Cols().CreateTime)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcherId, weekStart.Format(utils.FormatDateTime), weekEnd.Format(utils.FormatDateTime))
+		count, e := formOb.GetCountByCondition(cond, pars)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取本周填报单总数失败, %v", e)
+			return
+		}
+		if count == 0 {
+			resp.NewForm = true
+		}
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// WeekBase
+// @Title 获取本周填报信息
+// @Description 获取本周填报信息
+// @Success 200 string "获取成功"
+// @router /form/week_base [get]
+func (this *AssessmentFormController) WeekBase() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	resp := new(models.AssessmentFormViewResp)
+
+	// 校验研究员身份
+	researcher := new(models.AssessmentResearcher)
+	{
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s = ? AND %s = ?`, researcherOb.Cols().AdminId, researcherOb.Cols().Enabled)
+		pars := make([]interface{}, 0)
+		pars = append(pars, sysUser.AdminId, models.AssessmentResearcherEnabled)
+		item, e := researcherOb.GetItemByCondition(cond, pars, "")
+		if e != nil {
+			if utils.IsErrNoRow(e) {
+				br.Msg = "您不是研究员,不可填报观点"
+				return
+			}
+			br.Msg = "获取失败"
+			br.ErrMsg = "获取研究员失败"
+			return
+		}
+		if item == nil || item.AssessmentResearcherId <= 0 {
+			br.Msg = "您不是研究员,不可填报观点"
+			return
+		}
+		researcher = item
+	}
+
+	// 获取关联品种
+	varietyOb := new(models.AssessmentVariety)
+	varietyMatch := make(map[int]*models.AssessmentVariety)
+	{
+		varieties, e := varietyOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取品种列表失败, %v", e)
+			return
+		}
+		for _, v := range varieties {
+			varietyMatch[v.AssessmentVarietyId] = v
+		}
+	}
+	varietyMappings := make([]*models.AssessmentResearcherVarietyMapping, 0)
+	{
+		mappingOb := new(models.AssessmentResearcherVarietyMapping)
+		cond := fmt.Sprintf(` AND %s = ?`, mappingOb.Cols().AssessmentResearcherId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcher.AssessmentResearcherId)
+		mappings, e := mappingOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员品种关联失败, %v", e)
+			return
+		}
+		varietyMappings = mappings
+	}
+	if len(varietyMappings) == 0 {
+		br.Msg = "您无关联品种,不可填报观点"
+		return
+	}
+
+	// 校验本周是否已填报过
+	formOb := new(models.AssessmentForm)
+	{
+		weekStart, weekEnd := utils.GetWeekRange(time.Now().Local())
+		cond := fmt.Sprintf(` AND %s = ? AND (%s BETWEEN ? AND ?)`, formOb.Cols().ResearcherId, formOb.Cols().CreateTime)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcher.AssessmentResearcherId, weekStart.Format(utils.FormatDateTime), weekEnd.Format(utils.FormatDateTime))
+		count, e := formOb.GetCountByCondition(cond, pars)
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取本周填报单总数失败, %v", e)
+			return
+		}
+		if count > 0 {
+			br.Msg = "本周已填报,请勿重复填报"
+			return
+		}
+	}
+
+	// 获取单号、周度、价格基准日期
+	formCode, e := services.GenerateAssessmentFormCode()
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取填报单单号失败, %v", e)
+		return
+	}
+	weekTime, _, friday := services.GetAssessmentWeekAndFriday(time.Now().Local())
+
+	resp.List = make([]*models.AssessmentFormDetail, 0)
+	for _, v := range varietyMappings {
+		variety := varietyMatch[v.VarietyId]
+		if variety == nil {
+			continue
+		}
+		if variety.Enabled != models.AssessmentVarietyEnabled {
+			continue
+		}
+		t := new(models.AssessmentFormDetail)
+		t.FormCode = formCode
+		t.ResearcherId = researcher.AssessmentResearcherId
+		t.ResearcherAdminId = researcher.AdminId
+		t.ResearcherName = researcher.RealName
+		t.VarietyId = variety.AssessmentVarietyId
+		t.VarietyCode = variety.VarietyCode
+		t.VarietyName = variety.VarietyName
+		t.WeekTime = weekTime
+		t.BaseDate = utils.TimeTransferString(utils.FormatDate, friday)
+		resp.List = append(resp.List, t)
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// Detail
+// @Title 填报单详情
+// @Description 填报单详情
+// @Param   FormCode  query  string  true  "填报单号"
+// @Success 200 string "获取成功"
+// @router /form/detail [get]
+func (this *AssessmentFormController) Detail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	formCode := this.GetString("FormCode")
+	if formCode == "" {
+		br.Msg = "参数有误"
+		return
+	}
+	resp := new(models.AssessmentFormViewResp)
+
+	// 根据单号获取填报单
+	formOb := new(models.AssessmentForm)
+	cond := fmt.Sprintf(` AND %s = ?`, formOb.Cols().FormCode)
+	pars := make([]interface{}, 0)
+	pars = append(pars, formCode)
+	list, e := formOb.GetItemsByCondition(cond, pars, []string{}, "")
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取本周填报单总数失败, %v", e)
+		return
+	}
+	resp.List = make([]*models.AssessmentFormDetail, 0)
+	for _, v := range list {
+		resp.List = append(resp.List, v.Format2Detail())
+	}
+
+	// 获取品种价格详情及观点评价
+	varietyPrice, forecastComment, e := services.GetVarietyPriceAndForecastComment(list)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取品种价格详情及观点评价失败, %v", e)
+		return
+	}
+	resp.VarietyPrice = varietyPrice
+	resp.ForecastComment = forecastComment
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// PageList
+// @Title 填报单列表-分页
+// @Description 填报单列表-分页
+// @Param	request	body models.AssessmentFormPageListReq true "type json string"
+// @Success 200 string "获取成功"
+// @router /form/page_list [get]
+func (this *AssessmentFormController) PageList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	params := new(models.AssessmentFormPageListReq)
+	if e := this.ParseForm(params); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "参数解析失败, Err: " + e.Error()
+		return
+	}
+	resp := new(models.AssessmentFormPageListResp)
+	resp.List = make([]*models.AssessmentFormDetail, 0)
+
+	// 分页
+	var startSize int
+	if params.PageSize <= 0 {
+		params.PageSize = utils.PageSize20
+	}
+	if params.CurrentIndex <= 0 {
+		params.CurrentIndex = 1
+	}
+	startSize = utils.StartIndex(params.CurrentIndex, params.PageSize)
+
+	// 当前用户是否为研究员
+	researcher := new(models.AssessmentResearcher)
+	{
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s = ? AND %s = ?`, researcherOb.Cols().AdminId, researcherOb.Cols().Enabled)
+		pars := make([]interface{}, 0)
+		pars = append(pars, sysUser.AdminId, models.AssessmentResearcherEnabled)
+		item, e := researcherOb.GetItemByCondition(cond, pars, "")
+		if e != nil && !utils.IsErrNoRow(e) {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+			return
+		}
+		if item != nil && item.AssessmentResearcherId > 0 {
+			researcher = item
+		}
+	}
+	var authResearcherIds []int
+
+	// 筛选项
+	formOb := new(models.AssessmentForm)
+	condList := ``
+	parsList := make([]interface{}, 0)
+	{
+		// 可查看的其他研究员的填报
+		var researcherIds []int
+		if researcher != nil && researcher.AssessmentResearcherId > 0 {
+			researcherIds = append(researcherIds, researcher.AssessmentResearcherId)
+			authResearcherIds = append(authResearcherIds, researcher.AssessmentResearcherId)
+		}
+		mappingOb := new(models.AssessmentResearcherAdminMapping)
+		mappingCond := fmt.Sprintf(` AND %s = ?`, mappingOb.Cols().AuthAdminId)
+		mappingPars := make([]interface{}, 0)
+		mappingPars = append(mappingPars, sysUser.AdminId)
+		mappings, e := mappingOb.GetItemsByCondition(mappingCond, mappingPars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员权限失败, %v", e)
+			return
+		}
+		for _, v := range mappings {
+			if v.AuthType == models.AssessmentResearcherAdminAuthTypeView {
+				researcherIds = append(researcherIds, v.AssessmentResearcherId)
+				authResearcherIds = append(authResearcherIds, v.AssessmentResearcherId)
+			}
+		}
+
+		// 研究员筛选项,与有权限的取交集
+		if params.AssessmentResearcherIds != "" {
+			var parResearcherIds []int
+			arr := strings.Split(params.AssessmentResearcherIds, ",")
+			for _, v := range arr {
+				i, _ := strconv.Atoi(v)
+				if i > 0 {
+					parResearcherIds = append(parResearcherIds, i)
+				}
+			}
+			researcherIds = utils.IntersectInt(researcherIds, parResearcherIds)
+		}
+		if len(researcherIds) == 0 {
+			resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+			br.Data = resp
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "获取成功"
+			return
+		}
+		condList += fmt.Sprintf(` AND %s IN (?)`, formOb.Cols().ResearcherId)
+		parsList = append(parsList, researcherIds)
+
+		// 品种
+		if params.AssessmentVarietyIds != "" {
+			var varietyIds []int
+			arr := strings.Split(params.AssessmentVarietyIds, ",")
+			for _, v := range arr {
+				i, _ := strconv.Atoi(v)
+				if i > 0 {
+					varietyIds = append(varietyIds, i)
+				}
+			}
+			if len(varietyIds) == 0 {
+				resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+				br.Data = resp
+				br.Ret = 200
+				br.Success = true
+				br.Msg = "获取成功"
+				return
+			}
+			condList += fmt.Sprintf(` AND %s IN (?)`, formOb.Cols().VarietyId)
+			parsList = append(parsList, varietyIds)
+		}
+
+		// 基准日期筛选
+		params.StartTime = strings.TrimSpace(params.StartTime)
+		params.EndTime = strings.TrimSpace(params.EndTime)
+		if params.StartTime != "" && params.EndTime != "" {
+			_, e = time.ParseInLocation(utils.FormatDate, params.StartTime, time.Local)
+			if e != nil {
+				br.Msg = "开始日期格式有误"
+				return
+			}
+			_, e = time.ParseInLocation(utils.FormatDate, params.EndTime, time.Local)
+			if e != nil {
+				br.Msg = "结束日期格式有误"
+				return
+			}
+			st := fmt.Sprintf("%s 00:00:00", params.StartTime)
+			ed := fmt.Sprintf("%s 23:59:59", params.EndTime)
+			condList += fmt.Sprintf(` AND (%s BETWEEN ? AND ?)`, formOb.Cols().BaseDate)
+			parsList = append(parsList, st, ed)
+		}
+	}
+
+	// 列表
+	total, e := formOb.GetCountByCondition(condList, parsList)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员列表总数失败, %v", e)
+		return
+	}
+	orderRule := fmt.Sprintf(`%s ASC, %s DESC, %s DESC`, formOb.Cols().Status, formOb.Cols().CreateTime, formOb.Cols().SubmitTime) // 排序规则:未提交在最前面,然后按提交时间降序
+	list, e := formOb.GetPageItemsByCondition(condList, parsList, []string{}, orderRule, startSize, params.PageSize)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+		return
+	}
+	if total == 0 || len(list) == 0 {
+		resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+		br.Data = resp
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "获取成功"
+		return
+	}
+	for _, v := range list {
+		d := v.Format2Detail()
+		d.Button = services.CheckAssessmentFormButton(v, sysUser.AdminId, authResearcherIds)
+		resp.List = append(resp.List, d)
+	}
+
+	resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, total)
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// Save
+// @Title 保存填报单
+// @Description 保存填报单
+// @Param	request	body models.AssessmentFormSaveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /form/save [post]
+func (this *AssessmentFormController) Save() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentFormSaveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	if len(req.List) == 0 {
+		br.Msg = "填报单品种有误"
+		br.ErrMsg = "参数异常, 填报单品种数有误"
+		return
+	}
+	formOb := new(models.AssessmentForm)
+	firstFormId := req.List[0].AssessmentFormId
+	status := req.List[0].Status
+	researcherId := req.List[0].ResearcherId
+
+	// 如果是新增那么,校验本周是否已填报过
+	if firstFormId <= 0 {
+		weekStart, weekEnd := utils.GetWeekRange(time.Now().Local())
+		cond := fmt.Sprintf(` AND %s = ? AND (%s BETWEEN ? AND ?)`, formOb.Cols().ResearcherId, formOb.Cols().CreateTime)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcherId, weekStart.Format(utils.FormatDateTime), weekEnd.Format(utils.FormatDateTime))
+		count, e := formOb.GetCountByCondition(cond, pars)
+		if e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取本周填报单总数失败, %v", e)
+			return
+		}
+		if count > 0 {
+			br.Msg = "本周已填报,请勿重复填报"
+			return
+		}
+	}
+
+	insertForms, updateForms := make([]*models.AssessmentForm, 0), make([]*models.AssessmentForm, 0)
+	for _, v := range req.List {
+		if v.FormCode == "" {
+			br.Msg = "单号异常"
+			br.ErrMsg = "参数异常:单号"
+			return
+		}
+		if v.ResearcherId <= 0 {
+			br.Msg = "研究员异常"
+			br.ErrMsg = "参数异常:研究员ID"
+			return
+		}
+		if v.VarietyId <= 0 {
+			br.Msg = "品种异常"
+			br.ErrMsg = "参数异常:品种ID"
+			return
+		}
+		if v.WeekTime == "" {
+			br.Msg = "周度异常"
+			br.ErrMsg = "参数异常:周度"
+			return
+		}
+		if v.BaseDate == "" {
+			br.Msg = "价格基准日期异常"
+			br.ErrMsg = "参数异常:价格基准日期"
+			return
+		}
+		baseDate, e := time.ParseInLocation(utils.FormatDate, v.BaseDate, time.Local)
+		if e != nil {
+			br.Msg = "价格基准日期格式异常"
+			br.ErrMsg = fmt.Sprintf("价格基准日期格式异常, %s", v.BaseDate)
+			return
+		}
+		weekStart, weekEnd := utils.GetWeekRange(baseDate)
+
+		fm := new(models.AssessmentForm)
+		fm.AssessmentFormId = v.AssessmentFormId
+		fm.FormCode = v.FormCode
+		fm.ResearcherId = v.ResearcherId
+		fm.ResearcherAdminId = v.ResearcherAdminId
+		fm.ResearcherName = v.ResearcherName
+		fm.VarietyId = v.VarietyId
+		fm.VarietyCode = v.VarietyCode
+		fm.VarietyName = v.VarietyName
+		fm.WeekTime = v.WeekTime
+		fm.WeekStart = weekStart
+		fm.WeekEnd = weekEnd
+		fm.BaseDate = baseDate
+		fm.Status = status
+		fm.MonthlyPriceForecast = v.MonthlyPriceForecast
+		fm.WeeklyUpForecast = v.WeeklyUpForecast
+		fm.WeeklyDownForecast = v.WeeklyDownForecast
+		fm.ModifyTime = time.Now().Local()
+
+		// 提交时才校验必填项
+		if status == models.AssessmentFormStatusSubmitted {
+			if v.MonthlyPriceForecast == "" {
+				br.Msg = "请选择月度涨跌"
+				return
+			}
+			if v.WeeklyUpForecast == "" {
+				br.Msg = "请选择周度上行风险"
+				return
+			}
+			if v.WeeklyDownForecast == "" {
+				br.Msg = "请选择周度下行风险"
+				return
+			}
+			// 当周填报单据如果超过周日未提交则不可再提交,但是可以编辑
+			if time.Now().After(weekEnd) {
+				br.Msg = fmt.Sprintf("该单据已超过最晚提交时间: %s,不允许提交", weekEnd.Format(utils.FormatDateTime))
+				return
+			}
+			fm.SubmitTime = time.Now().Local()
+		}
+		if firstFormId > 0 {
+			updateForms = append(updateForms, fm)
+		} else {
+			fm.CreateTime = time.Now().Local()
+			insertForms = append(insertForms, fm)
+			continue
+		}
+	}
+
+	// 首行表单ID大于0为更新
+	if firstFormId > 0 && len(updateForms) > 0 {
+		updateCols := []string{formOb.Cols().MonthlyPriceForecast, formOb.Cols().WeeklyUpForecast, formOb.Cols().WeeklyDownForecast, formOb.Cols().ModifyTime}
+		if status == models.AssessmentFormStatusSubmitted {
+			updateCols = append(updateCols, formOb.Cols().Status, formOb.Cols().SubmitTime)
+		}
+		for _, v := range updateForms {
+			if e := v.Update(updateCols); e != nil {
+				br.Msg = "操作失败"
+				br.ErrMsg = fmt.Sprintf("更新填报单失败, %v", e)
+				return
+			}
+		}
+	}
+
+	// 新增
+	if firstFormId <= 0 && len(insertForms) > 0 {
+		if e := formOb.CreateMulti(insertForms); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("批量新增填报单失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Submit
+// @Title 提交填报单
+// @Description 提交填报单
+// @Param	request	body models.AssessmentFormSubmitReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /form/submit [post]
+func (this *AssessmentFormController) Submit() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentFormSubmitReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	req.FormCode = strings.TrimSpace(req.FormCode)
+	if req.FormCode == "" {
+		br.Msg = "参数有误"
+		br.ErrMsg = "参数有误, FormCode"
+		return
+	}
+
+	// 获取单号关联的填报单
+	formOb := new(models.AssessmentForm)
+	cond := fmt.Sprintf(` AND %s = ?`, formOb.Cols().FormCode)
+	pars := make([]interface{}, 0)
+	pars = append(pars, req.FormCode)
+	list, e := formOb.GetItemsByCondition(cond, pars, []string{}, "")
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取单号关联填报单失败, %v", e)
+		return
+	}
+	if len(list) == 0 {
+		br.Msg = "填报单有误"
+		br.ErrMsg = "单号无关联填报单"
+		return
+	}
+
+	// 提交校验
+	adminId := list[0].ResearcherAdminId
+	if sysUser.AdminId != adminId {
+		br.Msg = "无权操作"
+		return
+	}
+	status := list[0].Status
+	if status == models.AssessmentFormStatusSubmitted {
+		br.Msg = "该单据已提交,请勿重复提交"
+		return
+	}
+	weekEnd := list[0].WeekEnd
+	submitOk := !weekEnd.IsZero() && time.Now().Local().Before(weekEnd)
+	if !submitOk {
+		br.Msg = fmt.Sprintf("该单据已超过最晚提交时间: %s,不允许提交", weekEnd.Format(utils.FormatDateTime))
+		return
+	}
+	for _, v := range list {
+		if v.MonthlyPriceForecast == "" {
+			br.Msg = "请选择月度涨跌"
+			return
+		}
+		if v.WeeklyUpForecast == "" {
+			br.Msg = "请选择周度上行风险"
+			return
+		}
+		if v.WeeklyDownForecast == "" {
+			br.Msg = "请选择周度下行风险"
+			return
+		}
+	}
+
+	// 更新状态
+	updateCols := []string{formOb.Cols().Status, formOb.Cols().SubmitTime, formOb.Cols().ModifyTime}
+	for _, v := range list {
+		v.Status = models.AssessmentFormStatusSubmitted
+		v.SubmitTime = time.Now().Local()
+		v.ModifyTime = time.Now().Local()
+		if e = v.Update(updateCols); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("更新填报单状态失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Cancel
+// @Title 撤销填报单
+// @Description 撤销填报单
+// @Param	request	body models.AssessmentFormCancelReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /form/cancel [post]
+func (this *AssessmentFormController) Cancel() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentFormCancelReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	req.FormCode = strings.TrimSpace(req.FormCode)
+	if req.FormCode == "" {
+		br.Msg = "参数有误"
+		br.ErrMsg = "参数有误, FormCode"
+		return
+	}
+
+	// 获取单号关联的填报单
+	formOb := new(models.AssessmentForm)
+	cond := fmt.Sprintf(` AND %s = ?`, formOb.Cols().FormCode)
+	pars := make([]interface{}, 0)
+	pars = append(pars, req.FormCode)
+	list, e := formOb.GetItemsByCondition(cond, pars, []string{}, "")
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取单号关联填报单失败, %v", e)
+		return
+	}
+	if len(list) == 0 {
+		br.Msg = "填报单有误"
+		br.ErrMsg = "单号无关联填报单"
+		return
+	}
+
+	// 撤销校验
+	adminId := list[0].ResearcherAdminId
+	if sysUser.AdminId != adminId {
+		br.Msg = "无权操作"
+		return
+	}
+	status := list[0].Status
+	if status == models.AssessmentFormStatusDraft {
+		br.Msg = "单据状态有误,请刷新页面"
+		return
+	}
+	weekEnd := list[0].WeekEnd
+	cancelOk := !weekEnd.IsZero() && time.Now().Local().Before(weekEnd)
+	if !cancelOk {
+		br.Msg = fmt.Sprintf("该单据已超过最晚撤销时间: %s,不允许撤销", weekEnd.Format(utils.FormatDateTime))
+		return
+	}
+	// TODO:这里象屿会有一个外部条件:当周周报未提交,暂不处理视作true
+
+	// 更新状态
+	updateCols := []string{formOb.Cols().Status, formOb.Cols().SubmitTime, formOb.Cols().ModifyTime}
+	for _, v := range list {
+		v.Status = models.AssessmentFormStatusDraft
+		v.SubmitTime = time.Time{}
+		v.ModifyTime = time.Now().Local()
+		if e = v.Update(updateCols); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("更新填报单状态失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Remove
+// @Title 删除填报单
+// @Description 删除填报单
+// @Param	request	body models.AssessmentFormRemoveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /form/remove [post]
+func (this *AssessmentFormController) Remove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentFormRemoveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	req.FormCode = strings.TrimSpace(req.FormCode)
+	if req.FormCode == "" {
+		br.Msg = "参数有误"
+		br.ErrMsg = "参数有误, FormCode"
+		return
+	}
+
+	// 获取单号关联的填报单
+	formOb := new(models.AssessmentForm)
+	cond := fmt.Sprintf(` AND %s = ?`, formOb.Cols().FormCode)
+	pars := make([]interface{}, 0)
+	pars = append(pars, req.FormCode)
+	list, e := formOb.GetItemsByCondition(cond, pars, []string{}, "")
+	if e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取单号关联填报单失败, %v", e)
+		return
+	}
+	if len(list) == 0 {
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "操作成功"
+		return
+	}
+
+	// 撤销校验
+	adminId := list[0].ResearcherAdminId
+	if sysUser.AdminId != adminId {
+		br.Msg = "无权操作"
+		return
+	}
+	status := list[0].Status
+	if status != models.AssessmentFormStatusDraft {
+		br.Msg = "单据状态有误,请刷新页面"
+		return
+	}
+
+	// 删除填报单
+	cond = fmt.Sprintf(`%s = ?`, formOb.Cols().FormCode)
+	if e = formOb.RemoveByCondition(cond, pars); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("批量删除填报单失败, %v", e)
+		return
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// ResultStatistic
+// @Title 结果统计
+// @Description 结果统计
+// @Param	request	body models.AssessmentFormResultStatisticReq true "type json string"
+// @Success 200 string "获取成功"
+// @router /form/result_statistic [get]
+func (this *AssessmentFormController) ResultStatistic() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	params := new(models.AssessmentFormResultStatisticReq)
+	if e := this.ParseForm(params); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "参数解析失败, Err: " + e.Error()
+		return
+	}
+	params.AssessmentResearcherIds = strings.TrimSpace(params.AssessmentResearcherIds)
+	if params.AssessmentResearcherIds == "" {
+		br.Msg = "请选择研究员"
+		return
+	}
+	params.StartTime = strings.TrimSpace(params.StartTime)
+	params.EndTime = strings.TrimSpace(params.EndTime)
+	if params.StartTime == "" || params.EndTime == "" {
+		br.Msg = "请选择开始结束时间"
+		return
+	}
+	_, e := time.Parse(utils.FormatDate, params.StartTime)
+	if e != nil {
+		br.Msg = "开始时间格式有误"
+		return
+	}
+	endDate, e := time.Parse(utils.FormatDate, params.EndTime)
+	if e != nil {
+		br.Msg = "结束时间格式有误"
+		return
+	}
+	resp := make([]*models.AssessmentFormResultStatisticItem, 0)
+
+	var researcherIds []int
+	arr := strings.Split(params.AssessmentResearcherIds, ",")
+	for _, v := range arr {
+		i, _ := strconv.Atoi(v)
+		if i > 0 {
+			researcherIds = append(researcherIds, i)
+		}
+	}
+	if len(researcherIds) == 0 {
+		br.Msg = "研究员有误"
+		br.ErrMsg = fmt.Sprintf("选择的研究员IDs有误, %s", params.AssessmentResearcherIds)
+		return
+	}
+
+	// 获取研究员在[开始日期至结束日期]的填报单
+	forms := make([]*models.AssessmentForm, 0)
+	{
+		formOb := new(models.AssessmentForm)
+		cond := fmt.Sprintf(` AND %s = ? AND %s IN (?) AND (%s BETWEEN ? AND ?)`, formOb.Cols().Status, formOb.Cols().ResearcherId, formOb.Cols().BaseDate)
+		pars := make([]interface{}, 0)
+		pars = append(pars, models.AssessmentFormStatusSubmitted, researcherIds, params.StartTime, endDate.Format(utils.FormatDate))
+		list, e := formOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取填报单失败, %v", e)
+			return
+		}
+		forms = list
+	}
+	if len(forms) == 0 {
+		br.Data = resp
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "获取成功"
+		return
+	}
+
+	// 初始化响应结果,取出填报单关联的品种
+	var varietyIds []int
+	existVarietyId := make(map[int]bool)
+	existRespKey := make(map[string]bool)
+	for _, v := range forms {
+		if !existVarietyId[v.VarietyId] {
+			existVarietyId[v.VarietyId] = true
+			varietyIds = append(varietyIds, v.VarietyId)
+		}
+		k := fmt.Sprintf("%d-%d", v.ResearcherId, v.VarietyId)
+		if existRespKey[k] {
+			continue
+		}
+		existRespKey[k] = true
+		t := new(models.AssessmentFormResultStatisticItem)
+		t.ResearcherId = v.ResearcherId
+		t.ResearcherAdminId = v.ResearcherAdminId
+		t.ResearcherName = v.ResearcherName
+		t.VarietyId = v.VarietyId
+		t.VarietyName = v.VarietyName
+		t.VarietyCode = v.VarietyCode
+		resp = append(resp, t)
+	}
+
+	// 查询[开始日期至结束日期+4周]的品种数据
+	endMonthDate := endDate.AddDate(0, 0, 28).Format(utils.FormatDate)
+	varietyData := make([]*models.AssessmentVarietyData, 0)
+	{
+		dataOb := new(models.AssessmentVarietyData)
+		cond := fmt.Sprintf(` AND %s IN (?) AND (%s BETWEEN ? AND ?)`, dataOb.Cols().VarietyId, dataOb.Cols().WeekDate)
+		pars := make([]interface{}, 0)
+		pars = append(pars, varietyIds, params.StartTime, endMonthDate)
+		list, e := dataOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取品种数据失败, %v", e)
+			return
+		}
+		varietyData = list
+	}
+
+	// 计算正确率
+	resp, e = services.CalculateResultStatistic(forms, varietyData, resp)
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}

+ 672 - 0
controllers/assessment/assessment_researcher.go

@@ -0,0 +1,672 @@
+package assessment
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/models/system"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// AssessmentResearcherController 考核研究员
+type AssessmentResearcherController struct {
+	controllers.BaseAuthController
+}
+
+// Add
+// @Title 新增研究员
+// @Description 新增研究员
+// @Param	request	body models.AssessmentResearcherAddReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /researcher/add [post]
+func (this *AssessmentResearcherController) Add() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentResearcherAddReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	if req.AdminId <= 0 {
+		br.Msg = "请选择研究员"
+		return
+	}
+	disabledResearcher := new(models.AssessmentResearcher)
+	researcherOb := new(models.AssessmentResearcher)
+	mappingOb := new(models.AssessmentResearcherAdminMapping)
+
+	// 去重校验:系统用户ID
+	{
+		cond := fmt.Sprintf(` AND %s = ?`, researcherOb.Cols().AdminId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, req.AdminId)
+		exists, e := researcherOb.GetItemByCondition(cond, pars, "")
+		if e != nil && !utils.IsErrNoRow(e) {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+			return
+		}
+		// PS.由于删除研究员需要保留历史填报数据,如果研究员被删除(即禁用)那么重新启用即可
+		if exists != nil && exists.AssessmentResearcherId > 0 {
+			if exists.Enabled == models.AssessmentResearcherEnabled {
+				br.Msg = "该研究员已添加,请勿重复添加"
+				return
+			}
+			disabledResearcher = exists
+		}
+	}
+
+	sysAdmin, e := system.GetSysAdminById(req.AdminId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Msg = "该用户不存在,请重新选择"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取系统用户失败, %v", e)
+		return
+	}
+	if sysAdmin == nil || sysAdmin.AdminId <= 0 {
+		br.Msg = "该用户不存在,请重新选择"
+		return
+	}
+	if sysAdmin.Enabled != 1 {
+		br.Msg = "该用户被禁用,请重新选择"
+		return
+	}
+
+	var researcherId int
+	if disabledResearcher != nil && disabledResearcher.AssessmentResearcherId > 0 {
+		disabledResearcher.RealName = sysAdmin.RealName
+		disabledResearcher.Enabled = models.AssessmentResearcherEnabled
+		disabledResearcher.ModifyTime = time.Now().Local()
+		updateCols := []string{researcherOb.Cols().RealName, researcherOb.Cols().Enabled, researcherOb.Cols().ModifyTime}
+		if e := disabledResearcher.Update(updateCols); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("重新启用研究员失败, %v", e)
+			return
+		}
+		researcherId = disabledResearcher.AssessmentResearcherId
+
+		// 清除原有关联
+		cond := fmt.Sprintf(`%s = ?`, mappingOb.Cols().AssessmentResearcherId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcherId)
+		if e = mappingOb.RemoveByCondition(cond, pars); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("清空权限失败, %v", e)
+			return
+		}
+	} else {
+		newResearcher := new(models.AssessmentResearcher)
+		newResearcher.AdminId = req.AdminId
+		newResearcher.RealName = sysAdmin.RealName
+		newResearcher.Enabled = models.AssessmentResearcherEnabled
+		newResearcher.CreateTime = time.Now().Local()
+		newResearcher.ModifyTime = time.Now().Local()
+		if e = newResearcher.Create(); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("新增研究员失败, %v", e)
+			return
+		}
+		researcherId = newResearcher.AssessmentResearcherId
+	}
+
+	// 查看/统计权限
+	mappings := make([]*models.AssessmentResearcherAdminMapping, 0)
+	if len(req.ViewAdminIds) > 0 {
+		for _, v := range req.ViewAdminIds {
+			m := new(models.AssessmentResearcherAdminMapping)
+			m.AssessmentResearcherId = researcherId
+			m.AdminId = req.AdminId
+			m.AuthAdminId = v
+			m.AuthType = models.AssessmentResearcherAdminAuthTypeView
+			mappings = append(mappings, m)
+		}
+	}
+	if len(req.AssessmentAdminIds) > 0 {
+		for _, v := range req.AssessmentAdminIds {
+			m := new(models.AssessmentResearcherAdminMapping)
+			m.AssessmentResearcherId = researcherId
+			m.AdminId = req.AdminId
+			m.AuthAdminId = v
+			m.AuthType = models.AssessmentResearcherAdminAuthTypeStatistics
+			mappings = append(mappings, m)
+		}
+	}
+	if len(mappings) > 0 {
+		if e = mappingOb.CreateMulti(mappings); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("批量新增权限失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Edit
+// @Title 编辑研究员
+// @Description 编辑研究员
+// @Param	request	body models.AssessmentResearcherEditReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /researcher/edit [post]
+func (this *AssessmentResearcherController) Edit() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentResearcherEditReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	if req.AssessmentResearcherId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, AssessmentResearcherId: %d", req.AssessmentResearcherId)
+		return
+	}
+
+	researcherOb := new(models.AssessmentResearcher)
+	researcher, e := researcherOb.GetItemById(req.AssessmentResearcherId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Msg = "研究员不存在,请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+		return
+	}
+	if researcher.Enabled == models.AssessmentResearcherDisabled {
+		br.Msg = "研究员不存在,请刷新页面"
+		return
+	}
+
+	// 查看/统计权限
+	mappings := make([]*models.AssessmentResearcherAdminMapping, 0)
+	if len(req.ViewAdminIds) > 0 {
+		for _, v := range req.ViewAdminIds {
+			m := new(models.AssessmentResearcherAdminMapping)
+			m.AssessmentResearcherId = researcher.AssessmentResearcherId
+			m.AdminId = researcher.AdminId
+			m.AuthAdminId = v
+			m.AuthType = models.AssessmentResearcherAdminAuthTypeView
+			mappings = append(mappings, m)
+		}
+	}
+	if len(req.AssessmentAdminIds) > 0 {
+		for _, v := range req.AssessmentAdminIds {
+			m := new(models.AssessmentResearcherAdminMapping)
+			m.AssessmentResearcherId = researcher.AssessmentResearcherId
+			m.AdminId = researcher.AdminId
+			m.AuthAdminId = v
+			m.AuthType = models.AssessmentResearcherAdminAuthTypeStatistics
+			mappings = append(mappings, m)
+		}
+	}
+	// 清空关联后新增
+	mappingOb := new(models.AssessmentResearcherAdminMapping)
+	{
+		cond := fmt.Sprintf(`%s = ?`, mappingOb.Cols().AssessmentResearcherId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcher.AssessmentResearcherId)
+		if e = mappingOb.RemoveByCondition(cond, pars); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("清空权限失败, %v", e)
+			return
+		}
+	}
+	if len(mappings) > 0 {
+		if e = mappingOb.CreateMulti(mappings); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("批量新增权限失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Remove
+// @Title 删除研究员
+// @Description 删除研究员
+// @Param	request	body models.AssessmentResearcherRemoveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /researcher/remove [post]
+func (this *AssessmentResearcherController) Remove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentResearcherRemoveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	if req.AssessmentResearcherId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, AssessmentResearcherId: %d", req.AssessmentResearcherId)
+		return
+	}
+
+	researcherOb := new(models.AssessmentResearcher)
+	researcher, e := researcherOb.GetItemById(req.AssessmentResearcherId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "操作成功"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+		return
+	}
+	if researcher.Enabled == models.AssessmentResearcherDisabled {
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "操作成功"
+		return
+	}
+
+	// 由于要保留历史填报数据,这里实际为禁用操作
+	updateCols := []string{researcherOb.Cols().Enabled, researcherOb.Cols().ModifyTime}
+	researcher.Enabled = models.AssessmentResearcherDisabled
+	researcher.ModifyTime = time.Now()
+	if e = researcher.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("禁用研究员失败, %v", e)
+		return
+	}
+
+	// 关联也不需要清理
+	//adminMappingOb := new(models.AssessmentResearcherAdminMapping)
+	//{
+	//	cond := fmt.Sprintf(`%s = ?`, adminMappingOb.Cols().AdminId)
+	//	pars := make([]interface{}, 0)
+	//	pars = append(pars, researcher.AdminId)
+	//	if e = adminMappingOb.RemoveByCondition(cond, pars); e != nil {
+	//		br.Msg = "操作失败"
+	//		br.ErrMsg = fmt.Sprintf("清空权限失败, %v", e)
+	//		return
+	//	}
+	//}
+	//varietyMappingOb := new(models.AssessmentResearcherVarietyMapping)
+	//{
+	//	cond := fmt.Sprintf(`%s = ?`, varietyMappingOb.Cols().AdminId)
+	//	pars := make([]interface{}, 0)
+	//	pars = append(pars, researcher.AdminId)
+	//	if e = varietyMappingOb.RemoveByCondition(cond, pars); e != nil {
+	//		br.Msg = "操作失败"
+	//		br.ErrMsg = fmt.Sprintf("清空品种关联失败, %v", e)
+	//		return
+	//	}
+	//}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Detail
+// @Title 研究员详情
+// @Description 研究员详情
+// @Param   AssessmentResearcherId  query  int  true  "研究员ID"
+// @Success 200 string "操作成功"
+// @router /researcher/detail [get]
+func (this *AssessmentResearcherController) Detail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	researcherId, _ := this.GetInt("AssessmentResearcherId")
+	if researcherId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, AssessmentResearcherId: %d", researcherId)
+		return
+	}
+
+	researcherOb := new(models.AssessmentResearcher)
+	researcher, e := researcherOb.GetItemById(researcherId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Msg = "研究员不存在,请重新页面"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+		return
+	}
+	if researcher.Enabled == models.AssessmentResearcherDisabled {
+		br.Msg = "研究员不存在,请刷新页面"
+		return
+	}
+	resp := researcher.Format2Detail()
+
+	// 获取系统用户
+	adminMapping := make(map[int]*system.Admin)
+	{
+		admins, e := system.GetSysAdminList(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取系统用户失败, %v", e)
+			return
+		}
+		for _, v := range admins {
+			adminMapping[v.AdminId] = v
+		}
+	}
+
+	mappings := make([]*models.AssessmentResearcherAdminMapping, 0)
+	{
+		mappingOb := new(models.AssessmentResearcherAdminMapping)
+		cond := fmt.Sprintf(` AND %s = ?`, mappingOb.Cols().AdminId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, researcher.AdminId)
+		list, e := mappingOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员用户权限关系失败, %v", e)
+			return
+		}
+		mappings = list
+	}
+	for _, m := range mappings {
+		ad := adminMapping[m.AuthAdminId]
+		if ad == nil {
+			continue
+		}
+		// 详情这里只显示启用的系统用户
+		if ad.Enabled != 1 {
+			continue
+		}
+		ar := models.AssessmentResearcherDetail{
+			AdminId:  m.AuthAdminId,
+			RealName: ad.RealName,
+			Enabled:  ad.Enabled,
+		}
+		if m.AuthType == models.AssessmentResearcherAdminAuthTypeView {
+			resp.ViewAdmins = append(resp.ViewAdmins, ar)
+			continue
+		}
+		resp.AssessmentAdmins = append(resp.AssessmentAdmins, ar)
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// PageList
+// @Title 研究员列表-分页
+// @Description 研究员列表-分页
+// @Param	request	body models.AssessmentResearcherPageListReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /researcher/page_list [get]
+func (this *AssessmentResearcherController) PageList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	params := new(models.AssessmentResearcherPageListReq)
+	if e := this.ParseForm(params); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "参数解析失败, Err: " + e.Error()
+		return
+	}
+	params.RealName = strings.TrimSpace(params.RealName)
+	resp := new(models.AssessmentResearcherPageListResp)
+	resp.List = make([]*models.AssessmentResearcherDetail, 0)
+
+	// 分页
+	var startSize int
+	if params.PageSize <= 0 {
+		params.PageSize = utils.PageSize20
+	}
+	if params.CurrentIndex <= 0 {
+		params.CurrentIndex = 1
+	}
+	startSize = utils.StartIndex(params.CurrentIndex, params.PageSize)
+
+	// 筛选条件
+	researcherOb := new(models.AssessmentResearcher)
+	cond := ``
+	pars := make([]interface{}, 0)
+	{
+		// 研究员姓名
+		if params.RealName != "" {
+			kw := fmt.Sprint("%", params.RealName, "%")
+			cond += fmt.Sprintf(` AND %s LIKE ?`, researcherOb.Cols().RealName)
+			pars = append(pars, kw)
+		}
+
+		// 研究员IDs
+		if params.AssessmentResearcherIds != "" {
+			var researcherIds []int
+			arr := strings.Split(params.AssessmentResearcherIds, ",")
+			for _, v := range arr {
+				i, _ := strconv.Atoi(v)
+				if i > 0 {
+					researcherIds = append(researcherIds, i)
+				}
+			}
+			if len(researcherIds) == 0 {
+				resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+				br.Data = resp
+				br.Ret = 200
+				br.Success = true
+				br.Msg = "获取成功"
+				return
+			}
+			cond += fmt.Sprintf(` AND %s IN (?)`, researcherOb.Cols().PrimaryId)
+			pars = append(pars, researcherIds)
+		}
+
+		// 仅获取自己有权限的,与研究员IDs条件冲突
+		if params.AssessmentResearcherIds == "" && params.ResearcherAuthType > 0 {
+			// 自己如果是研究员的话要把自己也加进去
+			var researcherIds []int
+			condResearcher := fmt.Sprintf(` AND %s = ?`, researcherOb.Cols().AdminId)
+			parsResearcher := make([]interface{}, 0)
+			parsResearcher = append(parsResearcher, sysUser.AdminId)
+			researcher, e := researcherOb.GetItemByCondition(condResearcher, parsResearcher, "")
+			if e != nil && !utils.IsErrNoRow(e) {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+				return
+			}
+			if researcher != nil && researcher.AssessmentResearcherId > 0 {
+				researcherIds = append(researcherIds, researcher.AssessmentResearcherId)
+			}
+
+			mappingOb := new(models.AssessmentResearcherAdminMapping)
+			condMapping := fmt.Sprintf(` AND %s = ? AND %s = ?`, mappingOb.Cols().AuthAdminId, mappingOb.Cols().AuthType)
+			parsMapping := make([]interface{}, 0)
+			parsMapping = append(parsMapping, sysUser.AdminId, params.ResearcherAuthType)
+			list, e := mappingOb.GetItemsByCondition(condMapping, parsMapping, []string{}, "")
+			if e != nil {
+				br.Msg = "获取失败"
+				br.ErrMsg = fmt.Sprintf("获取用户有权限的研究员失败, %v", e)
+				return
+			}
+			for _, v := range list {
+				researcherIds = append(researcherIds, v.AssessmentResearcherId)
+			}
+			if len(researcherIds) == 0 {
+				resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+				br.Data = resp
+				br.Ret = 200
+				br.Success = true
+				br.Msg = "获取成功"
+				return
+			}
+			cond += fmt.Sprintf(` AND %s IN (?)`, researcherOb.Cols().PrimaryId)
+			pars = append(pars, researcherIds)
+		}
+
+		// 仅显示启用的
+		if params.OnlyEnabled {
+			cond += fmt.Sprintf(` AND %s = ?`, researcherOb.Cols().Enabled)
+			pars = append(pars, models.AssessmentResearcherEnabled)
+		}
+	}
+
+	// 分页列表
+	total, e := researcherOb.GetCountByCondition(cond, pars)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员列表总数失败, %v", e)
+		return
+	}
+	researchers, e := researcherOb.GetPageItemsByCondition(cond, pars, []string{}, "", startSize, params.PageSize)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+		return
+	}
+	var researcherIds []int
+	for _, v := range researchers {
+		researcherIds = append(researcherIds, v.AssessmentResearcherId)
+	}
+	if total == 0 || len(researcherIds) == 0 {
+		resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+		br.Data = resp
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "获取成功"
+		return
+	}
+
+	// 获取系统用户
+	adminMapping := make(map[int]*system.Admin)
+	{
+		admins, e := system.GetSysAdminList(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取系统用户失败, %v", e)
+			return
+		}
+		for _, v := range admins {
+			adminMapping[v.AdminId] = v
+		}
+	}
+
+	// 研究员关联权限
+	viewAdminMappings, assessmentAdminMappings := make(map[int][]models.AssessmentResearcherDetail), make(map[int][]models.AssessmentResearcherDetail)
+	{
+		mappingOb := new(models.AssessmentResearcherAdminMapping)
+		condMapping := fmt.Sprintf(` AND %s IN (?)`, mappingOb.Cols().AssessmentResearcherId)
+		parsMapping := make([]interface{}, 0)
+		parsMapping = append(parsMapping, researcherIds)
+		list, e := mappingOb.GetItemsByCondition(condMapping, parsMapping, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员用户权限关系失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			ad := adminMapping[v.AuthAdminId]
+			if ad == nil {
+				continue
+			}
+			// 详情这里只显示启用的系统用户
+			if ad.Enabled != 1 {
+				continue
+			}
+			ar := models.AssessmentResearcherDetail{
+				AdminId:  v.AuthAdminId,
+				RealName: ad.RealName,
+				Enabled:  ad.Enabled,
+			}
+			if v.AuthType == models.AssessmentResearcherAdminAuthTypeView {
+				viewAdminMappings[v.AdminId] = append(viewAdminMappings[v.AdminId], ar)
+				continue
+			}
+			assessmentAdminMappings[v.AdminId] = append(assessmentAdminMappings[v.AdminId], ar)
+		}
+	}
+
+	for _, v := range researchers {
+		t := v.Format2Detail()
+		t.ViewAdmins = viewAdminMappings[v.AdminId]
+		t.AssessmentAdmins = assessmentAdminMappings[v.AdminId]
+		resp.List = append(resp.List, t)
+	}
+
+	resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, total)
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}

+ 881 - 0
controllers/assessment/assessment_variety.go

@@ -0,0 +1,881 @@
+package assessment
+
+import (
+	"encoding/json"
+	"eta/eta_api/controllers"
+	"eta/eta_api/models"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"github.com/tealeg/xlsx"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// AssessmentVarietyController 考核品种
+type AssessmentVarietyController struct {
+	controllers.BaseAuthController
+}
+
+// Add
+// @Title 新增品种
+// @Description 新增品种
+// @Param	request	body models.AssessmentVarietyAddReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /variety/add [post]
+func (this *AssessmentVarietyController) Add() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentVarietyAddReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	req.VarietyCode = strings.TrimSpace(req.VarietyCode)
+	if req.VarietyCode == "" {
+		br.Msg = "请输入品种编码"
+		return
+	}
+	req.VarietyName = strings.TrimSpace(req.VarietyName)
+	if req.VarietyName == "" {
+		br.Msg = "请输入品种名称"
+		return
+	}
+	if req.MonthlyFluctuate <= 0 {
+		br.Msg = "请输入月度波动阈值"
+		return
+	}
+	if req.WeeklyFluctuate <= 0 {
+		br.Msg = "请输入周度波动阈值"
+		return
+	}
+	disabledVariety := new(models.AssessmentVariety)
+	varietyOb := new(models.AssessmentVariety)
+	mappingOb := new(models.AssessmentResearcherVarietyMapping)
+
+	// 去重校验:编码唯一
+	{
+		cond := fmt.Sprintf(` AND %s = ?`, varietyOb.Cols().VarietyCode)
+		pars := make([]interface{}, 0)
+		pars = append(pars, req.VarietyCode)
+		exists, e := varietyOb.GetItemByCondition(cond, pars, "")
+		if e != nil && !utils.IsErrNoRow(e) {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("获取品种失败, %v", e)
+			return
+		}
+		// PS.由于删除品种需要保留历史填报数据,如果品种被删除(即禁用)那么重新启用即可
+		if exists != nil && exists.AssessmentVarietyId > 0 {
+			if exists.Enabled == models.AssessmentVarietyEnabled {
+				br.Msg = "品种编码已存在,请勿重复添加"
+				return
+			}
+			disabledVariety = exists
+		}
+	}
+
+	var varietyId int
+	if disabledVariety != nil && disabledVariety.AssessmentVarietyId > 0 {
+		disabledVariety.VarietyName = req.VarietyName
+		disabledVariety.MonthlyFluctuate = req.MonthlyFluctuate
+		disabledVariety.WeeklyFluctuate = req.WeeklyFluctuate
+		disabledVariety.Enabled = models.AssessmentVarietyEnabled
+		disabledVariety.ModifyTime = time.Now().Local()
+		updateCols := []string{varietyOb.Cols().VarietyName, varietyOb.Cols().MonthlyFluctuate, varietyOb.Cols().WeeklyFluctuate, varietyOb.Cols().Enabled, varietyOb.Cols().ModifyTime}
+		if e := disabledVariety.Update(updateCols); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("重新启用品种失败, %v", e)
+			return
+		}
+		varietyId = disabledVariety.AssessmentVarietyId
+
+		// 清除原有关联
+		cond := fmt.Sprintf(`%s = ?`, mappingOb.Cols().VarietyId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, varietyId)
+		if e := mappingOb.RemoveByCondition(cond, pars); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("清空品种原关联失败, %v", e)
+			return
+		}
+	} else {
+		newVariety := new(models.AssessmentVariety)
+		newVariety.VarietyCode = req.VarietyCode
+		newVariety.VarietyName = req.VarietyName
+		newVariety.MonthlyFluctuate = req.MonthlyFluctuate
+		newVariety.WeeklyFluctuate = req.WeeklyFluctuate
+		newVariety.Enabled = models.AssessmentVarietyEnabled
+		newVariety.CreateTime = time.Now().Local()
+		newVariety.ModifyTime = time.Now().Local()
+		if e := newVariety.Create(); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("新增品种失败, %v", e)
+			return
+		}
+		varietyId = newVariety.AssessmentVarietyId
+	}
+
+	// 关联研究员
+	mappings := make([]*models.AssessmentResearcherVarietyMapping, 0)
+	if len(req.AssessmentResearcherIds) > 0 {
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s IN (?)`, researcherOb.Cols().PrimaryId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, req.AssessmentResearcherIds)
+		researchers, e := researcherOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+			return
+		}
+		for _, v := range researchers {
+			m := new(models.AssessmentResearcherVarietyMapping)
+			m.VarietyId = varietyId
+			m.AssessmentResearcherId = v.AssessmentResearcherId
+			m.AdminId = v.AdminId
+			mappings = append(mappings, m)
+		}
+	}
+	if len(mappings) > 0 {
+		if e := mappingOb.CreateMulti(mappings); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("批量新增研究员品种关联失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Edit
+// @Title 编辑品种
+// @Description 编辑品种
+// @Param	request	body models.AssessmentVarietyEditReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /variety/edit [post]
+func (this *AssessmentVarietyController) Edit() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentVarietyEditReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	if req.AssessmentVarietyId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, AssessmentVarietyId: %d", req.AssessmentVarietyId)
+		return
+	}
+	if req.MonthlyFluctuate <= 0 {
+		br.Msg = "请输入月度波动阈值"
+		return
+	}
+	if req.WeeklyFluctuate <= 0 {
+		br.Msg = "请输入周度波动阈值"
+		return
+	}
+
+	varietyOb := new(models.AssessmentVariety)
+	variety, e := varietyOb.GetItemById(req.AssessmentVarietyId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Msg = "品种不存在,请刷新页面"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取品种失败, %v", e)
+		return
+	}
+	if variety.Enabled == models.AssessmentVarietyDisabled {
+		br.Msg = "品种不存在,请刷新页面"
+		return
+	}
+
+	variety.MonthlyFluctuate = req.MonthlyFluctuate
+	variety.WeeklyFluctuate = req.WeeklyFluctuate
+	variety.ModifyTime = time.Now().Local()
+	updateCols := []string{varietyOb.Cols().MonthlyFluctuate, varietyOb.Cols().WeeklyFluctuate, varietyOb.Cols().ModifyTime}
+	if e := variety.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("更新品种失败, %v", e)
+		return
+	}
+
+	// 关联研究员,清空后新增
+	mappingOb := new(models.AssessmentResearcherVarietyMapping)
+	{
+		cond := fmt.Sprintf(`%s = ?`, mappingOb.Cols().VarietyId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, variety.AssessmentVarietyId)
+		if e = mappingOb.RemoveByCondition(cond, pars); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("清空关联失败, %v", e)
+			return
+		}
+	}
+	mappings := make([]*models.AssessmentResearcherVarietyMapping, 0)
+	if len(req.AssessmentResearcherIds) > 0 {
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s IN (?)`, researcherOb.Cols().PrimaryId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, req.AssessmentResearcherIds)
+		researchers, e := researcherOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+			return
+		}
+		for _, v := range researchers {
+			m := new(models.AssessmentResearcherVarietyMapping)
+			m.VarietyId = variety.AssessmentVarietyId
+			m.AssessmentResearcherId = v.AssessmentResearcherId
+			m.AdminId = v.AdminId
+			mappings = append(mappings, m)
+		}
+	}
+	if len(mappings) > 0 {
+		if e := mappingOb.CreateMulti(mappings); e != nil {
+			br.Msg = "操作失败"
+			br.ErrMsg = fmt.Sprintf("批量新增研究员品种关联失败, %v", e)
+			return
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Remove
+// @Title 删除品种
+// @Description 删除品种
+// @Param	request	body models.AssessmentVarietyRemoveReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /variety/remove [post]
+func (this *AssessmentVarietyController) Remove() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	var req models.AssessmentVarietyRemoveReq
+	if e := json.Unmarshal(this.Ctx.Input.RequestBody, &req); e != nil {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数解析失败, %v", e)
+		return
+	}
+	if req.AssessmentVarietyId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, AssessmentVarietyId: %d", req.AssessmentVarietyId)
+		return
+	}
+
+	varietyOb := new(models.AssessmentVariety)
+	variety, e := varietyOb.GetItemById(req.AssessmentVarietyId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Ret = 200
+			br.Success = true
+			br.Msg = "操作成功"
+			return
+		}
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("获取品种失败, %v", e)
+		return
+	}
+	if variety.Enabled == models.AssessmentVarietyDisabled {
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "操作成功"
+		return
+	}
+
+	// 由于要保留历史填报数据,这里实际为禁用操作
+	updateCols := []string{varietyOb.Cols().Enabled, varietyOb.Cols().ModifyTime}
+	variety.Enabled = models.AssessmentVarietyDisabled
+	variety.ModifyTime = time.Now()
+	if e = variety.Update(updateCols); e != nil {
+		br.Msg = "操作失败"
+		br.ErrMsg = fmt.Sprintf("禁用品种失败, %v", e)
+		return
+	}
+
+	// 关联也不需要清理
+	//mappingOb := new(models.AssessmentResearcherVarietyMapping)
+	//{
+	//	cond := fmt.Sprintf(`%s = ?`, mappingOb.Cols().VarietyId)
+	//	pars := make([]interface{}, 0)
+	//	pars = append(pars, variety.AssessmentVarietyId)
+	//	if e = mappingOb.RemoveByCondition(cond, pars); e != nil {
+	//		br.Msg = "操作失败"
+	//		br.ErrMsg = fmt.Sprintf("清空关联失败, %v", e)
+	//		return
+	//	}
+	//}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}
+
+// Detail
+// @Title 品种详情
+// @Description 品种详情
+// @Param   AssessmentVarietyId  query  int  true  "品种ID"
+// @Success 200 string "操作成功"
+// @router /variety/detail [get]
+func (this *AssessmentVarietyController) Detail() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	varietyId, _ := this.GetInt("AssessmentVarietyId")
+	if varietyId <= 0 {
+		br.Msg = "参数有误"
+		br.ErrMsg = fmt.Sprintf("参数有误, VarietyId: %d", varietyId)
+		return
+	}
+
+	varietyOb := new(models.AssessmentVariety)
+	variety, e := varietyOb.GetItemById(varietyId)
+	if e != nil {
+		if utils.IsErrNoRow(e) {
+			br.Msg = "品种不存在,请重新页面"
+			return
+		}
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取品种失败, %v", e)
+		return
+	}
+	if variety.Enabled == models.AssessmentVarietyDisabled {
+		br.Msg = "品种不存在,请刷新页面"
+		return
+	}
+	resp := variety.Format2Detail()
+
+	// 获取启用研究员
+	researcherMapping := make(map[int]*models.AssessmentResearcher)
+	{
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s = ?`, researcherOb.Cols().Enabled)
+		pars := make([]interface{}, 0)
+		pars = append(pars, models.AssessmentResearcherEnabled)
+		researchers, e := researcherOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+			return
+		}
+		for _, v := range researchers {
+			researcherMapping[v.AssessmentResearcherId] = v
+		}
+	}
+
+	// 获取研究员关联
+	mappings := make([]*models.AssessmentResearcherVarietyMapping, 0)
+	{
+		mappingOb := new(models.AssessmentResearcherVarietyMapping)
+		cond := fmt.Sprintf(` AND %s = ?`, mappingOb.Cols().VarietyId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, variety.AssessmentVarietyId)
+		mappings, e = mappingOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员品种关联失败, %v", e)
+			return
+		}
+	}
+	for _, v := range mappings {
+		rs := researcherMapping[v.AssessmentResearcherId]
+		if rs == nil {
+			continue
+		}
+		ar := models.AssessmentResearcherDetail{
+			AssessmentResearcherId: rs.AssessmentResearcherId,
+			AdminId:                v.AdminId,
+			RealName:               rs.RealName,
+			Enabled:                rs.Enabled,
+		}
+		resp.RelationResearcher = append(resp.RelationResearcher, ar)
+	}
+
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// PageList
+// @Title 品种列表-分页
+// @Description 品种列表-分页
+// @Param	request	body models.AssessmentVarietyPageListReq true "type json string"
+// @Success 200 string "操作成功"
+// @router /variety/page_list [get]
+func (this *AssessmentVarietyController) PageList() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+	params := new(models.AssessmentVarietyPageListReq)
+	if e := this.ParseForm(params); e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = "参数解析失败, Err: " + e.Error()
+		return
+	}
+	resp := new(models.AssessmentVarietyPageListResp)
+	resp.List = make([]*models.AssessmentVarietyDetail, 0)
+
+	// 分页
+	var startSize int
+	if params.PageSize <= 0 {
+		params.PageSize = utils.PageSize20
+	}
+	if params.CurrentIndex <= 0 {
+		params.CurrentIndex = 1
+	}
+	startSize = utils.StartIndex(params.CurrentIndex, params.PageSize)
+
+	// 筛选项
+	varietyOb := new(models.AssessmentVariety)
+	condVariety := ``
+	parsVariety := make([]interface{}, 0)
+	{
+		if params.AssessmentVarietyIds != "" {
+			var varietyIds []int
+			arr := strings.Split(params.AssessmentVarietyIds, ",")
+			for _, v := range arr {
+				i, _ := strconv.Atoi(v)
+				if i > 0 {
+					varietyIds = append(varietyIds, i)
+				}
+			}
+			if len(varietyIds) == 0 {
+				resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+				br.Data = resp
+				br.Ret = 200
+				br.Success = true
+				br.Msg = "获取成功"
+				return
+			}
+			condVariety += fmt.Sprintf(" AND %s IN (?)", varietyOb.Cols().PrimaryId)
+			parsVariety = append(parsVariety, varietyIds)
+		}
+
+		// 仅显示启用的
+		if params.OnlyEnabled {
+			condVariety += fmt.Sprintf(` AND %s = ?`, varietyOb.Cols().Enabled)
+			parsVariety = append(parsVariety, models.AssessmentVarietyEnabled)
+		}
+	}
+
+	// 列表
+	total, e := varietyOb.GetCountByCondition(condVariety, parsVariety)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员列表总数失败, %v", e)
+		return
+	}
+	varieties, e := varietyOb.GetPageItemsByCondition(condVariety, parsVariety, []string{}, "", startSize, params.PageSize)
+	if e != nil {
+		br.Msg = "获取失败"
+		br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+		return
+	}
+	var varietyIds []int
+	for _, v := range varieties {
+		varietyIds = append(varietyIds, v.AssessmentVarietyId)
+	}
+	if total == 0 || len(varietyIds) == 0 {
+		resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, 0)
+		br.Data = resp
+		br.Ret = 200
+		br.Success = true
+		br.Msg = "获取成功"
+		return
+	}
+
+	// 获取启用研究员
+	researcherMapping := make(map[int]*models.AssessmentResearcher)
+	{
+		researcherOb := new(models.AssessmentResearcher)
+		cond := fmt.Sprintf(` AND %s = ?`, researcherOb.Cols().Enabled)
+		pars := make([]interface{}, 0)
+		pars = append(pars, models.AssessmentResearcherEnabled)
+		researchers, e := researcherOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员失败, %v", e)
+			return
+		}
+		for _, v := range researchers {
+			researcherMapping[v.AssessmentResearcherId] = v
+		}
+	}
+
+	// 关联研究员
+	varietyResearcherMapping := make(map[int][]models.AssessmentResearcherDetail)
+	{
+		mappingOb := new(models.AssessmentResearcherVarietyMapping)
+		cond := fmt.Sprintf(` AND %s IN (?)`, mappingOb.Cols().VarietyId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, varietyIds)
+		list, e := mappingOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "获取失败"
+			br.ErrMsg = fmt.Sprintf("获取研究员品种关联失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			rs := researcherMapping[v.AssessmentResearcherId]
+			if rs == nil {
+				continue
+			}
+			ar := models.AssessmentResearcherDetail{
+				AssessmentResearcherId: v.AssessmentResearcherId,
+				AdminId:                v.AdminId,
+				RealName:               rs.RealName,
+			}
+			varietyResearcherMapping[v.VarietyId] = append(varietyResearcherMapping[v.VarietyId], ar)
+		}
+	}
+
+	for _, v := range varieties {
+		t := v.Format2Detail()
+		t.RelationResearcher = varietyResearcherMapping[v.AssessmentVarietyId]
+		resp.List = append(resp.List, t)
+	}
+
+	resp.Paging = paging.GetPaging(params.CurrentIndex, params.PageSize, total)
+	br.Data = resp
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "获取成功"
+}
+
+// ImportData
+// @Title 导入品种数据
+// @Description 导入品种数据
+// @Success 200 string "操作成功"
+// @router /variety/import_data [post]
+func (this *AssessmentVarietyController) ImportData() {
+	br := new(models.BaseResponse).Init()
+	defer func() {
+		if br.ErrMsg == "" {
+			br.IsSendEmail = false
+		}
+		this.Data["json"] = br
+		this.ServeJSON()
+	}()
+	sysUser := this.SysUser
+	if sysUser == nil {
+		br.Msg = "请登录"
+		br.ErrMsg = "请登录,SysUser Is Empty"
+		br.Ret = 408
+		return
+	}
+
+	file, header, e := this.GetFile("VarietyData")
+	if e != nil {
+		br.Msg = "导入失败"
+		br.ErrMsg = fmt.Sprintf("获取文件失败, %v", e)
+		return
+	}
+
+	// 文件格式校验
+	ext := strings.ToLower(filepath.Ext(header.Filename))
+	if ext != ".xlsx" && ext != ".xls" {
+		br.Msg = "文件格式有误,请上传Excel文件"
+		_ = file.Close()
+		return
+	}
+
+	path := "./static/assessment_variety_" + time.Now().Format(utils.FormatDateTimeUnSpace) + ".xlsx"
+	defer func() {
+		_ = file.Close()
+		_ = os.Remove(path)
+	}()
+	if e = this.SaveToFile("VarietyData", path); e != nil {
+		br.Msg = "导入失败"
+		br.ErrMsg = fmt.Sprintf("保存文件失败, %v", e)
+		return
+	}
+	xlFile, e := xlsx.OpenFile(path)
+	if e != nil {
+		br.Msg = "导入失败"
+		br.ErrMsg = fmt.Sprintf("打开excel文件失败, %v", e)
+		return
+	}
+	if len(xlFile.Sheets) == 0 {
+		br.Msg = "无有效数据,请检查"
+		br.ErrMsg = "Sheet为空"
+		return
+	}
+	// PS.导入规则:品种管理中的每个品种都需要有对应的日期数据,少一个都不允许上传,允许一次传多周的数据
+
+	varieties, weekDates := make([]string, 0), make([]string, 0)
+	varietyNameMatch := make(map[string]*models.AssessmentVariety)
+	varietyDateExist := make(map[string]bool) // [品种-日期]用于校验品种是否和日期一一对应
+	{
+		varietyOb := new(models.AssessmentVariety)
+		cond := fmt.Sprintf(` AND %s = ?`, varietyOb.Cols().Enabled)
+		pars := make([]interface{}, 0)
+		pars = append(pars, models.AssessmentVarietyEnabled)
+		list, e := varietyOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			br.Msg = "导入失败"
+			br.ErrMsg = fmt.Sprintf("获取品种列表失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			varieties = append(varieties, v.VarietyName)
+			varietyNameMatch[v.VarietyName] = v
+		}
+	}
+	rowData := make(map[int]*models.AssessmentVarietyData)
+
+	titleArr := []string{"品种名称", "当周收盘日期", "当周收盘价", "当周最高价", "当周最低价"}
+	for _, sheet := range xlFile.Sheets {
+		maxRow := sheet.MaxRow
+		colMax := len(titleArr)
+		for i := 0; i < maxRow; i++ {
+			// 检查首行表头
+			row := sheet.Row(i)
+			cells := row.Cells
+			cellLen := len(cells)
+			if i == 0 {
+				for j := 0; j < colMax; j++ {
+					if cellLen <= j {
+						br.Msg = fmt.Sprintf("第%d列应为%s,请检查表头", j+1, titleArr[j])
+						return
+					}
+					cv := cells[j]
+					if cv == nil {
+						br.Msg = fmt.Sprintf("第%d列应为%s,请检查表头", j+1, titleArr[j])
+						return
+					}
+					title := strings.TrimSpace(cv.Value)
+					if titleArr[j] != title {
+						br.Msg = fmt.Sprintf("第%d列应为%s,请检查表头", j+1, titleArr[j])
+						return
+					}
+				}
+				continue
+			}
+
+			// 数据行,每格均为必填
+			for j := 0; j < colMax; j++ {
+				if cellLen <= j {
+					br.Msg = fmt.Sprintf("第%d行第%d列数据有误,请检查", i+1, j+1)
+					return
+				}
+				cv := cells[j]
+				if cv == nil {
+					br.Msg = fmt.Sprintf("第%d行第%d列数据有误,请检查", i+1, j+1)
+					return
+				}
+				val := strings.TrimSpace(cv.Value)
+				if val == "" {
+					br.Msg = fmt.Sprintf("第%d行第%d列数据为空,请检查", i+1, j+1)
+					return
+				}
+				switch j {
+				case 0:
+					// 品种名称
+					vat := varietyNameMatch[val]
+					if vat == nil {
+						br.Msg = fmt.Sprintf("第%d行第%d列品种不存在,请检查", i+1, j+1)
+						return
+					}
+					rowData[i] = new(models.AssessmentVarietyData)
+					rowData[i].VarietyId = vat.AssessmentVarietyId
+					rowData[i].VarietyName = vat.VarietyName
+					rowData[i].VarietyCode = vat.VarietyCode
+					rowData[i].CreateTime = time.Now().Local()
+					rowData[i].ModifyTime = time.Now().Local()
+				case 1:
+					// 当周收盘日期
+					d, e := time.ParseInLocation(utils.FormatDate, val, time.Local)
+					if e != nil {
+						br.Msg = fmt.Sprintf("第%d行第%d列日期格式有误,请检查", i+1, j+1)
+						return
+					}
+					rd := rowData[i]
+					if rd == nil {
+						br.Msg = fmt.Sprintf("第%d行第%d列异常,请检查", i+1, j+1)
+						return
+					}
+					rd.WeekDate = d
+
+					// 加入日期集合,后面统一校验品种和日期是否一一对应
+					if !utils.InArrayByStr(weekDates, val) {
+						weekDates = append(weekDates, val)
+					}
+
+					// 品种的日期存在重复需要提示
+					k := fmt.Sprintf("%s-%s", rd.VarietyName, val)
+					if varietyDateExist[k] {
+						br.Msg = fmt.Sprintf("品种%s%s的数据重复,请检查", rd.VarietyName, val)
+						return
+					}
+					varietyDateExist[k] = true
+				case 2, 3, 4:
+					// 当周收盘价
+					price, e := strconv.ParseFloat(val, 64)
+					if e != nil {
+						br.Msg = fmt.Sprintf("第%d行第%d列价格异常,请检查", i+1, j+1)
+						return
+					}
+					rd := rowData[i]
+					if rd == nil {
+						br.Msg = fmt.Sprintf("第%d行第%d列异常,请检查", i+1, j+1)
+						return
+					}
+					if j == 2 {
+						rd.CloseValue = price
+					}
+					if j == 3 {
+						rd.HighValue = price
+					}
+					if j == 4 {
+						rd.LowValue = price
+					}
+				}
+			}
+		}
+	}
+
+	// 校验品种和日期是否一一对应
+	for _, v := range varieties {
+		for _, d := range weekDates {
+			k := fmt.Sprintf("%s-%s", v, d)
+			if !varietyDateExist[k] {
+				br.Msg = fmt.Sprintf("品种%s%s数据不存在,请确保品种和周度数据一一对应", v, d)
+				return
+			}
+		}
+	}
+	if len(rowData) == 0 {
+		br.Msg = "无有效数据"
+		return
+	}
+
+	// 获取已有数据根据[品种-日期]判断新增还是更新
+	dataOb := new(models.AssessmentVarietyData)
+	existsData, e := dataOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+	if e != nil {
+		br.Msg = "导入失败"
+		br.ErrMsg = fmt.Sprintf("获取品种数据列表失败, %v", e)
+		return
+	}
+	existsMapping := make(map[string]*models.AssessmentVarietyData)
+	for _, v := range existsData {
+		k := fmt.Sprintf("%s-%s", v.VarietyName, v.WeekDate.Format(utils.FormatDate))
+		existsMapping[k] = v
+	}
+	insertData, updateData := make([]*models.AssessmentVarietyData, 0), make([]*models.AssessmentVarietyData, 0)
+	for _, v := range rowData {
+		k := fmt.Sprintf("%s-%s", v.VarietyName, v.WeekDate.Format(utils.FormatDate))
+		exist := existsMapping[k]
+		if exist == nil {
+			insertData = append(insertData, v)
+			continue
+		}
+		// 价格数据一致不更新
+		if exist.CloseValue == v.CloseValue && exist.HighValue == v.HighValue && exist.LowValue == v.LowValue {
+			continue
+		}
+		exist.CloseValue = v.CloseValue
+		exist.HighValue = v.HighValue
+		exist.LowValue = v.LowValue
+		exist.ModifyTime = time.Now().Local()
+		updateData = append(updateData, exist)
+	}
+
+	// 新增/更新数据
+	if len(insertData) > 0 {
+		if e = dataOb.CreateMulti(insertData); e != nil {
+			br.Msg = "导入失败"
+			br.ErrMsg = fmt.Sprintf("新增品种数据失败, %v", e)
+			return
+		}
+	}
+	if len(updateData) > 0 {
+		updateCols := []string{dataOb.Cols().CloseValue, dataOb.Cols().HighValue, dataOb.Cols().LowValue, dataOb.Cols().ModifyTime}
+		for _, v := range updateData {
+			if e = v.Update(updateCols); e != nil {
+				br.Msg = "导入失败"
+				br.ErrMsg = fmt.Sprintf("更新品种数据失败, %v", e)
+				return
+			}
+		}
+	}
+
+	br.Ret = 200
+	br.Success = true
+	br.Msg = "操作成功"
+}

+ 17 - 0
controllers/sys_admin.go

@@ -654,6 +654,8 @@ func (this *SysAdminController) Edit() {
 		br.Msg = "账号名称已存在,请重新输入"
 		return
 	}
+	originName := item.RealName
+	originEnabled := item.Enabled
 
 	// 手机号和邮箱必填一个
 	req.Mobile = strings.TrimSpace(req.Mobile)
@@ -840,6 +842,11 @@ func (this *SysAdminController) Edit() {
 		}()
 	}
 
+	// 如果姓名或者状态被改变,那么同步更新考核研究员表
+	if originName != req.RealName || originEnabled != req.Enabled {
+		go services.SyncAssessmentResearcher(adminInfo.AdminId)
+	}
+
 	go eta_forum.AdminSave(adminInfo.AdminId)
 	br.Ret = 200
 	br.Success = true
@@ -927,6 +934,10 @@ func (this *SysAdminController) EditEnabled() {
 		br.ErrMsg = "操作失败,Err:" + err.Error()
 		return
 	}
+
+	// 同步更新考核研究员表
+	go services.SyncAssessmentResearcher(adminItem.AdminId)
+
 	go eta_forum.AdminSave(adminItem.AdminId)
 	br.Ret = 200
 	br.Success = true
@@ -997,6 +1008,12 @@ func (this *SysAdminController) Delete() {
 			_, _ = etaTrialService.RemoveEtaTrialUser(r)
 		}()
 	}
+
+	// 同步更新考核研究员表
+	if adminInfo != nil && adminInfo.AdminId > 0 {
+		go services.SyncAssessmentResearcher(adminInfo.AdminId)
+	}
+
 	go eta_forum.AdminDelete(adminInfo.AdminName)
 	br.Ret = 200
 	br.Success = true

+ 363 - 0
models/assessment_form.go

@@ -0,0 +1,363 @@
+package models
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"strings"
+	"time"
+)
+
+const (
+	AssessmentFormStatusDraft     = 0
+	AssessmentFormStatusSubmitted = 1
+
+	AssessmentFormMonthlyPriceUp    = "涨"
+	AssessmentFormMonthlyPriceDown  = "跌"
+	AssessmentFormMonthlyPriceShake = "震荡"
+	AssessmentFormWeekUpYes         = "是"
+	AssessmentFormWeekUpNo          = "否"
+	AssessmentFormWeekDownYes       = "是"
+	AssessmentFormWeekDownNo        = "否"
+)
+
+// AssessmentForm 研究员考核填报单
+type AssessmentForm struct {
+	AssessmentFormId     int       `gorm:"column:assessment_form_id;primaryKey;autoIncrement"`
+	FormCode             string    `description:"单号"`
+	ResearcherId         int       `description:"研究员ID"`
+	ResearcherAdminId    int       `description:"研究员用户ID"`
+	ResearcherName       string    `description:"研究员姓名"`
+	VarietyId            int       `description:"品种ID"`
+	VarietyCode          string    `description:"品种编码"`
+	VarietyName          string    `description:"品种名称"`
+	WeekTime             string    `description:"周度(格式:202501,202502)"`
+	WeekStart            time.Time `description:"当周开始日期"`
+	WeekEnd              time.Time `description:"当周结束日期"`
+	BaseDate             time.Time `description:"价格基准日期"`
+	MonthlyPriceForecast string    `description:"月度涨跌:涨/跌/震荡"`
+	WeeklyUpForecast     string    `description:"周度上行风险:是/否"`
+	WeeklyDownForecast   string    `description:"周度下行风险:是/否"`
+	Status               int       `description:"状态:0-草稿;1-已提交;"`
+	SubmitTime           time.Time `description:"提交时间"`
+	CreateTime           time.Time `description:"创建时间"`
+	ModifyTime           time.Time `description:"修改时间"`
+}
+
+func (m *AssessmentForm) TableName() string {
+	return "assessment_form"
+}
+
+type AssessmentFormCols struct {
+	PrimaryId            string
+	FormCode             string
+	ResearcherId         string
+	ResearcherAdminId    string
+	ResearcherName       string
+	VarietyId            string
+	VarietyCode          string
+	VarietyName          string
+	WeekTime             string
+	WeekStart            string
+	WeekEnd              string
+	BaseDate             string
+	MonthlyPriceForecast string
+	WeeklyUpForecast     string
+	WeeklyDownForecast   string
+	Status               string
+	SubmitTime           string
+	CreateTime           string
+	ModifyTime           string
+}
+
+func (m *AssessmentForm) Cols() AssessmentFormCols {
+	return AssessmentFormCols{
+		PrimaryId:            "assessment_form_id",
+		FormCode:             "form_code",
+		ResearcherId:         "researcher_id",
+		ResearcherAdminId:    "researcher_admin_id",
+		ResearcherName:       "researcher_name",
+		VarietyId:            "variety_id",
+		VarietyCode:          "variety_code",
+		VarietyName:          "variety_name",
+		WeekTime:             "week_time",
+		WeekStart:            "week_start",
+		WeekEnd:              "week_end",
+		BaseDate:             "base_date",
+		MonthlyPriceForecast: "monthly_price_forecast",
+		WeeklyUpForecast:     "weekly_up_forecast",
+		WeeklyDownForecast:   "weekly_down_forecast",
+		Status:               "status",
+		SubmitTime:           "submit_time",
+		CreateTime:           "create_time",
+		ModifyTime:           "modify_time",
+	}
+}
+
+func (m *AssessmentForm) Create() (err error) {
+	err = global.DEFAULT_DB.Create(m).Error
+	return
+}
+
+func (m *AssessmentForm) CreateMulti(items []*AssessmentForm) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DB.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *AssessmentForm) Update(cols []string) (err error) {
+	err = global.DEFAULT_DB.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *AssessmentForm) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Exec(sql, m.AssessmentFormId).Error
+	return
+}
+
+func (m *AssessmentForm) 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_DB.Exec(sql, ids).Error
+	return
+}
+
+func (m *AssessmentForm) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DB.Exec(sql, pars...).Error
+	return
+}
+
+func (m *AssessmentForm) GetItemById(primaryId int) (item *AssessmentForm, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Raw(sql, primaryId).First(&item).Error
+	return
+}
+
+func (m *AssessmentForm) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *AssessmentForm, 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_DB.Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *AssessmentForm) 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_DB.Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+func (m *AssessmentForm) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AssessmentForm, 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_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+func (m *AssessmentForm) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*AssessmentForm, 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 LIMIT ?,?`, fields, m.TableName(), condition, order)
+	pars = append(pars, startSize, pageSize)
+	err = global.DEFAULT_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+// AssessmentFormSaveReq 保存填报单请求
+type AssessmentFormSaveReq struct {
+	List []AssessmentFormSaveItem
+}
+
+// AssessmentFormSaveItem 填报单保存信息
+type AssessmentFormSaveItem struct {
+	AssessmentFormId     int    `description:"填报单ID"`
+	FormCode             string `description:"单号"`
+	ResearcherId         int    `description:"研究员ID"`
+	ResearcherAdminId    int    `description:"研究员用户ID"`
+	ResearcherName       string `description:"研究员姓名"`
+	VarietyId            int    `description:"品种ID"`
+	VarietyCode          string `description:"品种编码"`
+	VarietyName          string `description:"品种名称"`
+	WeekTime             string `description:"周度(格式:202501,202502)"`
+	BaseDate             string `description:"价格基准日期"`
+	MonthlyPriceForecast string `description:"月度涨跌:涨/跌/震荡"`
+	WeeklyUpForecast     string `description:"周度上行风险:是/否"`
+	WeeklyDownForecast   string `description:"周度下行风险:是/否"`
+	Status               int    `description:"状态:0-草稿;1-已提交;"`
+}
+
+// AssessmentFormSubmitReq 提交填报单请求
+type AssessmentFormSubmitReq struct {
+	FormCode string `description:"单号"`
+}
+
+// AssessmentFormCancelReq 撤销填报单请求
+type AssessmentFormCancelReq struct {
+	FormCode string `description:"单号"`
+}
+
+// AssessmentFormRemoveReq 删除填报单请求
+type AssessmentFormRemoveReq struct {
+	FormCode string `description:"单号"`
+}
+
+// AssessmentFormPageListReq 填报单列表筛选
+type AssessmentFormPageListReq struct {
+	PageSize                int    `form:"PageSize"`
+	CurrentIndex            int    `form:"CurrentIndex"`
+	AssessmentResearcherIds string `form:"AssessmentResearcherIds" description:"研究员IDs"`
+	AssessmentVarietyIds    string `form:"AssessmentVarietyIds" description:"品种IDs"`
+	StartTime               string `form:"StartTime" description:"开始时间"`
+	EndTime                 string `form:"EndTime" description:"结束时间"`
+}
+
+// AssessmentFormPageListResp 填报单分页列表
+type AssessmentFormPageListResp struct {
+	List   []*AssessmentFormDetail
+	Paging *paging.PagingItem `description:"分页数据"`
+}
+
+// AssessmentFormDetail 填报单信息
+type AssessmentFormDetail struct {
+	AssessmentFormId     int                  `description:"填报单ID"`
+	FormCode             string               `description:"单号"`
+	ResearcherId         int                  `description:"研究员ID"`
+	ResearcherAdminId    int                  `description:"研究员用户ID"`
+	ResearcherName       string               `description:"研究员姓名"`
+	VarietyId            int                  `description:"品种ID"`
+	VarietyCode          string               `description:"品种编码"`
+	VarietyName          string               `description:"品种名称"`
+	WeekTime             string               `description:"周度(格式:202501,202502)"`
+	BaseDate             string               `description:"价格基准日期"`
+	MonthlyPriceForecast string               `description:"月度涨跌:涨/跌/震荡"`
+	WeeklyUpForecast     string               `description:"周度上行风险:是/否"`
+	WeeklyDownForecast   string               `description:"周度下行风险:是/否"`
+	Status               int                  `description:"状态:0-草稿;1-已提交;"`
+	SubmitTime           string               `description:"提交时间"`
+	CreateTime           string               `description:"创建时间"`
+	ModifyTime           string               `description:"修改时间"`
+	Button               AssessmentFormButton `description:"按钮权限"`
+}
+
+type AssessmentFormButton struct {
+	ViewButton   bool `description:"查看按钮"`
+	EditButton   bool `description:"编辑按钮"`
+	RemoveButton bool `description:"删除按钮"`
+	SubmitButton bool `description:"提交按钮"`
+	CancelButton bool `description:"撤销按钮"`
+}
+
+func (m *AssessmentForm) Format2Detail() (item *AssessmentFormDetail) {
+	item = new(AssessmentFormDetail)
+	item.AssessmentFormId = m.AssessmentFormId
+	item.FormCode = m.FormCode
+	item.ResearcherId = m.ResearcherId
+	item.ResearcherAdminId = m.ResearcherAdminId
+	item.ResearcherName = m.ResearcherName
+	item.VarietyId = m.VarietyId
+	item.VarietyCode = m.VarietyCode
+	item.VarietyName = m.VarietyName
+	item.WeekTime = m.WeekTime
+	item.BaseDate = utils.TimeTransferString(utils.FormatDate, m.BaseDate)
+	item.MonthlyPriceForecast = m.MonthlyPriceForecast
+	item.WeeklyUpForecast = m.WeeklyUpForecast
+	item.WeeklyDownForecast = m.WeeklyDownForecast
+	item.Status = m.Status
+	item.SubmitTime = utils.TimeTransferString(utils.FormatDate, m.SubmitTime)
+	item.CreateTime = utils.TimeTransferString(utils.FormatDateTime, m.CreateTime)
+	item.ModifyTime = utils.TimeTransferString(utils.FormatDateTime, m.ModifyTime)
+	return
+}
+
+// AssessmentFormViewResp 查看填报单
+type AssessmentFormViewResp struct {
+	List            []*AssessmentFormDetail          `description:"填报单详情"`
+	VarietyPrice    []*AssessmentFormVarietyPrice    `description:"品种价格详情"`
+	ForecastComment []*AssessmentFormForecastComment `description:"观点评价"`
+}
+
+// AssessmentFormVarietyPrice 品种价格详情
+type AssessmentFormVarietyPrice struct {
+	VarietyId      int    `description:"品种ID"`
+	VarietyCode    string `description:"品种编码"`
+	VarietyName    string `description:"品种名称"`
+	EndDate        string `description:"最新日期"`
+	LatestValue    string `description:"最新价格"`
+	BaseDatePrice  string `description:"基准日收盘价"`
+	NextWeekPrice  string `description:"N+1周价格"`
+	NextMonthPrice string `description:"N+4周价格"`
+}
+
+// AssessmentFormForecastComment 观点评价
+type AssessmentFormForecastComment struct {
+	VarietyId                 int    `description:"品种ID"`
+	VarietyCode               string `description:"品种编码"`
+	VarietyName               string `description:"品种名称"`
+	WeekTime                  string `description:"周度(格式:202501,202502)"`
+	SubmitTime                string `description:"填报日期"`
+	MonthlyPriceComment       string `description:"月度涨跌评价"`
+	MonthlyPriceForecastRight bool   `description:"月度涨跌判断是否正确"`
+	WeeklyUpComment           string `description:"周度上行风险评价"`
+	WeeklyUpForecastRight     bool   `description:"周度上行风险判断是否正确"`
+	WeeklyDownComment         string `description:"周度下行风险评价"`
+	WeeklyDownForecastRight   bool   `description:"周度下行风险判断是否正确"`
+}
+
+// AssessmentFormCheckAddResp 新建填报权限响应
+type AssessmentFormCheckAddResp struct {
+	IsResearcher bool `description:"是否为研究员"`
+	HasVariety   bool `description:"是否绑定品种"`
+	NewForm      bool `description:"是否可新建本周填报"`
+}
+
+// AssessmentFormResultStatisticReq 填报单结果统计请求
+type AssessmentFormResultStatisticReq struct {
+	AssessmentResearcherIds string `form:"AssessmentResearcherIds" description:"研究员IDs"`
+	StartTime               string `form:"StartTime" description:"开始时间"`
+	EndTime                 string `form:"EndTime" description:"结束时间"`
+}
+
+// AssessmentFormResultStatisticItem 填报单结果统计
+type AssessmentFormResultStatisticItem struct {
+	ResearcherId          int     `description:"研究员ID"`
+	ResearcherAdminId     int     `description:"研究员用户ID"`
+	ResearcherName        string  `description:"研究员姓名"`
+	VarietyId             int     `description:"品种ID"`
+	VarietyCode           string  `description:"品种编码"`
+	VarietyName           string  `description:"品种名称"`
+	MonthlyTrendAccuracy  float64 `description:"月度趋势正确率"`
+	WeeklyWarningAccuracy float64 `description:"周度预警正确率"`
+	TotalAccuracyA        float64 `description:"综合正确率1"`
+	TotalAccuracyB        float64 `description:"综合正确率2"`
+}
+
+func (m *AssessmentForm) UpdateResearcherName(researcherId int, researcherName string) (err error) {
+	sql := fmt.Sprintf(`UPDATE %s SET %s = ? WHERE %s = ?`, m.TableName(), m.Cols().ResearcherName, m.Cols().ResearcherId)
+	err = global.DEFAULT_DB.Exec(sql, researcherName, researcherId).Error
+	return
+}

+ 206 - 0
models/assessment_researcher.go

@@ -0,0 +1,206 @@
+package models
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"gorm.io/gorm"
+	"strings"
+	"time"
+)
+
+const (
+	AssessmentResearcherDisabled = 0
+	AssessmentResearcherEnabled  = 1
+)
+
+// AssessmentResearcher 考核研究员表
+type AssessmentResearcher struct {
+	AssessmentResearcherId int       `gorm:"column:assessment_researcher_id;primaryKey;autoIncrement"`
+	AdminId                int       `description:"系统用户ID"`
+	RealName               string    `description:"用户姓名"`
+	Enabled                int       `description:"状态:0-禁用;1-正常"`
+	CreateTime             time.Time `description:"创建时间"`
+	ModifyTime             time.Time `description:"修改时间"`
+}
+
+func (m *AssessmentResearcher) TableName() string {
+	return "assessment_researcher"
+}
+
+type AssessmentResearcherCols struct {
+	PrimaryId  string
+	AdminId    string
+	RealName   string
+	Enabled    string
+	CreateTime string
+	ModifyTime string
+}
+
+func (m *AssessmentResearcher) Cols() AssessmentResearcherCols {
+	return AssessmentResearcherCols{
+		PrimaryId:  "assessment_researcher_id",
+		AdminId:    "admin_id",
+		RealName:   "real_name",
+		Enabled:    "enabled",
+		CreateTime: "create_time",
+		ModifyTime: "modify_time",
+	}
+}
+
+func (m *AssessmentResearcher) Create() (err error) {
+	err = global.DEFAULT_DB.Create(m).Error
+	return
+}
+
+func (m *AssessmentResearcher) CreateMulti(items []*AssessmentResearcher) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DB.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *AssessmentResearcher) Update(cols []string) (err error) {
+	err = global.DEFAULT_DB.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *AssessmentResearcher) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Exec(sql, m.AssessmentResearcherId).Error
+	return
+}
+
+func (m *AssessmentResearcher) 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_DB.Exec(sql, ids).Error
+	return
+}
+
+func (m *AssessmentResearcher) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DB.Exec(sql, pars...).Error
+	return
+}
+
+func (m *AssessmentResearcher) GetItemById(primaryId int) (item *AssessmentResearcher, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Raw(sql, primaryId).First(&item).Error
+	return
+}
+
+func (m *AssessmentResearcher) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *AssessmentResearcher, 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_DB.Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *AssessmentResearcher) 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_DB.Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+func (m *AssessmentResearcher) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AssessmentResearcher, 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_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+func (m *AssessmentResearcher) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*AssessmentResearcher, 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 LIMIT ?,?`, fields, m.TableName(), condition, order)
+	pars = append(pars, startSize, pageSize)
+	err = global.DEFAULT_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+// AfterUpdate 同步更新其他表冗余
+func (m *AssessmentResearcher) AfterUpdate(tx *gorm.DB) (err error) {
+	if tx.Statement.Changed(m.Cols().RealName) {
+		fmt.Println(111)
+		formOb := new(AssessmentForm)
+		err = tx.Model(formOb).Where(fmt.Sprintf("%s = ?", formOb.Cols().ResearcherId), m.AssessmentResearcherId).Update(formOb.Cols().ResearcherName, m.RealName).Error
+		fmt.Println("AfterUpdate", err)
+	}
+	return
+}
+
+// AssessmentResearcherAddReq 新增研究员请求
+type AssessmentResearcherAddReq struct {
+	AdminId            int   `description:"用户ID"`
+	ViewAdminIds       []int `description:"查看权限用户IDs"`
+	AssessmentAdminIds []int `description:"统计权限用户IDs"`
+}
+
+// AssessmentResearcherEditReq 编辑研究员请求
+type AssessmentResearcherEditReq struct {
+	AssessmentResearcherId int `description:"研究员ID"`
+	AssessmentResearcherAddReq
+}
+
+// AssessmentResearcherRemoveReq 删除研究员请求
+type AssessmentResearcherRemoveReq struct {
+	AssessmentResearcherId int `description:"研究员ID"`
+}
+
+// AssessmentResearcherPageListReq 研究员列表
+type AssessmentResearcherPageListReq struct {
+	PageSize                int    `form:"PageSize"`
+	CurrentIndex            int    `form:"CurrentIndex"`
+	RealName                string `form:"RealName" description:"用户姓名"`
+	AssessmentResearcherIds string `form:"AssessmentResearcherIds" description:"研究员IDs"`
+	OnlyEnabled             bool   `form:"OnlyEnabled" description:"是否仅显示启用:true-是"`
+	ResearcherAuthType      int    `form:"ResearcherAuthType" description:"权限类型:0-全部;1-当前用户有查看权限的;2-当前用户有统计权限的"`
+}
+
+// AssessmentResearcherPageListResp 研究员列表响应
+type AssessmentResearcherPageListResp struct {
+	List   []*AssessmentResearcherDetail
+	Paging *paging.PagingItem `description:"分页数据"`
+}
+
+type AssessmentResearcherDetail struct {
+	AssessmentResearcherId int                          `description:"研究员ID"`
+	AdminId                int                          `description:"用户ID"`
+	RealName               string                       `description:"用户姓名"`
+	Enabled                int                          `description:"状态:0-禁用;1-正常"`
+	ViewAdmins             []AssessmentResearcherDetail `description:"查看权限用户"`
+	AssessmentAdmins       []AssessmentResearcherDetail `description:"统计权限用户"`
+}
+
+func (m *AssessmentResearcher) Format2Detail() (item *AssessmentResearcherDetail) {
+	item = new(AssessmentResearcherDetail)
+	item.AssessmentResearcherId = m.AssessmentResearcherId
+	item.AdminId = m.AdminId
+	item.RealName = m.RealName
+	item.Enabled = m.Enabled
+	return
+}

+ 116 - 0
models/assessment_researcher_admin_mapping.go

@@ -0,0 +1,116 @@
+package models
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"strings"
+)
+
+const (
+	AssessmentResearcherAdminAuthTypeView       = 1
+	AssessmentResearcherAdminAuthTypeStatistics = 2
+)
+
+// AssessmentResearcherAdminMapping 研究员考核-用户权限关系表
+type AssessmentResearcherAdminMapping struct {
+	Id                     int `gorm:"column:id;primaryKey;autoIncrement"`
+	AssessmentResearcherId int `description:"研究员ID"`
+	AdminId                int `description:"系统用户ID"`
+	AuthAdminId            int `description:"被授权的用户ID"`
+	AuthType               int `description:"权限类型:1-查看;2-统计"`
+}
+
+func (m *AssessmentResearcherAdminMapping) TableName() string {
+	return "assessment_researcher_admin_mapping"
+}
+
+type AssessmentResearcherAdminMappingCols struct {
+	PrimaryId              string
+	AssessmentResearcherId string
+	AdminId                string
+	AuthAdminId            string
+	AuthType               string
+}
+
+func (m *AssessmentResearcherAdminMapping) Cols() AssessmentResearcherAdminMappingCols {
+	return AssessmentResearcherAdminMappingCols{
+		PrimaryId:              "id",
+		AssessmentResearcherId: "assessment_researcher_id",
+		AdminId:                "admin_id",
+		AuthAdminId:            "auth_admin_id",
+		AuthType:               "auth_type",
+	}
+}
+
+func (m *AssessmentResearcherAdminMapping) Create() (err error) {
+	err = global.DEFAULT_DB.Create(m).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) CreateMulti(items []*AssessmentResearcherAdminMapping) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DB.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) Update(cols []string) (err error) {
+	err = global.DEFAULT_DB.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Exec(sql, m.Id).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) 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_DB.Exec(sql, ids).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DB.Exec(sql, pars...).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) GetItemById(primaryId int) (item *AssessmentResearcherAdminMapping, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Raw(sql, primaryId).First(&item).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) GetItemByCondition(condition string, pars []interface{}, orderRule string) (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_DB.Raw(sql, pars...).First(&m).Error
+	return
+}
+
+func (m *AssessmentResearcherAdminMapping) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AssessmentResearcherAdminMapping, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := ``
+	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_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}

+ 108 - 0
models/assessment_researcher_variety_mapping.go

@@ -0,0 +1,108 @@
+package models
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"strings"
+)
+
+// AssessmentResearcherVarietyMapping 研究员考核-用户权限关系表
+type AssessmentResearcherVarietyMapping struct {
+	Id                     int `gorm:"column:id;primaryKey;autoIncrement"`
+	AssessmentResearcherId int `description:"研究员ID"`
+	AdminId                int `description:"系统用户ID"`
+	VarietyId              int `description:"品种ID"`
+}
+
+func (m *AssessmentResearcherVarietyMapping) TableName() string {
+	return "assessment_researcher_variety_mapping"
+}
+
+type AssessmentResearcherVarietyMappingCols struct {
+	PrimaryId              string
+	AssessmentResearcherId string
+	AdminId                string
+	VarietyId              string
+}
+
+func (m *AssessmentResearcherVarietyMapping) Cols() AssessmentResearcherVarietyMappingCols {
+	return AssessmentResearcherVarietyMappingCols{
+		PrimaryId:              "id",
+		AssessmentResearcherId: "assessment_researcher_id",
+		AdminId:                "admin_id",
+		VarietyId:              "variety_id",
+	}
+}
+
+func (m *AssessmentResearcherVarietyMapping) Create() (err error) {
+	err = global.DEFAULT_DB.Create(m).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) CreateMulti(items []*AssessmentResearcherVarietyMapping) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DB.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) Update(cols []string) (err error) {
+	err = global.DEFAULT_DB.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Exec(sql, m.Id).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) 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_DB.Exec(sql, ids).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DB.Exec(sql, pars...).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) GetItemById(primaryId int) (item *AssessmentResearcherVarietyMapping, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Raw(sql, primaryId).First(&item).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) GetItemByCondition(condition string, pars []interface{}, orderRule string) (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_DB.Raw(sql, pars...).First(&m).Error
+	return
+}
+
+func (m *AssessmentResearcherVarietyMapping) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AssessmentResearcherVarietyMapping, err error) {
+	fields := strings.Join(fieldArr, ",")
+	if len(fieldArr) == 0 {
+		fields = `*`
+	}
+	order := ``
+	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_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}

+ 203 - 0
models/assessment_variety.go

@@ -0,0 +1,203 @@
+package models
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"github.com/rdlucklib/rdluck_tools/paging"
+	"strings"
+	"time"
+)
+
+const (
+	AssessmentVarietyDisabled = 0
+	AssessmentVarietyEnabled  = 1
+)
+
+// AssessmentVariety 研究员考核品种表
+type AssessmentVariety struct {
+	AssessmentVarietyId int       `gorm:"column:assessment_variety_id;primaryKey;autoIncrement"`
+	VarietyName         string    `description:"品种名称"`
+	VarietyCode         string    `description:"品种编码"`
+	MonthlyFluctuate    float64   `description:"月度波动阈值"`
+	WeeklyFluctuate     float64   `description:"周度波动阈值"`
+	Enabled             int       `description:"状态:0-禁用;1-正常"`
+	CreateTime          time.Time `description:"创建时间"`
+	ModifyTime          time.Time `description:"修改时间"`
+}
+
+func (m *AssessmentVariety) TableName() string {
+	return "assessment_variety"
+}
+
+type AssessmentVarietyCols struct {
+	PrimaryId        string
+	VarietyName      string
+	VarietyCode      string
+	MonthlyFluctuate string
+	WeeklyFluctuate  string
+	Enabled          string
+	CreateTime       string
+	ModifyTime       string
+}
+
+func (m *AssessmentVariety) Cols() AssessmentVarietyCols {
+	return AssessmentVarietyCols{
+		PrimaryId:        "assessment_variety_id",
+		VarietyName:      "variety_name",
+		VarietyCode:      "variety_code",
+		MonthlyFluctuate: "monthly_fluctuate",
+		WeeklyFluctuate:  "weekly_fluctuate",
+		Enabled:          "enabled",
+		CreateTime:       "create_time",
+		ModifyTime:       "modify_time",
+	}
+}
+
+func (m *AssessmentVariety) Create() (err error) {
+	err = global.DEFAULT_DB.Create(m).Error
+	return
+}
+
+func (m *AssessmentVariety) CreateMulti(items []*AssessmentVariety) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DB.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *AssessmentVariety) Update(cols []string) (err error) {
+	err = global.DEFAULT_DB.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *AssessmentVariety) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Exec(sql, m.AssessmentVarietyId).Error
+	return
+}
+
+func (m *AssessmentVariety) 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_DB.Exec(sql, ids).Error
+	return
+}
+
+func (m *AssessmentVariety) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DB.Exec(sql, pars...).Error
+	return
+}
+
+func (m *AssessmentVariety) GetItemById(primaryId int) (item *AssessmentVariety, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Raw(sql, primaryId).First(&item).Error
+	return
+}
+
+func (m *AssessmentVariety) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *AssessmentVariety, 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_DB.Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *AssessmentVariety) 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_DB.Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+func (m *AssessmentVariety) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AssessmentVariety, 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_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+func (m *AssessmentVariety) GetPageItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string, startSize, pageSize int) (items []*AssessmentVariety, 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 LIMIT ?,?`, fields, m.TableName(), condition, order)
+	pars = append(pars, startSize, pageSize)
+	err = global.DEFAULT_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+// AssessmentVarietyAddReq 新增品种请求
+type AssessmentVarietyAddReq struct {
+	VarietyName             string  `description:"品种名称"`
+	VarietyCode             string  `description:"品种编码"`
+	MonthlyFluctuate        float64 `description:"月度波动阈值"`
+	WeeklyFluctuate         float64 `description:"周度波动阈值"`
+	AssessmentResearcherIds []int   `description:"研究员ID"`
+}
+
+// AssessmentVarietyEditReq 编辑品种请求
+type AssessmentVarietyEditReq struct {
+	AssessmentVarietyId int `description:"品种ID"`
+	AssessmentVarietyAddReq
+}
+
+// AssessmentVarietyRemoveReq 删除品种请求
+type AssessmentVarietyRemoveReq struct {
+	AssessmentVarietyId int `description:"研究员ID"`
+}
+
+// AssessmentVarietyPageListReq 品种列表
+type AssessmentVarietyPageListReq struct {
+	PageSize             int    `form:"PageSize"`
+	CurrentIndex         int    `form:"CurrentIndex"`
+	AssessmentVarietyIds string `form:"AssessmentVarietyIds" description:"品种IDs"`
+	OnlyEnabled          bool   `form:"OnlyEnabled" description:"是否仅显示启用:true-是"`
+}
+
+// AssessmentVarietyPageListResp 研究员列表响应
+type AssessmentVarietyPageListResp struct {
+	List   []*AssessmentVarietyDetail
+	Paging *paging.PagingItem `description:"分页数据"`
+}
+
+type AssessmentVarietyDetail struct {
+	AssessmentVarietyId int                          `description:"研究员ID"`
+	VarietyName         string                       `description:"品种名称"`
+	VarietyCode         string                       `description:"品种编码"`
+	MonthlyFluctuate    float64                      `description:"月度波动阈值"`
+	WeeklyFluctuate     float64                      `description:"周度波动阈值"`
+	Enabled             int                          `description:"状态:0-禁用;1-正常"`
+	RelationResearcher  []AssessmentResearcherDetail `description:"关联研究员信息"`
+}
+
+func (m *AssessmentVariety) Format2Detail() (item *AssessmentVarietyDetail) {
+	item = new(AssessmentVarietyDetail)
+	item.AssessmentVarietyId = m.AssessmentVarietyId
+	item.VarietyName = m.VarietyName
+	item.VarietyCode = m.VarietyCode
+	item.MonthlyFluctuate = m.MonthlyFluctuate
+	item.WeeklyFluctuate = m.WeeklyFluctuate
+	item.Enabled = m.Enabled
+	return
+}

+ 150 - 0
models/assessment_variety_data.go

@@ -0,0 +1,150 @@
+package models
+
+import (
+	"eta/eta_api/global"
+	"eta/eta_api/utils"
+	"fmt"
+	"strings"
+	"time"
+)
+
+// AssessmentVarietyData 研究员考核品种表
+type AssessmentVarietyData struct {
+	Id          int       `gorm:"column:id;primaryKey;autoIncrement"`
+	VarietyId   int       `description:"品种ID"`
+	VarietyName string    `description:"品种名称"`
+	VarietyCode string    `description:"品种编码"`
+	WeekDate    time.Time `description:"当周日期"`
+	CloseValue  float64   `description:"当周收盘价"`
+	HighValue   float64   `description:"当周最高价"`
+	LowValue    float64   `description:"当周最低价"`
+	CreateTime  time.Time `description:"创建时间"`
+	ModifyTime  time.Time `description:"修改时间"`
+}
+
+func (m *AssessmentVarietyData) TableName() string {
+	return "assessment_variety_data"
+}
+
+type AssessmentVarietyDataCols struct {
+	PrimaryId   string
+	VarietyId   string
+	VarietyName string
+	VarietyCode string
+	WeekDate    string
+	CloseValue  string
+	HighValue   string
+	LowValue    string
+	CreateTime  string
+	ModifyTime  string
+}
+
+func (m *AssessmentVarietyData) Cols() AssessmentVarietyDataCols {
+	return AssessmentVarietyDataCols{
+		PrimaryId:   "id",
+		VarietyId:   "variety_id",
+		VarietyName: "variety_name",
+		VarietyCode: "variety_code",
+		WeekDate:    "week_date",
+		CloseValue:  "close_value",
+		HighValue:   "high_value",
+		LowValue:    "low_value",
+		CreateTime:  "create_time",
+		ModifyTime:  "modify_time",
+	}
+}
+
+func (m *AssessmentVarietyData) Create() (err error) {
+	err = global.DEFAULT_DB.Create(m).Error
+	return
+}
+
+func (m *AssessmentVarietyData) CreateMulti(items []*AssessmentVarietyData) (err error) {
+	if len(items) == 0 {
+		return
+	}
+	err = global.DEFAULT_DB.CreateInBatches(items, utils.MultiAddNum).Error
+	return
+}
+
+func (m *AssessmentVarietyData) Update(cols []string) (err error) {
+	err = global.DEFAULT_DB.Select(cols).Updates(m).Error
+	return
+}
+
+func (m *AssessmentVarietyData) Remove() (err error) {
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Exec(sql, m.Id).Error
+	return
+}
+
+func (m *AssessmentVarietyData) 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_DB.Exec(sql, ids).Error
+	return
+}
+
+func (m *AssessmentVarietyData) RemoveByCondition(condition string, pars []interface{}) (err error) {
+	if condition == "" {
+		return
+	}
+	sql := fmt.Sprintf(`DELETE FROM %s WHERE %s`, m.TableName(), condition)
+	err = global.DEFAULT_DB.Exec(sql, pars...).Error
+	return
+}
+
+func (m *AssessmentVarietyData) GetItemById(primaryId int) (item *AssessmentVarietyData, err error) {
+	sql := fmt.Sprintf(`SELECT * FROM %s WHERE %s = ? LIMIT 1`, m.TableName(), m.Cols().PrimaryId)
+	err = global.DEFAULT_DB.Raw(sql, primaryId).First(&item).Error
+	return
+}
+
+func (m *AssessmentVarietyData) GetItemByCondition(condition string, pars []interface{}, orderRule string) (item *AssessmentVarietyData, 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_DB.Raw(sql, pars...).First(&item).Error
+	return
+}
+
+func (m *AssessmentVarietyData) 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_DB.Raw(sql, pars...).Scan(&count).Error
+	return
+}
+
+func (m *AssessmentVarietyData) GetItemsByCondition(condition string, pars []interface{}, fieldArr []string, orderRule string) (items []*AssessmentVarietyData, 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_DB.Raw(sql, pars...).Find(&items).Error
+	return
+}
+
+// GetVarietyMaxDateData 获取品种最新数据
+func (m *AssessmentVarietyData) GetVarietyMaxDateData(varietyIds []int) (items []*AssessmentVarietyData, err error) {
+	if len(varietyIds) == 0 {
+		return
+	}
+	sql := `SELECT a.*
+		FROM assessment_variety_data a
+		INNER JOIN (
+			SELECT variety_id, MAX(week_date) AS max_week_date
+			FROM assessment_variety_data
+			WHERE variety_id IN (?)
+			GROUP BY variety_id
+		) b ON a.variety_id = b.variety_id AND a.week_date = b.max_week_date`
+	err = global.DEFAULT_DB.Raw(sql, varietyIds).Find(&items).Error
+	return
+}

+ 15 - 0
models/db.go

@@ -234,6 +234,9 @@ func init2() {
 
 	// AI预测模型表
 	initAiPredictModel()
+
+	// 研究员考核表
+	initAssessmentResearcher()
 }
 
 // initSystem 系统表 数据表
@@ -725,6 +728,18 @@ func initAiPredictModel() {
 	)
 }
 
+// initAssessmentResearcher 研究员考核表
+func initAssessmentResearcher() {
+	orm.RegisterModel(
+		new(AssessmentResearcher),
+		new(AssessmentVariety),
+		new(AssessmentVarietyData),
+		new(AssessmentResearcherAdminMapping),
+		new(AssessmentResearcherVarietyMapping),
+		new(AssessmentForm),
+	)
+}
+
 // AfterInitTable
 // @Description: 初始化表结构的的后置操作
 // @author: Roc

+ 180 - 0
routers/commentsRouter.go

@@ -241,6 +241,186 @@ func init() {
             Filters: nil,
             Params: nil})
 
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "Cancel",
+            Router: `/form/cancel`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "CheckAdd",
+            Router: `/form/check_add`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "Detail",
+            Router: `/form/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "PageList",
+            Router: `/form/page_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "Remove",
+            Router: `/form/remove`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "ResultStatistic",
+            Router: `/form/result_statistic`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "Save",
+            Router: `/form/save`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "Submit",
+            Router: `/form/submit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentFormController"],
+        beego.ControllerComments{
+            Method: "WeekBase",
+            Router: `/form/week_base`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/researcher/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"],
+        beego.ControllerComments{
+            Method: "Detail",
+            Router: `/researcher/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"],
+        beego.ControllerComments{
+            Method: "Edit",
+            Router: `/researcher/edit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"],
+        beego.ControllerComments{
+            Method: "PageList",
+            Router: `/researcher/page_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentResearcherController"],
+        beego.ControllerComments{
+            Method: "Remove",
+            Router: `/researcher/remove`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"],
+        beego.ControllerComments{
+            Method: "Add",
+            Router: `/variety/add`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"],
+        beego.ControllerComments{
+            Method: "Detail",
+            Router: `/variety/detail`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"],
+        beego.ControllerComments{
+            Method: "Edit",
+            Router: `/variety/edit`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"],
+        beego.ControllerComments{
+            Method: "ImportData",
+            Router: `/variety/import_data`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"],
+        beego.ControllerComments{
+            Method: "PageList",
+            Router: `/variety/page_list`,
+            AllowHTTPMethods: []string{"get"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
+    beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/assessment:AssessmentVarietyController"],
+        beego.ControllerComments{
+            Method: "Remove",
+            Router: `/variety/remove`,
+            AllowHTTPMethods: []string{"post"},
+            MethodParams: param.Make(),
+            Filters: nil,
+            Params: nil})
+
     beego.GlobalControllerRouter["eta/eta_api/controllers/data_manage/ai_predict_model:AiPredictModelClassifyController"] = append(beego.GlobalControllerRouter["eta/eta_api/controllers/data_manage/ai_predict_model:AiPredictModelClassifyController"],
         beego.ControllerComments{
             Method: "Add",

+ 8 - 0
routers/router.go

@@ -10,6 +10,7 @@ package routers
 import (
 	"eta/eta_api/controllers"
 	"eta/eta_api/controllers/ai"
+	"eta/eta_api/controllers/assessment"
 	"eta/eta_api/controllers/data_manage"
 	"eta/eta_api/controllers/data_manage/ai_predict_model"
 	"eta/eta_api/controllers/data_manage/correlation"
@@ -479,6 +480,13 @@ func init() {
 				&eta_forum.EtaForumController{},
 			),
 		),
+		web.NSNamespace("/assessment",
+			web.NSInclude(
+				&assessment.AssessmentResearcherController{},
+				&assessment.AssessmentVarietyController{},
+				&assessment.AssessmentFormController{},
+			),
+		),
 	)
 	web.AddNamespace(ns)
 }

+ 587 - 0
services/assessment_form.go

@@ -0,0 +1,587 @@
+package services
+
+import (
+	"eta/eta_api/models"
+	"eta/eta_api/models/system"
+	"eta/eta_api/services/alarm_msg"
+	"eta/eta_api/utils"
+	"fmt"
+	"math"
+	"sync"
+	"time"
+)
+
+// 填报单单号锁
+var (
+	formCodeMutex    sync.Mutex
+	formCodeUnique   sync.Map
+	formCodeLastDate string
+)
+
+// GenerateAssessmentFormCode 生成单号
+func GenerateAssessmentFormCode() (formCode string, err error) {
+	formCodeMutex.Lock()
+	defer formCodeMutex.Unlock()
+
+	currentDate := time.Now().Format("20060102")
+	maxAttempts := 1000
+	baseNumber := 1
+
+	// 检查日期变化,清空map
+	if formCodeLastDate != "" && formCodeLastDate != currentDate {
+		formCodeUnique = sync.Map{}
+	}
+	formCodeLastDate = currentDate
+
+	// 获取单号最大ID
+	formOb := new(models.AssessmentForm)
+	maxForm, e := formOb.GetItemByCondition(``, make([]interface{}, 0), fmt.Sprintf("%s DESC", formOb.Cols().PrimaryId))
+	if e != nil && !utils.IsErrNoRow(e) {
+		err = fmt.Errorf("获取最大单号失败, %v", e)
+		return
+	}
+	if maxForm != nil && maxForm.AssessmentFormId > 0 {
+		baseNumber = maxForm.AssessmentFormId + 1
+	}
+
+	// 尝试生成自增ID
+	for i := 0; i < maxAttempts; i++ {
+		serialNumber := fmt.Sprintf("%06d", baseNumber+i)
+		uniqueCode := fmt.Sprintf("GDKH%s%s", currentDate, serialNumber)
+
+		if _, loaded := formCodeUnique.LoadOrStore(uniqueCode, true); loaded {
+			continue
+		}
+
+		// 检查单号是否已存在
+		cond := fmt.Sprintf(` AND %s = ?`, formOb.Cols().FormCode)
+		pars := make([]interface{}, 0)
+		pars = append(pars, uniqueCode)
+		exists, e := formOb.GetCountByCondition(cond, pars)
+		if e != nil {
+			formCodeUnique.Delete(uniqueCode)
+			err = fmt.Errorf("检查单号是否存在失败, %v", e)
+			return
+		}
+		if exists <= 0 {
+			formCode = uniqueCode
+			return
+		}
+	}
+	err = fmt.Errorf("生成单号失败, 尝试次数超过%d次", maxAttempts)
+	return
+}
+
+// GetAssessmentWeekAndFriday 获取给定日期的周数,以及所在周的周五
+// 规则:某年的第一个周五所在周为该年的第一周
+// 返回:格式化字符串(如"202504")、周数(如4)和所在周的周五日期
+func GetAssessmentWeekAndFriday(t time.Time) (string, int, time.Time) {
+	year := t.Year()
+
+	// 找到该年第一个周五的日期
+	firstFriday := findFirstFriday(year)
+
+	// 计算当前日期与该年第一个周五所在周的第一天(周一)的天数差
+	daysSinceFirstWeek := t.YearDay() - firstFriday.YearDay() + int(firstFriday.Weekday()) - int(time.Monday)
+
+	weekNum := 1
+	if daysSinceFirstWeek > 0 {
+		weekNum += daysSinceFirstWeek / 7
+		if daysSinceFirstWeek%7 != 0 {
+			weekNum++
+		}
+	} else {
+		// 如果当前日期在第一个周五所在周之前,则属于上一年的最后一周
+		prevYear := year - 1
+		prevFirstFriday := findFirstFriday(prevYear)
+		daysInPrevYear := daysInYear(prevYear)
+
+		daysSincePrevFirstWeek := t.YearDay() + (daysInPrevYear - prevFirstFriday.YearDay()) + int(prevFirstFriday.Weekday()) - int(time.Monday)
+		weekNum = daysSincePrevFirstWeek / 7
+		if daysSincePrevFirstWeek%7 != 0 {
+			weekNum++
+		}
+		year = prevYear
+	}
+
+	// 计算当前日期所在周的周五
+	currentWeekFriday := findFridayOfWeek(t)
+
+	// 格式化输出
+	formatted := fmt.Sprintf("%04d%02d", year, weekNum)
+	return formatted, weekNum, currentWeekFriday
+}
+
+// findFirstFriday 找到某年的第一个周五
+func findFirstFriday(year int) time.Time {
+	// 从1月1日开始找
+	date := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
+
+	// 找到第一个周五
+	for date.Weekday() != time.Friday {
+		date = date.AddDate(0, 0, 1)
+	}
+
+	return date
+}
+
+// findFridayOfWeek 找到给定日期所在周的周五
+func findFridayOfWeek(t time.Time) time.Time {
+	// 获取当前日期是周几 (0=周日, 1=周一, ..., 6=周六)
+	weekday := int(t.Weekday())
+
+	// 计算到周五的天数差 (周五是5)
+	daysToFriday := (5 - weekday + 7) % 7
+
+	// 如果当前就是周五,daysToFriday会是0
+	if daysToFriday < 0 {
+		daysToFriday += 7
+	}
+
+	return t.AddDate(0, 0, daysToFriday)
+}
+
+// daysInYear 计算某年有多少天
+func daysInYear(year int) int {
+	first := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
+	last := time.Date(year, time.December, 31, 0, 0, 0, 0, time.UTC)
+	return last.YearDay() - first.YearDay() + 1
+}
+
+// CheckAssessmentFormButton 校验填报单按钮
+func CheckAssessmentFormButton(item *models.AssessmentForm, sysAdminId int, authResearcherIds []int) (button models.AssessmentFormButton) {
+	if item == nil || sysAdminId <= 0 {
+		return
+	}
+
+	// 已提交状态
+	if item.Status == models.AssessmentFormStatusSubmitted {
+		if sysAdminId == item.ResearcherAdminId || utils.InArrayByInt(authResearcherIds, item.ResearcherId) {
+			button.ViewButton = true
+		}
+
+		// 本周结束前可撤销(TODO:这里象屿会有一个外部条件:当周周报未提交,暂不处理视作true)
+		if sysAdminId == item.ResearcherAdminId && !item.WeekEnd.IsZero() && time.Now().Local().Before(item.WeekEnd) {
+			button.CancelButton = true
+		}
+	}
+
+	// 待提交状态(仅自己有操作按钮)
+	if item.Status == models.AssessmentFormStatusDraft && sysAdminId == item.ResearcherAdminId {
+		button.EditButton = true
+		button.RemoveButton = true
+
+		// 本周结束前可提交
+		if !item.WeekEnd.IsZero() && time.Now().Local().Before(item.WeekEnd) {
+			button.SubmitButton = true
+		}
+	}
+	return
+}
+
+// GetVarietyPriceAndForecastComment 获取品种价格详情及观点评价
+func GetVarietyPriceAndForecastComment(forms []*models.AssessmentForm) (varietyPrice []*models.AssessmentFormVarietyPrice, forecastComment []*models.AssessmentFormForecastComment, err error) {
+	if len(forms) == 0 {
+		return
+	}
+	baseDate := forms[0].BaseDate
+	var varietyIds []int
+	for _, v := range forms {
+		varietyIds = append(varietyIds, v.VarietyId)
+	}
+	if len(varietyIds) == 0 || baseDate.IsZero() {
+		return
+	}
+
+	// 获取品种信息
+	varietyOb := new(models.AssessmentVariety)
+	varietyMatch := make(map[int]*models.AssessmentVariety)
+	{
+		cond := fmt.Sprintf(` AND %s IN (?)`, varietyOb.Cols().PrimaryId)
+		pars := make([]interface{}, 0)
+		pars = append(pars, varietyIds)
+		list, e := varietyOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取品种信息失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			varietyMatch[v.AssessmentVarietyId] = v
+		}
+	}
+
+	varietyLatestData := make(map[int]*models.AssessmentVarietyData)
+	varietyBaseDateData := make(map[int]*models.AssessmentVarietyData)
+	varietyNextWeekData := make(map[int]*models.AssessmentVarietyData)
+	varietyNextMonthData := make(map[int]*models.AssessmentVarietyData)
+
+	// 获取最新日期和数据
+	dataOb := new(models.AssessmentVarietyData)
+	latestData, e := dataOb.GetVarietyMaxDateData(varietyIds)
+	if e != nil {
+		err = fmt.Errorf("获取品种最新数据失败, %v", e)
+		return
+	}
+	for _, v := range latestData {
+		varietyLatestData[v.VarietyId] = v
+	}
+
+	// 获取基准日期N至N+4周的数据
+	var dateArr []string
+	nextWeek := baseDate.AddDate(0, 0, 7)
+	nextMonth := baseDate.AddDate(0, 0, 28)
+	dateArr = append(dateArr, baseDate.Format(utils.FormatDate), nextWeek.Format(utils.FormatDate), nextMonth.Format(utils.FormatDate))
+	{
+		cond := fmt.Sprintf(` AND %s IN (?) AND %s IN (?)`, dataOb.Cols().VarietyId, dataOb.Cols().WeekDate)
+		pars := make([]interface{}, 0)
+		pars = append(pars, varietyIds, dateArr)
+		list, e := dataOb.GetItemsByCondition(cond, pars, []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取基准日至N+4周数据失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			if varietyMatch[v.VarietyId] == nil {
+				continue
+			}
+			if baseDate.Equal(v.WeekDate) {
+				varietyBaseDateData[v.VarietyId] = v
+				continue
+			}
+			if baseDate.AddDate(0, 0, 7).Equal(v.WeekDate) {
+				varietyNextWeekData[v.VarietyId] = v
+				continue
+			}
+			if baseDate.AddDate(0, 0, 28).Equal(v.WeekDate) {
+				varietyNextMonthData[v.VarietyId] = v
+				continue
+			}
+		}
+	}
+
+	varietyPrice = make([]*models.AssessmentFormVarietyPrice, 0)
+	forecastComment = make([]*models.AssessmentFormForecastComment, 0)
+	for _, v := range forms {
+		vat := varietyMatch[v.VarietyId]
+		if vat == nil {
+			utils.FileLog.Info(fmt.Sprintf("GetVarietyPriceAndForecastComment, 品种不存在: %d", v.VarietyId))
+			continue
+		}
+
+		// 品种价格评价
+		vp := new(models.AssessmentFormVarietyPrice)
+		vp.VarietyId = v.VarietyId
+		vp.VarietyCode = v.VarietyCode
+		vp.VarietyName = v.VarietyName
+		if varietyLatestData[v.VarietyId] != nil {
+			vp.EndDate = varietyLatestData[v.VarietyId].WeekDate.Format(utils.FormatDate)
+			vp.LatestValue = fmt.Sprint(varietyLatestData[v.VarietyId].CloseValue)
+		}
+		var (
+			baseDataPrice     *float64
+			nextMonthPrice    *float64
+			nextWeekHighPrice *float64
+			nextWeekLowPrice  *float64
+		)
+		if varietyBaseDateData[v.VarietyId] != nil {
+			baseDataPrice = &varietyBaseDateData[v.VarietyId].CloseValue
+			vp.BaseDatePrice = fmt.Sprint(varietyBaseDateData[v.VarietyId].CloseValue)
+		}
+		if varietyNextWeekData[v.VarietyId] != nil {
+			nextWeekHighPrice = &varietyNextWeekData[v.VarietyId].HighValue
+			nextWeekLowPrice = &varietyNextWeekData[v.VarietyId].LowValue
+			vp.NextWeekPrice = fmt.Sprint(varietyNextWeekData[v.VarietyId].CloseValue)
+		}
+		if varietyNextMonthData[v.VarietyId] != nil {
+			nextMonthPrice = &varietyNextMonthData[v.VarietyId].CloseValue
+			vp.NextMonthPrice = fmt.Sprint(varietyNextMonthData[v.VarietyId].CloseValue)
+		}
+		varietyPrice = append(varietyPrice, vp)
+
+		// 观点评价
+		fc := new(models.AssessmentFormForecastComment)
+		fc.VarietyId = v.VarietyId
+		fc.VarietyCode = v.VarietyCode
+		fc.VarietyName = v.VarietyName
+		fc.WeekTime = v.WeekTime
+		if !v.SubmitTime.IsZero() {
+			fc.SubmitTime = v.SubmitTime.Format(utils.FormatDateUnSpace)
+		}
+
+		// 月度涨跌评价
+		if baseDataPrice != nil && nextMonthPrice != nil {
+			_, tips, right := calculateMonthlyPriceTrend(*baseDataPrice, *nextMonthPrice, vat.MonthlyFluctuate, v.MonthlyPriceForecast)
+			fc.MonthlyPriceComment = tips
+			fc.MonthlyPriceForecastRight = right
+		}
+
+		// 周度上下行风险
+		if baseDataPrice != nil && nextWeekHighPrice != nil {
+			_, tips, right := calculateWeekUpDownTrend(*baseDataPrice, *nextWeekHighPrice, vat.WeeklyFluctuate, v.WeeklyUpForecast, true)
+			fc.WeeklyUpComment = tips
+			fc.WeeklyUpForecastRight = right
+		}
+		if baseDataPrice != nil && nextWeekLowPrice != nil {
+			_, tips, right := calculateWeekUpDownTrend(*baseDataPrice, *nextWeekLowPrice, vat.WeeklyFluctuate, v.WeeklyUpForecast, false)
+			fc.WeeklyDownComment = tips
+			fc.WeeklyDownForecastRight = right
+		}
+		forecastComment = append(forecastComment, fc)
+	}
+	return
+}
+
+// calculateMonthly 判断月度涨跌趋势
+func calculateMonthlyPriceTrend(basePrice, monthPrice, monthlyFluctuate float64, forecast string) (result, tips string, right bool) {
+	if basePrice <= 0 || monthPrice <= 0 || monthlyFluctuate <= 0 {
+		return
+	}
+	// 计算月度价格变化比例:月度价格/当周价格-1
+	percent := (monthPrice/basePrice - 1) * 100
+
+	// 判断价格趋势
+	switch {
+	case percent > monthlyFluctuate:
+		result = models.AssessmentFormMonthlyPriceUp
+	case percent < -monthlyFluctuate:
+		result = models.AssessmentFormMonthlyPriceDown
+	default:
+		result = models.AssessmentFormMonthlyPriceShake
+	}
+
+	// 如果有进行预测,判断是否正确,返回提示语句
+	if forecast == "" {
+		return
+	}
+	tips = fmt.Sprintf("判断 %s,实际 %s", forecast, result)
+	if forecast == result {
+		right = true
+	}
+	return
+}
+
+// calculateWeekUpDownTrend 判断周度上下行风险
+func calculateWeekUpDownTrend(basePrice, weekPrice, weekFluctuate float64, forecast string, calculateUp bool) (result, tips string, right bool) {
+	if basePrice <= 0 || weekPrice <= 0 || weekFluctuate <= 0 {
+		return
+	}
+	percent := (weekPrice/basePrice - 1) * 100
+
+	// 上下行
+	result = models.AssessmentFormWeekUpNo
+	if calculateUp {
+		if percent > weekFluctuate {
+			result = models.AssessmentFormWeekUpYes
+		}
+	} else {
+		if percent < -weekFluctuate {
+			result = models.AssessmentFormWeekUpYes
+		}
+	}
+
+	// 如果有进行预测,判断是否正确,返回提示语句
+	if forecast == "" {
+		return
+	}
+	if forecast == result {
+		right = true
+	}
+	forecastMap := map[string]string{models.AssessmentFormWeekUpYes: "提示风险", models.AssessmentFormWeekUpNo: "未提示风险"}
+	resultMap := map[string]string{models.AssessmentFormWeekUpYes: "风险发生", models.AssessmentFormWeekUpNo: "风险未发生"}
+	tips = fmt.Sprint(forecastMap[forecast], " ", resultMap[result])
+	return
+}
+
+// CalculateResultStatistic 计算正确率
+func CalculateResultStatistic(forms []*models.AssessmentForm, varietyData []*models.AssessmentVarietyData, results []*models.AssessmentFormResultStatisticItem) (resp []*models.AssessmentFormResultStatisticItem, err error) {
+	if len(forms) == 0 {
+		return
+	}
+	calculateMappingQ := make(map[string]int) // 月度趋势分子Q(即月度涨跌趋势判断正确的次数)
+	calculateMappingP := make(map[string]int) // 月度趋势分母P(即月度涨跌趋势判断总次数)
+	calculateMappingT := make(map[string]int) // 周度风险分子T(即周度上下行风险判断正确的次数)
+	calculateMappingS := make(map[string]int) // 周度风险分母S(即周度上下行风险判断的总次数)
+
+	// 品种数据
+	varietyDateData := make(map[string]*models.AssessmentVarietyData)
+	for _, v := range varietyData {
+		k := fmt.Sprintf("%d-%s", v.VarietyId, v.WeekDate.Format(utils.FormatDate))
+		varietyDateData[k] = v
+	}
+
+	// 获取品种信息
+	varietyMatch := make(map[int]*models.AssessmentVariety)
+	{
+		varietyOb := new(models.AssessmentVariety)
+		list, e := varietyOb.GetItemsByCondition(``, make([]interface{}, 0), []string{}, "")
+		if e != nil {
+			err = fmt.Errorf("获取品种信息失败, %v", e)
+			return
+		}
+		for _, v := range list {
+			varietyMatch[v.AssessmentVarietyId] = v
+		}
+	}
+
+	// 对填报单的[研究员ID-品种ID]进行QPTS的计数
+	for _, v := range forms {
+		vat := varietyMatch[v.VarietyId]
+		if vat == nil {
+			utils.FileLog.Info(fmt.Sprintf("CalculateResultStatistic, 品种不存在: %d", v.VarietyId))
+			continue
+		}
+		key := fmt.Sprintf("%d-%d", v.ResearcherId, v.VarietyId)
+
+		// 找出填报单品种对应的基准日期数据、N+1周的最高最低价格、N+4周的价格
+		var (
+			baseDataPrice     *float64
+			nextMonthPrice    *float64
+			nextWeekHighPrice *float64
+			nextWeekLowPrice  *float64
+		)
+		kb := fmt.Sprintf("%d-%s", v.VarietyId, v.BaseDate.Format(utils.FormatDate))
+		if varietyDateData[kb] != nil {
+			baseDataPrice = &varietyDateData[kb].CloseValue
+		}
+		kw := fmt.Sprintf("%d-%s", v.VarietyId, v.BaseDate.AddDate(0, 0, 7).Format(utils.FormatDate))
+		if varietyDateData[kw] != nil {
+			nextWeekHighPrice = &varietyDateData[kw].HighValue
+			nextWeekLowPrice = &varietyDateData[kw].LowValue
+		}
+		km := fmt.Sprintf("%d-%s", v.VarietyId, v.BaseDate.AddDate(0, 0, 28).Format(utils.FormatDate))
+		if varietyDateData[km] != nil {
+			nextMonthPrice = &varietyDateData[km].CloseValue
+		}
+
+		// 月度涨跌评价
+		if baseDataPrice != nil && nextMonthPrice != nil {
+			_, _, right := calculateMonthlyPriceTrend(*baseDataPrice, *nextMonthPrice, vat.MonthlyFluctuate, v.MonthlyPriceForecast)
+			calculateMappingP[key] += 1
+			if right {
+				calculateMappingQ[key] += 1
+			}
+		}
+
+		// 周度上下行风险
+		if baseDataPrice != nil && nextWeekHighPrice != nil {
+			_, _, right := calculateWeekUpDownTrend(*baseDataPrice, *nextWeekHighPrice, vat.WeeklyFluctuate, v.WeeklyUpForecast, true)
+			calculateMappingS[key] += 1
+			if right {
+				calculateMappingT[key] += 1
+			}
+		}
+		if baseDataPrice != nil && nextWeekLowPrice != nil {
+			_, _, right := calculateWeekUpDownTrend(*baseDataPrice, *nextWeekLowPrice, vat.WeeklyFluctuate, v.WeeklyUpForecast, false)
+			calculateMappingS[key] += 1
+			if right {
+				calculateMappingT[key] += 1
+			}
+		}
+	}
+
+	// 计算正确率,结果取整
+	for _, v := range results {
+		k := fmt.Sprintf("%d-%d", v.ResearcherId, v.VarietyId)
+		// 月趋势正确率:Q/P
+		q := calculateMappingQ[k]
+		p := calculateMappingP[k]
+		if p > 0 {
+			v.MonthlyTrendAccuracy = math.Round(float64(q) / float64(p) * 100)
+		}
+
+		// 周度预警正确率:T/S
+		t := calculateMappingT[k]
+		s := calculateMappingS[k]
+		if s > 0 {
+			v.WeeklyWarningAccuracy = math.Round(float64(t) / float64(s) * 100)
+		}
+
+		// 综合正确率1:(Q+2T)/(P+2S)
+		a := q + 2*t
+		b := p + 2*s
+		if b > 0 {
+			v.TotalAccuracyA = math.Round(float64(a) / float64(b) * 100)
+		}
+
+		// 综合正确率2:Q/2P+T/2S
+		var c, d float64
+		if p > 0 {
+			c = math.Round(float64(q) / float64(2*p) * 100)
+		}
+		if s > 0 {
+			d = math.Round(float64(t) / float64(2*s) * 100)
+		}
+		v.TotalAccuracyB = c + d
+	}
+
+	resp = results
+	return
+}
+
+// SyncAssessmentResearcher 同步更新研究员表冗余字段
+func SyncAssessmentResearcher(adminId int) (err error) {
+	defer func() {
+		if err != nil {
+			tips := fmt.Sprintf("SyncAssessmentResearcher, 同步研究员表姓名/状态失败: %v", err)
+			utils.FileLog.Info(tips)
+			alarm_msg.SendAlarmMsg(tips, 3)
+		}
+	}()
+
+	// 查询是否有对应的研究员
+	researcherOb := new(models.AssessmentResearcher)
+	cond := fmt.Sprintf(` AND %s = ?`, researcherOb.Cols().AdminId)
+	pars := make([]interface{}, 0)
+	pars = append(pars, adminId)
+	researcher, e := researcherOb.GetItemByCondition(cond, pars, "")
+	if e != nil && !utils.IsErrNoRow(e) {
+		err = fmt.Errorf("获取研究员失败, %v", e)
+		return
+	}
+	if researcher == nil || researcher.AssessmentResearcherId <= 0 {
+		return
+	}
+	// 只有研究员状态是启用状态时,才同步系统用户状态
+	var updateEnabled bool
+	if researcher.Enabled == models.AssessmentResearcherEnabled {
+		updateEnabled = true
+	}
+
+	// 获取系统用户,如果不存在,那么禁用研究员
+	adminItem, e := system.GetSysAdminById(adminId)
+	if e != nil && !utils.IsErrNoRow(e) {
+		err = fmt.Errorf("获取系统用户失败, %v", e)
+		return
+	}
+	var updateCols []string
+	var updateRealName bool
+	if adminItem != nil && adminItem.AdminId > 0 {
+		researcher.RealName = adminItem.RealName
+		researcher.Enabled = adminItem.Enabled
+		researcher.ModifyTime = time.Now().Local()
+		updateCols = append(updateCols, researcherOb.Cols().RealName, researcherOb.Cols().ModifyTime)
+		updateRealName = true
+	} else {
+		researcher.Enabled = models.AssessmentResearcherDisabled
+		researcher.ModifyTime = time.Now().Local()
+		updateCols = append(updateCols, researcherOb.Cols().ModifyTime)
+	}
+	if updateEnabled {
+		updateCols = append(updateCols, researcherOb.Cols().Enabled)
+	}
+	if e = researcher.Update(updateCols); e != nil {
+		err = fmt.Errorf("同步研究员失败, %v", e)
+		return
+	}
+
+	// 更新填报单研究员姓名
+	if !updateRealName {
+		return
+	}
+	formOb := new(models.AssessmentForm)
+	if e = formOb.UpdateResearcherName(researcher.AssessmentResearcherId, researcher.RealName); e != nil {
+		err = fmt.Errorf("更新填报单研究员姓名失败, %v", e)
+		return
+	}
+	return
+}

+ 23 - 0
utils/common.go

@@ -3150,3 +3150,26 @@ func GetThsDsIndexCodeSuffix(period, days string) (suffix string) {
 	}
 	return
 }
+
+// GetWeekRange 获取标准周的开始和结束时间(周一到周日)
+// 返回:本周开始时间(周一00:00:00),本周结束时间(周日23:59:59)
+func GetWeekRange(now time.Time) (startOfWeek, endOfWeek time.Time) {
+	// 计算到本周一的天数差
+	weekday := now.Weekday()
+	if weekday == time.Sunday {
+		weekday = 7 // 将周日视为7(ISO标准)
+	}
+	daysToMonday := int(time.Monday) - int(weekday)
+
+	// 计算本周一00:00:00
+	startOfWeek = now.AddDate(0, 0, daysToMonday)
+	startOfWeek = time.Date(
+		startOfWeek.Year(), startOfWeek.Month(), startOfWeek.Day(),
+		0, 0, 0, 0, startOfWeek.Location(),
+	)
+
+	// 计算本周日23:59:59
+	endOfWeek = startOfWeek.AddDate(0, 0, 6).Add(23*time.Hour + 59*time.Minute + 59*time.Second)
+
+	return startOfWeek, endOfWeek
+}