ソースを参照

add:新增文件管理及报告导入

zoie 1 ヶ月 前
コミット
4c4cb7dafd

+ 5 - 5
controllers/Account.go

@@ -353,11 +353,11 @@ func (c *AccountController) List_All() {
 		return
 	}
 
-	if User_r.T_power > 2 {
-		c.Data["json"] = lib.JSONS{Code: 202, Msg: "无权操作!"}
-		c.ServeJSON()
-		return
-	}
+	//if User_r.T_power > 2 {
+	//	c.Data["json"] = lib.JSONS{Code: 202, Msg: "无权操作!"}
+	//	c.ServeJSON()
+	//	return
+	//}
 	PowerList := Account.Read_Power_List_ALL_1()
 	PowerMap := Account.UserPowerListToPowerMap(PowerList)
 

+ 673 - 20
controllers/Task.go

@@ -13,20 +13,25 @@ import (
 	"ColdVerify_server/models/System"
 	"ColdVerify_server/models/Task"
 	"ColdVerify_server/models/VerifyTemplate"
+	"archive/zip"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"image/color"
+	"io"
 	"math"
 	"os"
 	"os/exec"
+	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 	"time"
 
+	"github.com/xuri/excelize/v2"
+
 	beego "github.com/beego/beego/v2/server/web"
 	"github.com/google/uuid"
-	"github.com/xuri/excelize/v2"
 	"gonum.org/v1/plot"
 	"gonum.org/v1/plot/plotter"
 	"gonum.org/v1/plot/vg"
@@ -39,7 +44,7 @@ type TaskController struct {
 
 // 导入Excel创建任务并写入模版数据
 func (c *TaskController) Import_Tasks() {
-	file, _, err := c.GetFile("file")
+	file, fileHeader, err := c.GetFile("file")
 	if err != nil {
 		c.Data["json"] = lib.JSONS{Code: 203, Msg: "读取上传文件失败!"}
 		c.ServeJSON()
@@ -47,19 +52,31 @@ func (c *TaskController) Import_Tasks() {
 	}
 	defer file.Close()
 
-	xlsx, err := excelize.OpenReader(file)
+	// 保存上传的文件到临时位置,以便后续提取图片
+	lib.Create_Dir("./ofile/temp_excel")
+	tempExcelPath := fmt.Sprintf("./ofile/temp_excel/%d_%s", time.Now().Unix(), fileHeader.Filename)
+	err = c.SaveToFile("file", tempExcelPath)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 203, Msg: "保存临时文件失败!"}
+		c.ServeJSON()
+		return
+	}
+	defer os.Remove(tempExcelPath) // 处理完成后删除临时文件
+
+	// 重新打开文件用于读取数据(必须使用文件路径,GetPicture 等功能依赖于完整文件)
+	f, err := excelize.OpenFile(tempExcelPath)
 	if err != nil {
 		c.Data["json"] = lib.JSONS{Code: 203, Msg: "Excel 文件解析失败!"}
 		c.ServeJSON()
 		return
 	}
 	defer func() {
-		_ = xlsx.Close()
+		_ = f.Close()
 	}()
 
 	sheetName := c.GetString("sheet")
 	if len(sheetName) == 0 {
-		sheets := xlsx.GetSheetList()
+		sheets := f.GetSheetList()
 		if len(sheets) == 0 {
 			c.Data["json"] = lib.JSONS{Code: 203, Msg: "Excel 文件为空!"}
 			c.ServeJSON()
@@ -68,7 +85,7 @@ func (c *TaskController) Import_Tasks() {
 		sheetName = sheets[0]
 	}
 
-	rows, err := xlsx.GetRows(sheetName)
+	rows, err := f.GetRows(sheetName)
 	if err != nil {
 		c.Data["json"] = lib.JSONS{Code: 203, Msg: "读取 Excel 内容失败!"}
 		c.ServeJSON()
@@ -85,6 +102,18 @@ func (c *TaskController) Import_Tasks() {
 	skippedRows := make([]int, 0)
 	failedRows := make([]string, 0)
 
+	// 从 Excel 文件中提取所有图片,建立 DISPIMG ID 到图片数据的映射
+	// pictureMap key格式: DISPIMG 中的 ID (例如 "ID_53A3119AC46541C694965B07E5E00F81")
+	pictureMap, err := c.extractPicturesFromExcel(tempExcelPath)
+	if err != nil {
+		logs.Warning(fmt.Sprintf("提取 Excel 图片失败: %v,将继续处理数据", err))
+		pictureMap = make(map[string][]byte)
+	}
+	logs.Info(fmt.Sprintf("共提取到 %d 个图片(ID映射)", len(pictureMap)))
+
+	// 缓存 InfoCollection,避免同一家公司重复查询或创建
+	infoCollectionCache := make(map[string]InfoCollection.InfoCollection)
+
 	for idx := 1; idx < len(rows); idx++ {
 		rowCells := rows[idx]
 		rowData := make(map[string]string, len(headers))
@@ -101,23 +130,62 @@ func (c *TaskController) Import_Tasks() {
 			if len(value) > 0 {
 				nonEmpty = true
 			}
+
+			// 处理单元格内是图片的情况:只有value包含DISPIMG时才处理
+			if strings.Contains(value, "DISPIMG") {
+				logs.Info(fmt.Sprintf("单元格 %d:%d 包含DISPIMG,可能是图片: %s", idx+1, colIdx+1, value))
+
+				// 从 DISPIMG 公式中提取 ID
+				// 格式: =DISPIMG("ID_53A3119AC46541C694965B07E5E00F81",1) 或 DISPIMG("ID_...",1)
+				var picData []byte
+				var foundBy string
+
+				// 通过 ID 查找图片(pictureMap 现在使用 DISPIMG ID 作为 key)
+				dispimgIdRe := regexp.MustCompile(`DISPIMG\s*\(\s*"([^"]+)"`)
+				idMatches := dispimgIdRe.FindStringSubmatch(value)
+				if len(idMatches) > 1 {
+					imageId := idMatches[1]
+					logs.Info(fmt.Sprintf("从 DISPIMG 公式中提取到 ID: %s", imageId))
+					if data, exists := pictureMap[imageId]; exists && len(data) > 0 {
+						picData = data
+						foundBy = fmt.Sprintf("ID (%s)", imageId)
+						logs.Info(fmt.Sprintf("通过 ID %s 找到图片数据 (大小: %d 字节)", imageId, len(picData)))
+					} else {
+						logs.Warning(fmt.Sprintf("通过 ID %s 未找到图片数据,pictureMap 中有 %d 个图片", imageId, len(pictureMap)))
+					}
+				}
+
+				// 如果找到了图片数据,上传图片
+				if picData != nil && len(picData) > 0 {
+					imageURL, uploadErr := c.uploadImageData(picData, header)
+					if uploadErr == nil && len(imageURL) > 0 {
+						value = imageURL
+						nonEmpty = true
+						logs.Info(fmt.Sprintf("第%d行 %s 图片上传成功 (通过%s): %s", idx+1, header, foundBy, imageURL))
+						logs.Info(fmt.Sprintf("第%d行 %s 图片上传成功 (通过%s): %s", idx+1, header, foundBy, imageURL))
+					} else if uploadErr != nil {
+						logs.Error(fmt.Sprintf("第%d行 %s 上传单元格图片失败: %v", idx+1, header, uploadErr))
+					}
+				} else {
+					logs.Warning(fmt.Sprintf("第%d行 %s 包含DISPIMG但未找到对应图片数据", idx+1, header))
+				}
+			}
+
 			rowData[header] = value
 		}
 		if !nonEmpty {
 			continue
 		}
+
 		companyName := strings.TrimSpace(rowData["公司名称"])
 		taskName := strings.TrimSpace(rowData["任务名称"])
-		if len(companyName) == 0 || len(taskName) == 0 {
-			skippedRows = append(skippedRows, idx+1)
-			continue
-		}
-		if rowData["布点图片"] == "" {
+		belongYear := strings.TrimSpace(rowData["所属年份"])
+		if len(companyName) == 0 || len(taskName) == 0 || len(belongYear) == 0 {
 			skippedRows = append(skippedRows, idx+1)
 			continue
 		}
 
-		taskID, err := c.importTaskRow(rowData)
+		taskID, err := c.importTaskRow(rowData, infoCollectionCache)
 		if err != nil {
 			failedRows = append(failedRows, fmt.Sprintf("第%d行: %v", idx+1, err))
 			continue
@@ -138,8 +206,8 @@ func (c *TaskController) Import_Tasks() {
 	c.ServeJSON()
 }
 
-func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
-	templateName := strings.TrimSpace(row["模名称"])
+func (c *TaskController) importTaskRow(row map[string]string, infoCollectionCache map[string]InfoCollection.InfoCollection) (string, error) {
+	templateName := strings.TrimSpace(row["模名称"])
 	lastOpenBracket := strings.LastIndex(templateName, "[")
 	lastCloseBracket := strings.LastIndex(templateName, "]")
 	if lastOpenBracket == -1 || lastCloseBracket == -1 || lastOpenBracket >= lastCloseBracket {
@@ -197,6 +265,22 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 		user = cachedUser
 	}
 
+	// 根据用户信息获取或创建 InfoCollection,并使用 map 做本次导入缓存,避免重复查询
+	cacheKey := user.T_uuid
+	if len(cacheKey) == 0 {
+		cacheKey = user.T_name
+	}
+
+	infoCollectionRecord, ok := infoCollectionCache[cacheKey]
+	if !ok {
+		record, err := InfoCollection.GetOrCreate_InfoCollection(user.T_uuid, user.T_name)
+		if err != nil {
+			return "", fmt.Errorf("自动创建信息采集失败: %w", err)
+		}
+		infoCollectionRecord = record
+		infoCollectionCache[cacheKey] = infoCollectionRecord
+	}
+
 	resolveAdminUUID := func(fieldName, fallback string, strict bool) (string, error) {
 		value, ok := row[fieldName]
 		if !ok || len(strings.TrimSpace(value)) == 0 {
@@ -217,7 +301,6 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 	if len(taskName) == 0 {
 		return "", fmt.Errorf("任务名称缺失")
 	}
-	snValue := strings.TrimSpace(row["SN"])
 
 	classPath := ""
 	if vt.T_class > 0 {
@@ -287,13 +370,17 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 		verifyTypeValue = verifyTypeFallback
 	}
 
+	belongYearValue := strings.TrimSpace(row["所属年份"])
+
+	infoCollectionID := infoCollectionRecord.T_InfoCollection_id
+
 	taskRecord := Task.Task{
 		T_Distributor_id:       user.T_Distributor_id,
 		T_uuid:                 user.T_uuid,
 		T_name:                 taskName,
 		T_VerifyTemplate_id:    templateID,
 		T_VerifyTemplate_class: classPath,
-		T_sn:                   snValue,
+		T_InfoCollection_id:    infoCollectionID,
 		T_uid:                  taskUID,
 		T_scheme:               schemeUUID,
 		T_collection:           collectionUUID,
@@ -303,6 +390,7 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 		T_category:             categoryValue,
 		T_device_type:          deviceTypeValue,
 		T_verify_type:          verifyTypeValue,
+		T_belong_year:          belongYearValue,
 	}
 
 	var taskID string
@@ -310,7 +398,7 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 		taskRecord.Id = existingTask.Id
 		taskRecord.T_task_id = existingTask.T_task_id
 		taskRecord.T_State = existingTask.T_State
-		if ok := Task.Update_Task(taskRecord, "T_Distributor_id", "T_uuid", "T_name", "T_VerifyTemplate_id", "T_VerifyTemplate_class", "T_sn", "T_uid", "T_scheme", "T_collection", "T_reporting", "T_delivery", "T_project", "T_category", "T_device_type", "T_verify_type"); !ok {
+		if ok := Task.Update_Task(taskRecord, "T_Distributor_id", "T_uuid", "T_name", "T_VerifyTemplate_id", "T_VerifyTemplate_class", "T_uid", "T_scheme", "T_collection", "T_reporting", "T_delivery", "T_project", "T_category", "T_device_type", "T_verify_type", "T_belong_year"); !ok {
 			return "", fmt.Errorf("更新任务失败")
 		}
 		taskID = existingTask.T_task_id
@@ -350,6 +438,7 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 	dataList := make([]VerifyTemplate.VerifyTemplateMapData, 0, len(row))
 	for key, val := range row {
 		if vtm, exists := labelMap[key]; exists {
+			logs.Println("key:", key, "val:", val)
 			dataList = append(dataList, VerifyTemplate.VerifyTemplateMapData{
 				T_source:               vtm.T_source,
 				T_task_id:              taskID,
@@ -373,6 +462,250 @@ func (c *TaskController) importTaskRow(row map[string]string) (string, error) {
 
 	return taskID, nil
 }
+func (c *TaskController) extractPicturesFromExcel(filePath string) (map[string][]byte, error) {
+	filePath = strings.TrimSpace(filePath)
+	if len(filePath) == 0 {
+		return nil, fmt.Errorf("Excel 文件路径为空")
+	}
+
+	zipReader, err := zip.OpenReader(filePath)
+	if err != nil {
+		return nil, fmt.Errorf("打开 Excel 压缩包失败: %w", err)
+	}
+	defer zipReader.Close()
+
+	return c.extractWpsDispImgPictures(zipReader), nil
+}
+
+// extractWpsDispImgPictures 专门处理 WPS / _xlfn.DISPIMG 的图片提取逻辑
+// 原理参考: `https://www.hushlll.top/?p=644`
+// 1. 读取 xl/cellimages.xml,找到每个图片块中的 name="ID_xxx" 和 r:embed="rIdX"
+// 2. 读取 xl/_rels/cellimages.xml.rels(或退回到 xl/drawings/_rels/drawing1.xml.rels),把 rId 映射到具体的图片路径(如 media/image1.jpg)
+// 3. 从 ZIP 中按路径取出图片数据,建立 ID -> 图片数据 的映射
+func (c *TaskController) extractWpsDispImgPictures(zipReader *zip.ReadCloser) map[string][]byte {
+	result := make(map[string][]byte)
+
+	if zipReader == nil || zipReader.File == nil {
+		return result
+	}
+
+	// 1. 找到 xl/cellimages.xml
+	var cellImagesFile *zip.File
+	for _, f := range zipReader.File {
+		if f != nil && f.Name == "xl/cellimages.xml" {
+			cellImagesFile = f
+			break
+		}
+	}
+	if cellImagesFile == nil {
+		// 非 WPS / 无 cellimages.xml 的情况,直接返回空
+		return result
+	}
+
+	rc, err := cellImagesFile.Open()
+	if err != nil {
+		logs.Warning(fmt.Sprintf("无法打开 xl/cellimages.xml: %v", err))
+		return result
+	}
+	cellImagesData, err := io.ReadAll(rc)
+	rc.Close()
+	if err != nil {
+		logs.Warning(fmt.Sprintf("无法读取 xl/cellimages.xml: %v", err))
+		return result
+	}
+	cellImagesContent := string(cellImagesData)
+
+	// 2. 读取关系文件:优先 xl/_rels/cellimages.xml.rels,找不到再尝试 xl/drawings/_rels/drawing1.xml.rels
+	var relsFile *zip.File
+	for _, f := range zipReader.File {
+		if f != nil && f.Name == "xl/_rels/cellimages.xml.rels" {
+			relsFile = f
+			break
+		}
+	}
+	if relsFile == nil {
+		for _, f := range zipReader.File {
+			if f != nil && f.Name == "xl/drawings/_rels/drawing1.xml.rels" {
+				relsFile = f
+				break
+			}
+		}
+	}
+	if relsFile == nil {
+		logs.Warning("未找到 cellimages 对应的关系文件 (cellimages.xml.rels / drawing1.xml.rels)")
+		return result
+	}
+
+	rc, err = relsFile.Open()
+	if err != nil {
+		logs.Warning(fmt.Sprintf("无法打开关系文件 %s: %v", relsFile.Name, err))
+		return result
+	}
+	relsData, err := io.ReadAll(rc)
+	rc.Close()
+	if err != nil {
+		logs.Warning(fmt.Sprintf("无法读取关系文件 %s: %v", relsFile.Name, err))
+		return result
+	}
+	relsContent := string(relsData)
+
+	// 2.1 解析关系文件,建立 rId -> Target 的映射
+	relsMap := make(map[string]string)
+	reRel := regexp.MustCompile(`Id="([^"]+)"[^>]*Target="([^"]+)"`)
+	for _, m := range reRel.FindAllStringSubmatch(relsContent, -1) {
+		if len(m) >= 3 {
+			id := m[1]
+			target := m[2]
+			relsMap[id] = target
+		}
+	}
+	if len(relsMap) == 0 {
+		logs.Warning(fmt.Sprintf("关系文件 %s 中未解析到任何图片关系", relsFile.Name))
+		return result
+	}
+
+	// 3. 从 cellimages.xml 中解析每个图片块
+	// 结构大致类似:
+	// <xdr:pic> ... <xdr:nvPicPr>...<xdr:cNvPr name="ID_XXXX".../> ...</xdr:nvPicPr> ... <a:blip r:embed="rId1"/> ... </xdr:pic>
+	picBlocks := regexp.MustCompile(`(?s)<xdr:pic[^>]*>.*?</xdr:pic>`).FindAllString(cellImagesContent, -1)
+	if len(picBlocks) == 0 {
+		logs.Warning("xl/cellimages.xml 中未找到任何 <xdr:pic> 图片块")
+		return result
+	}
+
+	nameRe := regexp.MustCompile(`name="(ID_[^"]+)"`)
+	embedRe := regexp.MustCompile(`r:embed="([^"]+)"`)
+
+	for _, block := range picBlocks {
+		nameMatch := nameRe.FindStringSubmatch(block)
+		if len(nameMatch) < 2 {
+			continue
+		}
+		imgID := nameMatch[1]
+
+		embedMatch := embedRe.FindStringSubmatch(block)
+		if len(embedMatch) < 2 {
+			logs.Warning(fmt.Sprintf("WPS 图片块中 ID %s 未找到 r:embed 属性", imgID))
+			continue
+		}
+		rId := embedMatch[1]
+
+		target, ok := relsMap[rId]
+		if !ok {
+			logs.Warning(fmt.Sprintf("在关系文件中未找到 rId=%s 对应的图片路径 (ID=%s)", rId, imgID))
+			continue
+		}
+
+		// 关系文件里的 Target 一般是 media/image1.jpg 或 ../media/image1.jpg
+		// 在 ZIP 中的真实路径应为 xl/<Target 去掉前导 / >
+		imagePathInZip := "xl/" + strings.TrimLeft(target, "/")
+
+		var imageFile *zip.File
+		for _, f := range zipReader.File {
+			if f != nil && f.Name == imagePathInZip {
+				imageFile = f
+				break
+			}
+		}
+		if imageFile == nil {
+			logs.Warning(fmt.Sprintf("在 ZIP 中未找到图片文件 %s (ID=%s, rId=%s)", imagePathInZip, imgID, rId))
+			continue
+		}
+
+		rcImg, err := imageFile.Open()
+		if err != nil {
+			logs.Warning(fmt.Sprintf("无法打开图片文件 %s: %v", imagePathInZip, err))
+			continue
+		}
+		imgData, err := io.ReadAll(rcImg)
+		rcImg.Close()
+		if err != nil {
+			logs.Warning(fmt.Sprintf("无法读取图片文件 %s: %v", imagePathInZip, err))
+			continue
+		}
+		if len(imgData) == 0 {
+			logs.Warning(fmt.Sprintf("图片文件 %s 为空,跳过 (ID=%s)", imagePathInZip, imgID))
+			continue
+		}
+
+		result[imgID] = imgData
+		logs.Info(fmt.Sprintf("WPS cellimages 建立映射: ID %s -> %s (大小: %d 字节)", imgID, imagePathInZip, len(imgData)))
+	}
+
+	logs.Info(fmt.Sprintf("通过 WPS cellimages 共解析出 %d 个 ID->图片 映射", len(result)))
+	return result
+}
+
+// 上传图片数据到七牛云
+func (c *TaskController) uploadImageData(imageData []byte, headerName string) (string, error) {
+	if len(imageData) == 0 {
+		return "", fmt.Errorf("图片数据为空")
+	}
+
+	// 创建临时目录
+	tempDir := "./ofile/temp_images"
+	err := os.MkdirAll(tempDir, 0755)
+	if err != nil {
+		return "", fmt.Errorf("创建临时目录失败: %w", err)
+	}
+
+	// 根据图片数据的前几个字节判断图片格式
+	ext := ".png" // 默认扩展名
+	if len(imageData) >= 4 {
+		// PNG: 89 50 4E 47
+		if imageData[0] == 0x89 && imageData[1] == 0x50 && imageData[2] == 0x4E && imageData[3] == 0x47 {
+			ext = ".png"
+		} else if len(imageData) >= 2 {
+			// JPEG: FF D8
+			if imageData[0] == 0xFF && imageData[1] == 0xD8 {
+				ext = ".jpg"
+			} else if len(imageData) >= 6 {
+				// GIF: 47 49 46 38
+				if imageData[0] == 0x47 && imageData[1] == 0x49 && imageData[2] == 0x46 && imageData[3] == 0x38 {
+					ext = ".gif"
+				}
+			}
+		}
+	}
+
+	fileName := fmt.Sprintf("%s_%s_%d%s", headerName, uuid.New().String(), time.Now().Unix(), ext)
+	tempFilePath := filepath.Join(tempDir, fileName)
+
+	// 保存图片到临时文件
+	err = os.WriteFile(tempFilePath, imageData, 0644)
+	if err != nil {
+		return "", fmt.Errorf("保存图片失败: %w", err)
+	}
+	defer func() {
+		// 确保清理临时文件
+		if removeErr := os.Remove(tempFilePath); removeErr != nil {
+			logs.Error(fmt.Sprintf("删除临时文件失败: %s, 错误: %v", tempFilePath, removeErr))
+		}
+	}()
+
+	// 检查文件是否存在
+	fileInfo, err := os.Stat(tempFilePath)
+	if err != nil {
+		return "", fmt.Errorf("获取文件信息失败: %w", err)
+	}
+
+	// 上传到七牛云
+	qiniuFileName := fmt.Sprintf("UpImage/excel_import_%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
+	logs.Info(fmt.Sprintf("准备上传图片: %s, 大小: %d 字节, 目标: %s", tempFilePath, fileInfo.Size(), qiniuFileName))
+
+	imageURL, success := NatsServer.Qiniu_UploadFile(tempFilePath, qiniuFileName)
+	if !success {
+		logs.Error(fmt.Sprintf("上传图片到七牛云失败: 文件=%s, 大小=%d 字节, 目标=%s", tempFilePath, fileInfo.Size(), qiniuFileName))
+		return "", fmt.Errorf("上传图片到七牛云失败,文件大小: %d 字节", fileInfo.Size())
+	}
+
+	if imageURL == "" {
+		return "", fmt.Errorf("上传图片后未返回URL")
+	}
+
+	logs.Info(fmt.Sprintf("图片上传成功: %s -> %s", tempFilePath, imageURL))
+	return imageURL, nil
+}
 
 // 列表 -
 func (c *TaskController) List() {
@@ -854,6 +1187,92 @@ func (c *TaskController) Stat_Excel() {
 	return
 }
 
+// 导出唯一任务标识
+func (c *TaskController) ExportTaskUid() {
+	// 验证登录
+	_, User_is := Account.Verification_Admin(c.Ctx.GetCookie("User_tokey"), c.GetString("User_tokey"))
+	if !User_is {
+		c.Data["json"] = lib.JSONS{Code: 201, Msg: "请重新登录!"}
+		c.ServeJSON()
+		return
+	}
+
+	T_num, _ := c.GetInt("T_num")
+	if T_num <= 0 {
+		T_num = 10
+	}
+	if T_num > 1000 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "单次最多生成1000个唯一标识"}
+		c.ServeJSON()
+		return
+	}
+
+	uidList := make([]string, 0, T_num)
+	exists := make(map[string]struct{}, T_num)
+	exportBatch := fmt.Sprintf("export_%d", time.Now().UnixNano())
+
+	for len(uidList) < T_num {
+		candidate := strings.ToUpper(strings.ReplaceAll(uuid.New().String(), "-", ""))
+		if len(candidate) > 16 {
+			candidate = candidate[:16]
+		}
+
+		if _, ok := exists[candidate]; ok {
+			continue
+		}
+		if _, found := Task.Read_Task_ByUid(candidate); found {
+			continue
+		}
+		if Task.TaskUidExists(candidate) {
+			continue
+		}
+		exists[candidate] = struct{}{}
+		uidList = append(uidList, candidate)
+	}
+
+	f := excelize.NewFile()
+	sheet := "Sheet1"
+	f.SetCellValue(sheet, "A1", "序号")
+	f.SetCellValue(sheet, "B1", "唯一标识")
+	for i, uid := range uidList {
+		row := i + 2
+		f.SetCellValue(sheet, fmt.Sprintf("A%d", row), i+1)
+		f.SetCellValue(sheet, fmt.Sprintf("B%d", row), uid)
+	}
+	f.SetColWidth(sheet, "A", "A", 12)
+	f.SetColWidth(sheet, "B", "B", 30)
+
+	lib.Create_Dir("./ofile")
+	fileName := fmt.Sprintf("task_uid_%d.xlsx", time.Now().Unix())
+	localPath := "ofile/" + fileName
+	if err := f.SaveAs(localPath); err != nil {
+		logs.Error("ExportTaskUid SaveAs error:", err)
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "生成Excel失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	if !lib.Pload_qiniu(localPath, localPath) {
+		c.Data["json"] = lib.JSONS{Code: 203, Msg: "文件上传失败!"}
+		c.ServeJSON()
+		return
+	}
+	if err := os.Remove(localPath); err != nil {
+		logs.Error("ExportTaskUid remove temp file:", err)
+	}
+
+	if err := Task.SaveTaskUids(uidList, exportBatch); err != nil {
+		c.Data["json"] = lib.JSONS{Code: 204, Msg: "保存唯一标识失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	url := "https://bzdcoldverifyoss.baozhida.cn/" + localPath
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: url}
+	c.ServeJSON()
+	return
+}
+
 // 获取任务负责人列表
 func (c *TaskController) GetTaskUserList() {
 	// 验证登录 User_is, User_r
@@ -1315,6 +1734,7 @@ func (c *TaskController) UpCollectionState() {
 	c.ServeJSON()
 	return
 }
+
 func (c *TaskController) UpDeliveryState() {
 	// 验证登录 User_is, User_r
 	User_r, User_is := Account.Verification_Admin(c.Ctx.GetCookie("User_tokey"), c.GetString("User_tokey"))
@@ -1847,6 +2267,181 @@ func (c *TaskController) Up() {
 	return
 }
 
+// 修改-
+func (c *TaskController) UpInfoCollection() {
+	// 验证登录 User_is, User_r
+	User_r, User_is := Account.Verification_Admin(c.Ctx.GetCookie("User_tokey"), c.GetString("User_tokey"))
+	if !User_is {
+		c.Data["json"] = lib.JSONS{Code: 201, Msg: "请重新登录!"}
+		c.ServeJSON()
+		return
+	}
+
+	T_task_id := c.GetString("T_task_id")
+	r, is := Task.Read_Task(T_task_id)
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "Id 错误!"}
+		c.ServeJSON()
+		return
+	}
+
+	T_InfoCollection_id := c.GetString("T_InfoCollection_id") // 信息采集id
+
+	// 查询信息采集信息
+	infoCollection, is := InfoCollection.Read_InfoCollection(T_InfoCollection_id)
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "获取信息采集失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	r.T_InfoCollection_id = T_InfoCollection_id
+	r.T_InfoTemplate_id = infoCollection.T_InfoTemplate_id
+	r.T_start_time = infoCollection.T_start_time // 项目开始时间使用信息采集开始时间
+
+	if !Task.Update_Task(r, "T_InfoCollection_id", "T_InfoTemplate_id", "T_start_time") {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败!"}
+		c.ServeJSON()
+		return
+	}
+	// 添加任务操作日志
+	Task.Add_TaskLogs_T(User_r.T_uuid, T_task_id, "任务管理", "修改信息采集id", r)
+	System.Add_UserLogs_T(User_r.T_uuid, "任务管理", "修改信息采集id", r)
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!"}
+	c.ServeJSON()
+	return
+}
+
+// 修改任务的公司
+func (c *TaskController) UpdateTaskCompany() {
+	// 验证登录 User_is, User_r
+	User_r, User_is := Account.Verification_Admin(c.Ctx.GetCookie("User_tokey"), c.GetString("User_tokey"))
+	if !User_is {
+		c.Data["json"] = lib.JSONS{Code: 201, Msg: "请重新登录!"}
+		c.ServeJSON()
+		return
+	}
+
+	T_task_id := c.GetString("T_task_id")
+	new_T_uuid := c.GetString("T_uuid") // 新公司UUID
+
+	if len(T_task_id) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "T_task_id 不能为空!"}
+		c.ServeJSON()
+		return
+	}
+
+	if len(new_T_uuid) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "T_uuid 不能为空!"}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询任务
+	task, is := Task.Read_Task(T_task_id)
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "任务不存在!"}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询原公司信息
+	oldUser, is := Account.Read_User(task.T_uuid)
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "原公司信息不存在!"}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询新公司信息
+	newUser, is := Account.Read_User(new_T_uuid)
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "新公司信息不存在!"}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询信息采集
+	var infoCollection InfoCollection.InfoCollection
+	if len(task.T_InfoCollection_id) > 0 {
+		infoCollection, is = InfoCollection.Read_InfoCollection(task.T_InfoCollection_id)
+		if !is {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "信息采集不存在!"}
+			c.ServeJSON()
+			return
+		}
+	} else {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "任务没有关联信息采集!"}
+		c.ServeJSON()
+		return
+	}
+
+	var newInfoCollection InfoCollection.InfoCollection
+	var newInfoCollectionId string
+
+	// 判断信息采集名称是否和原公司名称相同
+	if infoCollection.T_name == oldUser.T_name {
+		// 如果信息采集名称和原公司名称相同,查询新公司下与新公司同名的信息采集
+		newInfoCollection, is = InfoCollection.Read_InfoCollection_By_T_uuid_Name(new_T_uuid, newUser.T_name)
+		if !is {
+			// 如果不存在,创建新公司下的信息采集(使用新公司名称)
+			var err error
+			newInfoCollection, err = InfoCollection.GetOrCreate_InfoCollection(new_T_uuid, newUser.T_name)
+			if err != nil {
+				c.Data["json"] = lib.JSONS{Code: 202, Msg: "创建信息采集失败: " + err.Error()}
+				c.ServeJSON()
+				return
+			}
+		}
+		newInfoCollectionId = newInfoCollection.T_InfoCollection_id
+	} else {
+		// 如果信息采集名称和原公司名称不相同,查询新公司下有没有同名的信息采集
+		newInfoCollection, is = InfoCollection.Read_InfoCollection_By_T_uuid_Name(new_T_uuid, infoCollection.T_name)
+		if !is {
+			// 如果不存在,创建新公司下的信息采集(使用原信息采集名称)
+			var err error
+			newInfoCollection, err = InfoCollection.GetOrCreate_InfoCollection(new_T_uuid, infoCollection.T_name)
+			if err != nil {
+				c.Data["json"] = lib.JSONS{Code: 202, Msg: "创建信息采集失败: " + err.Error()}
+				c.ServeJSON()
+				return
+			}
+		}
+		newInfoCollectionId = newInfoCollection.T_InfoCollection_id
+	}
+
+	// 更新任务的公司和信息采集
+	task.T_uuid = new_T_uuid
+	task.T_InfoCollection_id = newInfoCollectionId
+	if newInfoCollection.T_InfoTemplate_id != "" {
+		task.T_InfoTemplate_id = newInfoCollection.T_InfoTemplate_id
+	}
+
+	// 更新新公司的分销商ID
+	if newUser.T_Distributor_id != "" {
+		task.T_Distributor_id = newUser.T_Distributor_id
+	}
+
+	// 更新任务
+	updateCols := []string{"T_uuid", "T_InfoCollection_id", "T_InfoTemplate_id"}
+	if newUser.T_Distributor_id != "" {
+		updateCols = append(updateCols, "T_Distributor_id")
+	}
+
+	if !Task.Update_Task(task, updateCols...) {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改任务失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	// 添加任务操作日志
+	Task.Add_TaskLogs_T(User_r.T_uuid, T_task_id, "任务管理", "修改任务公司", task)
+	System.Add_UserLogs_T(User_r.T_uuid, "任务管理", "修改任务公司", task)
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!"}
+	c.ServeJSON()
+	return
+}
+
 // 保存电子签名pdf
 func (c *TaskController) SaveElectronicSignaturePDF() {
 	// 验证登录 User_is, User_r
@@ -3414,14 +4009,22 @@ func (c *TaskController) Copy() {
 
 	T_name := c.GetString("T_name")
 	T_task_id := c.GetString("T_task_id")
+	new_T_uuid := c.GetString("T_uuid") // 新增T_uuid参数
 	r, is := Task.Read_Task(T_task_id)
 	if !is {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "获取信息采集失败!"}
 		c.ServeJSON()
 		return
 	}
+
+	// 确定使用的T_uuid
+	target_T_uuid := r.T_uuid
+	if len(new_T_uuid) > 0 && new_T_uuid != r.T_uuid {
+		target_T_uuid = new_T_uuid
+	}
+
 	dc := Device.DeviceClass{
-		T_uuid:  User_r.T_uuid,
+		T_uuid:  target_T_uuid,
 		T_State: 1,
 	}
 	T_class_id, is := Device.Add_DeviceClass(dc)
@@ -3438,13 +4041,63 @@ func (c *TaskController) Copy() {
 		c.ServeJSON()
 		return
 	}
+
+	// 如果T_uuid不同,需要处理信息采集
+	target_InfoCollection_id := r.T_InfoCollection_id
+	if len(new_T_uuid) > 0 && new_T_uuid != r.T_uuid {
+		// 查询新的T_uuid是否有同名的信息采集
+		newInfoCollection, found := InfoCollection.Read_InfoCollection_By_T_uuid_Name(new_T_uuid, infoCollection.T_name)
+		if found {
+			// 如果有则使用该信息采集
+			target_InfoCollection_id = newInfoCollection.T_InfoCollection_id
+			infoCollection = newInfoCollection
+		} else {
+			// 如果没有则添加
+			// 获取原公司和新公司的名称
+			_, oldCompany := Account.Read_User_ByT_uuid(r.T_uuid)
+			_, newCompany := Account.Read_User_ByT_uuid(new_T_uuid)
+			infoCollectionName := infoCollection.T_name
+			// 如果信息采集名称与原公司名称相同,则添加的时候需要将名称更改为新的T_uuid的公司的名称
+			if infoCollection.T_name == oldCompany.T_name {
+				infoCollectionName = newCompany.T_name
+			}
+
+			// 创建新的信息采集
+			newInfoCollection := InfoCollection.InfoCollection{
+				T_uuid:               new_T_uuid,
+				T_name:               infoCollectionName,
+				T_InfoTemplate_class: infoCollection.T_InfoTemplate_class,
+				T_InfoTemplate_id:    infoCollection.T_InfoTemplate_id,
+				T_status:             1,
+				T_State:              1,
+				T_start_time:         infoCollection.T_start_time,
+				T_end_time:           infoCollection.T_end_time,
+				T_time_interval:      infoCollection.T_time_interval,
+			}
+			newInfoCollectionId, err := InfoCollection.Add_InfoCollection(newInfoCollection)
+			if err != nil {
+				c.Data["json"] = lib.JSONS{Code: 202, Msg: "添加信息采集失败: " + err.Error()}
+				c.ServeJSON()
+				return
+			}
+			target_InfoCollection_id = newInfoCollectionId
+			// 重新读取新创建的信息采集以获取完整信息
+			infoCollection, is = InfoCollection.Read_InfoCollection(target_InfoCollection_id)
+			if !is {
+				c.Data["json"] = lib.JSONS{Code: 202, Msg: "获取新创建的信息采集失败!"}
+				c.ServeJSON()
+				return
+			}
+		}
+	}
+
 	var_ := Task.Task{
 		T_Distributor_id:       r.T_Distributor_id,
-		T_InfoCollection_id:    r.T_InfoCollection_id,
+		T_InfoCollection_id:    target_InfoCollection_id,
 		T_InfoTemplate_id:      infoCollection.T_InfoTemplate_id,
 		T_start_time:           infoCollection.T_start_time, // 项目开始时间使用信息采集开始时间
 		T_class:                int(T_class_id),
-		T_uuid:                 r.T_uuid,
+		T_uuid:                 target_T_uuid,
 		T_name:                 T_name,
 		T_VerifyTemplate_class: r.T_VerifyTemplate_class,
 		T_VerifyTemplate_id:    r.T_VerifyTemplate_id,

+ 109 - 1
controllers/infoCollection.go

@@ -15,9 +15,10 @@ import (
 	"ColdVerify_server/models/VerifyTemplate"
 	"encoding/json"
 	"fmt"
-	beego "github.com/beego/beego/v2/server/web"
 	"math"
 	"time"
+
+	beego "github.com/beego/beego/v2/server/web"
 )
 
 type InfoCollectionController struct {
@@ -621,3 +622,110 @@ func (c *InfoCollectionController) Copy() {
 	c.ServeJSON()
 	return
 }
+
+// 根据UserList创建信息采集
+func (c *InfoCollectionController) SyncIDForTask() {
+	// 验证登录 User_is, User_r
+	User_r, User_is := Account.Verification_Admin(c.Ctx.GetCookie("User_tokey"), c.GetString("User_tokey"))
+	if !User_is {
+		c.Data["json"] = lib.JSONS{Code: 201, Msg: "请重新登录!"}
+		c.ServeJSON()
+		return
+	}
+
+	// 获取参数(可选)
+	var T_InfoTemplate_class, T_InfoTemplate_id, T_allot_task_id string
+	//T_InfoTemplate_class := c.GetString("T_InfoTemplate_class")
+	//T_InfoTemplate_id := c.GetString("T_InfoTemplate_id")
+	//T_allot_task_id := c.GetString("T_allot_task_id")
+	//T_Distributor_id := c.GetString("T_Distributor_id") // 经销商ID,用于查询用户列表
+	//T_name := c.GetString("T_name")                     // 用户名称,用于查询用户列表
+
+	// 从User表查询用户列表
+	var userList []Account.User
+
+	// 如果没有查询条件,获取所有启用的用户
+	var err error
+	userList, err = Account.Read_AllActiveUsers()
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "查询用户列表失败: " + err.Error()}
+		c.ServeJSON()
+		return
+	}
+
+	if len(userList) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "未找到符合条件的用户!"}
+		c.ServeJSON()
+		return
+	}
+
+	var results []map[string]interface{}
+
+	// 遍历用户列表,为每个用户创建信息采集
+	for _, user := range userList {
+		// 创建信息采集
+		var_ := InfoCollection.InfoCollection{
+			T_uuid:               user.T_uuid,
+			T_name:               user.T_name,
+			T_InfoTemplate_class: T_InfoTemplate_class,
+			T_InfoTemplate_id:    T_InfoTemplate_id,
+			T_status:             0,
+			T_State:              1,
+			T_submit_uuid:        "",
+			T_allot_task_id:      T_allot_task_id,
+			T_start_time:         time.Now().Format("2006-01-02 15:04:05"),
+		}
+
+		T_InfoCollection_id, err := InfoCollection.Add_InfoCollection(var_)
+		if err != nil {
+			if err.Error() == "信息采集标题重复" {
+				if infoCollection, ok := InfoCollection.Read_InfoCollection_By_T_uuid_Name(user.T_uuid, user.T_name); ok {
+					T_InfoCollection_id = infoCollection.T_InfoCollection_id
+				} else {
+					results = append(results, map[string]interface{}{
+						"T_uuid":  user.T_uuid,
+						"T_name":  user.T_name,
+						"success": false,
+						"msg":     "信息采集标题重复,查询历史信息采集失败",
+					})
+					continue
+				}
+			} else {
+				// 其他错误正常返回
+				results = append(results, map[string]interface{}{
+					"T_uuid":  user.T_uuid,
+					"T_name":  user.T_name,
+					"success": false,
+					"msg":     err.Error(),
+				})
+				continue
+			}
+		}
+
+		// 查询Task中对应T_uuid的并且T_InfoCollection_id为空的数据
+		taskList, _ := Task.Read_Task_List_By_T_uuid_And_Empty_InfoCollection_id(user.T_uuid)
+
+		// 更新Task的T_InfoCollection_id
+		if len(taskList) > 0 {
+			err = Task.Update_Task_T_InfoCollection_id_By_T_uuid(user.T_uuid, T_InfoCollection_id)
+			if err != nil {
+				results = append(results, map[string]interface{}{
+					"T_uuid":              user.T_uuid,
+					"T_name":              user.T_name,
+					"T_InfoCollection_id": T_InfoCollection_id,
+					"success":             false,
+					"msg":                 "创建信息采集成功,但更新任务失败: " + err.Error(),
+					"task_count":          len(taskList),
+				})
+				continue
+			}
+		}
+
+		// 添加日志
+		System.Add_UserLogs_T(User_r.T_uuid, "信息采集管理", "批量创建", var_)
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: results}
+	c.ServeJSON()
+	return
+}

+ 2 - 0
go.mod

@@ -24,6 +24,7 @@ require (
 
 require (
 	git.sr.ht/~sbinet/gg v0.4.1 // indirect
+	github.com/360EntSecGroup-Skylar/excelize v1.4.1 // indirect
 	github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect
 	github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
 	github.com/alibabacloud-go/debug v1.0.1 // indirect
@@ -58,6 +59,7 @@ require (
 	github.com/richardlehane/mscfb v1.0.4 // indirect
 	github.com/richardlehane/msoleps v1.0.3 // indirect
 	github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect
+	github.com/tealeg/xlsx v1.0.5 // indirect
 	github.com/tjfoc/gmsm v1.4.1 // indirect
 	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 	github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect

+ 5 - 0
go.sum

@@ -35,6 +35,8 @@ git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo=
 git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE=
 git.sr.ht/~sbinet/gg v0.4.1 h1:YccqPPS57/TpqX2fFnSRlisrqQ43gEdqVm3JtabPrp0=
 git.sr.ht/~sbinet/gg v0.4.1/go.mod h1:xKrQ22W53kn8Hlq+gzYeyyohGMwR8yGgSMlVpY/mHGc=
+github.com/360EntSecGroup-Skylar/excelize v1.4.1 h1:l55mJb6rkkaUzOpSsgEeKYtS6/0gHwBYyfo5Jcjv/Ks=
+github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ -385,6 +387,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -394,6 +397,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
 github.com/syndtr/goleveldb v0.0.0-20181127023241-353a9fca669c/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
+github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
+github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
 github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
 github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
 github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=

+ 45 - 3
models/InfoCollection/InfoCollection.go

@@ -7,12 +7,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"strings"
+	"time"
+
 	"github.com/astaxie/beego/cache"
 	"github.com/beego/beego/v2/adapter/orm"
 	orm2 "github.com/beego/beego/v2/client/orm"
 	_ "github.com/go-sql-driver/mysql"
-	"strings"
-	"time"
 )
 
 var (
@@ -47,7 +48,7 @@ type InfoCollection struct {
 	T_name               string  `orm:"size(256);null"`     // 标题
 	T_InfoTemplate_class string  `orm:"size(256);null"`     // 模版分类
 	T_InfoTemplate_id    string  `orm:"size(256);null"`     // 模版id
-	T_status             int     `orm:"size(2);default(0)"` // 状态 1待提交 2已提交 3已接收 4已退回 5已回款
+	T_status             int     `orm:"size(2);default(0)"` // 状态 1待提交 2已提交 3已接收 4已退回 5已回款 0兼容历史
 	T_State              int     `orm:"size(2);default(1)"` // 0 删除  1 正常
 	T_submit_uuid        string  `orm:"size(256);null"`     // 提交人 UUID
 	T_start_time         string  `orm:"size(256);null"`     // 开始时间
@@ -209,6 +210,47 @@ func Read_InfoCollection(T_InfoCollection_id string) (r InfoCollection, is bool)
 	return r, true
 }
 
+// 根据T_uuid和T_name获取信息采集
+func Read_InfoCollection_By_T_uuid_Name(T_uuid, T_name string) (r InfoCollection, is bool) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(InfoCollection))
+	err := qs.Filter("T_uuid", T_uuid).Filter("T_name", T_name).Filter("T_State", 1).One(&r)
+	if err != nil {
+		return r, false
+	}
+	return r, true
+}
+
+// GetOrCreate_InfoCollection 根据 T_uuid 和 T_name 查询或创建信息采集,返回 InfoCollection 对象
+func GetOrCreate_InfoCollection(T_uuid, T_name string) (InfoCollection, error) {
+	if len(T_uuid) == 0 || len(T_name) == 0 {
+		return InfoCollection{}, errors.New("T_uuid 或 T_name 不能为空")
+	}
+
+	if r, ok := Read_InfoCollection_By_T_uuid_Name(T_uuid, T_name); ok {
+		return r, nil
+	}
+
+	var_ := InfoCollection{
+		T_uuid:  T_uuid,
+		T_name:  T_name,
+		T_State: 1,
+	}
+
+	T_InfoCollection_id, err := Add_InfoCollection(var_)
+	if err != nil {
+		if err.Error() == "信息采集标题重复" {
+			if r, ok := Read_InfoCollection_By_T_uuid_Name(T_uuid, T_name); ok {
+				return r, nil
+			}
+		}
+		return InfoCollection{}, err
+	}
+
+	var_.T_InfoCollection_id = T_InfoCollection_id
+	return var_, nil
+}
+
 // 添加
 func Add_InfoCollection(r InfoCollection) (string, error) {
 	o := orm.NewOrm()

+ 63 - 21
models/Task/Task.go

@@ -151,10 +151,10 @@ var (
 	TaskCollectionTimeLimit float64 = 7 * 24 * 60
 
 	DeviceTypeMap = map[string]string{
-		"箱":  "X",
-		"柜":  "G",
-		"车":  "C",
-		"库":  "K",
+		"箱":   "X",
+		"柜":   "G",
+		"车":   "C",
+		"库":   "K",
 		"系统": "XT",
 		"位置": "WZ",
 		"培训": "PX",
@@ -200,7 +200,7 @@ type Task struct {
 	T_marking_state          int    `orm:"size(2);default(0)"` // 验证标识 状态 0 未完成 1 已完成
 	T_examining_report_state int    `orm:"size(2);default(0)"` // 检测报告 状态 0 未完成 1 已完成(客户通过) 2已退回(客户) 3已通过(负责人) 4已退回(负责人) 5已提交
 	T_original_record_state  int    `orm:"size(2);default(0)"` // 原始记录 状态 0 未完成 3 已通过(负责人) 4已退回(负责人) 5已提交
-	T_record                 string `orm:"type(text)"`         // 领导备注
+	T_record                 string `orm:"type(text);null"`    // 领导备注
 
 	T_collection_submit_time string `orm:"size(256);null"` // 数据采集 提交时间
 	T_reporting_submit_time  string `orm:"size(256);null"` // 报告编写 提交时间
@@ -251,21 +251,22 @@ type Task struct {
 	T_temp_range     string `orm:"size(256);null"` // 验证温度范围
 	T_report_number  string `orm:"size(256);null"` // 报告编号
 	T_report_type    string `orm:"size(256);null"` // 报告类型
+	T_belong_year    string `orm:"size(256);null"` // 所属年份
 
-	T_start_time    string  `orm:"size(256);null"` // 项目开始时间
-	T_end_time      string  `orm:"size(256);null"` // 结束时间 报告审核通过时间
-	T_time_interval float64 `orm:"size(256);null"` // 时间间隔 单位分钟
-	T_reject_times  int     `orm:"size(256);null"` // 驳回次数 客户退回方案和报告时
-	T_reject_record string  `orm:"type(text)"`     // 驳回记录
+	T_start_time    string  `orm:"size(256);null"`  // 项目开始时间
+	T_end_time      string  `orm:"size(256);null"`  // 结束时间 报告审核通过时间
+	T_time_interval float64 `orm:"size(256);null"`  // 时间间隔 单位分钟
+	T_reject_times  int     `orm:"size(256);null"`  // 驳回次数 客户退回方案和报告时
+	T_reject_record string  `orm:"type(text);null"` // 驳回记录
 
 	// 方案
-	T_scheme_start_time    string  `orm:"size(256);null"` // 验证方案开始时间 接收信息采集表的时间
-	T_scheme_end_time      string  `orm:"size(256);null"` // 验证方案结束时间 负责人审核通过后最后一次上传时间
-	T_scheme_time_interval float64 `orm:"size(256);null"` // 时间间隔 单位分钟
-	T_scheme_overtime      float64 `orm:"size(256);null"` // 验证方案超时时间 单位分钟
-	T_scheme_signature     string  `orm:"type(text)"`     // 验证方案客户签字确认图片
-	T_scheme_return_times  int     `orm:"size(256);null"` // 验证方案退回次数
-	T_scheme_audit_record  string  `orm:"type(text)"`     // 验证方案审核记录
+	T_scheme_start_time    string  `orm:"size(256);null"`  // 验证方案开始时间 接收信息采集表的时间
+	T_scheme_end_time      string  `orm:"size(256);null"`  // 验证方案结束时间 负责人审核通过后最后一次上传时间
+	T_scheme_time_interval float64 `orm:"size(256);null"`  // 时间间隔 单位分钟
+	T_scheme_overtime      float64 `orm:"size(256);null"`  // 验证方案超时时间 单位分钟
+	T_scheme_signature     string  `orm:"type(text);null"` // 验证方案客户签字确认图片
+	T_scheme_return_times  int     `orm:"size(256);null"`  // 验证方案退回次数
+	T_scheme_audit_record  string  `orm:"type(text);null"` // 验证方案审核记录
 
 	// 实施
 	T_enter_area_time          string  `orm:"size(256);null"`  // 进场时间
@@ -273,7 +274,7 @@ type Task struct {
 	T_collection_end_time      string  `orm:"size(256);null"`  // 实施结束时间 (审核通过后签字确认提交时间)
 	T_collection_time_interval float64 `orm:"size(256);null"`  // 时间间隔 单位分钟
 	T_collection_overtime      float64 `orm:"size(256);null"`  // 实施超时时间 单位分钟
-	T_collection_signature     string  `orm:"type(text)"`      // 实施人员签字确认图片
+	T_collection_signature     string  `orm:"type(text);null"` // 实施人员签字确认图片
 	T_collection_return_times  int     `orm:"size(256);null"`  // 实施方案退回次数
 	T_collection_audit_record  string  `orm:"type(text);null"` // 实施方案审核记录
 
@@ -282,7 +283,7 @@ type Task struct {
 	T_reporting_end_time      string  `orm:"size(256);null"`  // 验证报告结束时间 负责人审核通过前最后一次上传时间
 	T_reporting_time_interval float64 `orm:"size(256);null"`  // 时间间隔 单位分钟
 	T_reporting_overtime      float64 `orm:"size(256);null"`  // 验证报告超时时间 单位分钟
-	T_reporting_signature     string  `orm:"type(text)"`      // 验证报告客户签字确认图片
+	T_reporting_signature     string  `orm:"type(text);null"` // 验证报告客户签字确认图片
 	T_reporting_return_times  int     `orm:"size(256);null"`  // 验证报告退回次数
 	T_reporting_audit_record  string  `orm:"type(text);null"` // 验证报告审核记录
 	T_reporting_pass_time     string  `orm:"size(256);null"`  // 验证报告负责人通过时间
@@ -291,7 +292,7 @@ type Task struct {
 	T_examining_report_start_time    string  `orm:"size(256);null"`  // 检测报告开始时间 接收信息采集表的时间
 	T_examining_report_end_time      string  `orm:"size(256);null"`  // 检测报告结束时间 负责人审核通过前最后一次上传时间
 	T_examining_report_time_interval float64 `orm:"size(256);null"`  // 时间间隔 单位分钟
-	T_examining_report_signature     string  `orm:"type(text)"`      // 检测报告客户签字确认图片
+	T_examining_report_signature     string  `orm:"type(text);null"` // 检测报告客户签字确认图片
 	T_examining_report_return_times  int     `orm:"size(256);null"`  // 检测报告退回次数
 	T_examining_report_audit_record  string  `orm:"type(text);null"` // 检测报告审核记录
 
@@ -299,7 +300,7 @@ type Task struct {
 	T_original_record_start_time    string  `orm:"size(256);null"`  // 原始记录开始时间 接收信息采集表的时间
 	T_original_record_end_time      string  `orm:"size(256);null"`  // 原始记录结束时间 负责人审核通过前最后一次上传时间
 	T_original_record_time_interval float64 `orm:"size(256);null"`  // 时间间隔 单位分钟
-	T_original_record_signature     string  `orm:"type(text)"`      // 原始记录客户签字确认图片
+	T_original_record_signature     string  `orm:"type(text);null"` // 原始记录客户签字确认图片
 	T_original_record_return_times  int     `orm:"size(256);null"`  // 原始记录退回次数
 	T_original_record_audit_record  string  `orm:"type(text);null"` // 原始记录审核记录
 
@@ -1520,3 +1521,44 @@ func CorrectionSecond(T_task_id string) error {
 
 	return nil
 }
+
+// 查询T_uuid匹配且T_InfoCollection_id为空的任务列表
+func Read_Task_List_By_T_uuid_And_Empty_InfoCollection_id(T_uuid string) ([]Task, int) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(Task))
+	var r []Task
+
+	cond := orm.NewCondition()
+	// 查询T_InfoCollection_id为NULL或空字符串
+	cond1 := cond.And("T_uuid", T_uuid).And("T_State", 1).AndCond(cond.Or("T_InfoCollection_id__isnull", true).Or("T_InfoCollection_id", ""))
+
+	qs.SetCond((*orm2.Condition)(cond1)).OrderBy("-Id").All(&r)
+	cnt, _ := qs.SetCond((*orm2.Condition)(cond1)).Count()
+
+	return r, int(cnt)
+}
+
+// 批量更新任务的T_InfoCollection_id
+func Update_Task_T_InfoCollection_id_By_T_uuid(T_uuid, T_InfoCollection_id string) error {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(Task))
+	var r []Task
+	o.Begin()
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_uuid", T_uuid).And("T_State", 1).AndCond(cond.Or("T_InfoCollection_id__isnull", true).Or("T_InfoCollection_id", ""))
+	qs.SetCond((*orm2.Condition)(cond1)).All(&r)
+
+	for _, task := range r {
+		task.T_InfoCollection_id = T_InfoCollection_id
+		if _, err := o.Update(&task, "T_InfoCollection_id"); err == nil {
+			Redis_Task_Set(task.T_task_id, task)
+		} else {
+			o.Rollback()
+			logs.Error(lib.FuncName(), err)
+			return err
+		}
+	}
+	o.Commit()
+	return nil
+}

+ 54 - 0
models/Task/TaskUid.go

@@ -0,0 +1,54 @@
+package Task
+
+import (
+	"ColdVerify_server/logs"
+	"time"
+
+	"github.com/beego/beego/v2/adapter/orm"
+)
+
+// TaskUid 用于保存已经导出的任务唯一标识,避免重复生成
+type TaskUid struct {
+	Id          int       `orm:"column(ID);size(11);auto;pk"`
+	Uid         string    `orm:"column(T_uid);size(64);unique"`
+	ExportBatch string    `orm:"size(64);null"` // 批次标识,便于追踪
+	CreateTime  time.Time `orm:"column(create_time);type(timestamp);null;auto_now_add"`
+	UpdateTime  time.Time `orm:"column(update_time);type(timestamp);null;auto_now"`
+}
+
+func (t *TaskUid) TableName() string {
+	return "task_uid"
+}
+
+func init() {
+	orm.RegisterModel(new(TaskUid))
+}
+
+// TaskUidExists 判断唯一标识是否已经生成/导出过
+func TaskUidExists(uid string) bool {
+	o := orm.NewOrm()
+	return o.QueryTable(new(TaskUid)).Filter("T_uid", uid).Exist()
+}
+
+// SaveTaskUids 批量写入导出的唯一标识
+func SaveTaskUids(uids []string, batch string) error {
+	if len(uids) == 0 {
+		return nil
+	}
+
+	o := orm.NewOrm()
+	records := make([]TaskUid, 0, len(uids))
+	for _, uid := range uids {
+		records = append(records, TaskUid{
+			Uid:         uid,
+			ExportBatch: batch,
+		})
+	}
+
+	if _, err := o.InsertMulti(len(records), records); err != nil {
+		logs.Error("SaveTaskUids InsertMulti error:", err)
+		return err
+	}
+
+	return nil
+}

+ 103 - 2
models/VerifyTemplate/VerifyTemplateMapData.go

@@ -4,11 +4,12 @@ import (
 	"ColdVerify_server/lib"
 	"ColdVerify_server/logs"
 	"ColdVerify_server/models/Task"
+	"strings"
+	"time"
+
 	"github.com/beego/beego/v2/adapter/orm"
 	orm2 "github.com/beego/beego/v2/client/orm"
 	_ "github.com/go-sql-driver/mysql"
-	"strings"
-	"time"
 )
 
 // 模版
@@ -420,3 +421,103 @@ func Clear_VerifyTemplateMapData_T_value(T_task_id, T_VerifyTemplate_id, T_Verif
 	}
 	return nil
 }
+
+// 检查T_source=1和T_source=2的必填项是否都有值
+// 如果T_source=1(方案)必填项全部有数据,则修改Task的T_scheme_state为10
+// 如果T_source=2(报告)必填项全部有数据,则修改Task的T_reporting_state为10
+func Check_RequiredFields_HaveValue(T_task_id string) bool {
+	o := orm.NewOrm()
+
+	// 读取Task
+	task, is := Task.Read_Task(T_task_id)
+	if !is {
+		logs.Error(lib.FuncName(), "Task not found:", T_task_id)
+		return false
+	}
+	T_VerifyTemplate_id := task.T_VerifyTemplate_id
+
+	// 查询T_source=1的所有必填项(T_Required=1)
+	// T_source=1对应方案,包含source: 0,1,7
+	var requiredMaps_source1 []VerifyTemplateMap
+	qs_map1 := o.QueryTable(new(VerifyTemplateMap))
+	cond1 := orm.NewCondition()
+	cond1 = cond1.And("T_VerifyTemplate_id", T_VerifyTemplate_id).And("T_Required", 1).And("T_source__in", []int{0, 1, 7})
+	qs_map1.SetCond((*orm2.Condition)(cond1)).All(&requiredMaps_source1)
+
+	// 查询T_source=2的所有必填项(T_Required=1)
+	// T_source=2对应报告,包含source: 0,2,3,7
+	var requiredMaps_source2 []VerifyTemplateMap
+	qs_map2 := o.QueryTable(new(VerifyTemplateMap))
+	cond2 := orm.NewCondition()
+	cond2 = cond2.And("T_VerifyTemplate_id", T_VerifyTemplate_id).And("T_Required", 1).And("T_source__in", []int{0, 2, 3, 7})
+	qs_map2.SetCond((*orm2.Condition)(cond2)).All(&requiredMaps_source2)
+
+	// 查询所有数据
+	var allData []VerifyTemplateMapData
+	qs_data := o.QueryTable(new(VerifyTemplateMapData))
+	cond_data := orm.NewCondition()
+	cond_data = cond_data.And("T_task_id", T_task_id).And("T_VerifyTemplate_id", T_VerifyTemplate_id)
+	qs_data.SetCond((*orm2.Condition)(cond_data)).All(&allData)
+
+	// 将数据转换为map,方便查找
+	dataMap := make(map[string]VerifyTemplateMapData)
+	for _, v := range allData {
+		dataMap[v.T_VerifyTemplateMap_id] = v
+	}
+
+	// 检查T_source=1的必填项是否都有值
+	source1Complete := true
+	for _, mapItem := range requiredMaps_source1 {
+		data, exists := dataMap[mapItem.T_id]
+		if !exists {
+			source1Complete = false
+			break
+		}
+		if len(strings.TrimSpace(data.T_value)) == 0 {
+			source1Complete = false
+			break
+		}
+	}
+
+	// 检查T_source=2的必填项是否都有值
+	source2Complete := true
+	for _, mapItem := range requiredMaps_source2 {
+		data, exists := dataMap[mapItem.T_id]
+		if !exists {
+			source2Complete = false
+			break
+		}
+		if len(strings.TrimSpace(data.T_value)) == 0 {
+			source2Complete = false
+			break
+		}
+	}
+
+	// 更新状态
+	updateCols := make([]string, 0)
+	needUpdate := false
+
+	// 如果T_source=1(方案)必填项全部有数据,则修改T_scheme_state为10
+	if source1Complete && task.T_scheme_state != 10 {
+		task.T_scheme_state = 10
+		updateCols = append(updateCols, "T_scheme_state")
+		needUpdate = true
+	}
+
+	// 如果T_source=2(报告)必填项全部有数据,则修改T_reporting_state为10
+	if source2Complete && task.T_reporting_state != 10 {
+		task.T_reporting_state = 10
+		updateCols = append(updateCols, "T_reporting_state")
+		needUpdate = true
+	}
+
+	// 执行更新
+	if needUpdate {
+		if !Task.Update_Task(task, updateCols...) {
+			logs.Error(lib.FuncName(), "Failed to update Task state")
+			return false
+		}
+	}
+
+	return source1Complete && source2Complete
+}

+ 2 - 0
routers/InfoCollection.go

@@ -2,6 +2,7 @@ package routers
 
 import (
 	"ColdVerify_server/controllers"
+
 	beego "github.com/beego/beego/v2/server/web"
 )
 
@@ -16,6 +17,7 @@ func init() {
 	beego.Router("/InfoCollection/AuditRecordList", &controllers.InfoCollectionController{}, "*:AuditRecordList") // 信息采集审核记录
 	beego.Router("/InfoCollection/Statistics", &controllers.InfoCollectionController{}, "*:Statistics")           // 小程序统计
 	beego.Router("/InfoCollection/Copy", &controllers.InfoCollectionController{}, "*:Copy")                       // 复制信息采集
+	beego.Router("/InfoCollection/SyncIDForTask", &controllers.InfoCollectionController{}, "*:SyncIDForTask")     // 根据UserList创建信息采集
 
 	beego.Router("/InfoTemplate/Class_List", &controllers.InfoTemplateController{}, "*:Class_List") // 信息采集分类列表
 	beego.Router("/InfoTemplate/Class_Add", &controllers.InfoTemplateController{}, "*:Class_Add")   // 信息采集分类添加

+ 3 - 0
routers/Task.go

@@ -32,10 +32,13 @@ func init() {
 	beego.Router("/Task/Stat", &controllers.TaskController{}, "*:Stat")                                             // 报告统计
 	beego.Router("/Task/Stat_Excel", &controllers.TaskController{}, "*:Stat_Excel")                                 // 报告统计-excel
 	beego.Router("/Task/StatisticalRanking", &controllers.TaskController{}, "*:StatisticalRanking")                 // 报告统计-excel
+	beego.Router("/Task/ExportTaskUid", &controllers.TaskController{}, "*:ExportTaskUid")                           // 导出任务唯一标识
 	beego.Router("/Task/GenT_report_number", &controllers.TaskController{}, "*:GenT_report_number")                 // 生成报告编号
 	beego.Router("/Task/SyncPDFWatermark", &controllers.TaskController{}, "*:SyncPDFWatermark")                     // 生成报告编号
 	beego.Router("/Task/SaveElectronicSignaturePDF", &controllers.TaskController{}, "*:SaveElectronicSignaturePDF") // 生成报告编号
 	beego.Router("/Task/Import_Tasks", &controllers.TaskController{}, "*:Import_Tasks")                             // 导入任务
+	beego.Router("/Task/UpInfoCollection", &controllers.TaskController{}, "*:UpInfoCollection")                     // 修改信息采集id
+	beego.Router("/Task/UpdateTaskCompany", &controllers.TaskController{}, "*:UpdateTaskCompany")                   // 修改任务的公司
 
 	// 日志
 	beego.Router("/TaskLogs/List", &controllers.TaskController{}, "*:Logs_List")