Selaa lähdekoodia

add:售后服务、文件管理

zoie 3 viikkoa sitten
vanhempi
commit
f9567f6768

+ 1 - 0
.gitignore

@@ -34,6 +34,7 @@ _testmain.go
 lastupdate.tmp
 main
 Cold_Api6200
+Cold_Api
 Makefile
 ofile
 backup

+ 6 - 8
conf/app.conf

@@ -7,16 +7,13 @@ copyrequestbody = true
 Version = "/v3"
 
 # # Nats
-NatsServer_Url = "203.34.49.130:4222"
-; NatsServer_Url = "192.168.0.5:4222"
+NatsServer_Url = "8.148.211.203:4222"
 
 
 # Mysql
-; MysqlServer_UrlPort = "192.168.0.88:3306"
-MysqlServer_UrlPort = "203.34.49.130:3306"
+MysqlServer_UrlPort = "8.148.211.203:3306"
 MysqlServer_Database = "cold"
 MysqlServer_Username = "cold"
-; MysqlServer_Password = "yjwyEckZS7rE5H!"
 MysqlServer_Password = "yjwyEckZS7rE5H"
 MysqlServer_MaxIdleConnections = 100
 MysqlServer_MaxOpenConnections = 200
@@ -24,10 +21,8 @@ MysqlServer_Debug = true
 
 
 # Redis
-; Redis_address = "192.168.0.5:6379"
-Redis_address = "203.34.49.130:6379"
+Redis_address = "8.148.211.203:6379"
 Redis_password = "redis_JJ56d5"
-; Redis_password = "redis_wsxaMH"
 Redis_dbNum = "1"
 
 
@@ -72,3 +67,6 @@ UseNewWarningQuery = false
 # 对接精创数据
 apiurl = "http://192.168.0.5:8081/jc/api/"
 jcCron = "0 */2 * * * *"
+
+# 文件管理默认大小限制(字节),默认1GB = 1073741824字节
+DefaultFileSizeLimit = 1073741824

+ 5 - 4
conf/app_prod.conf

@@ -7,10 +7,11 @@ copyrequestbody = true
 Version = "/v3"
 
 # # Nats
-NatsServer_Url = "127.0.0.1:43422"
+NatsServer_Url = "192.168.0.5:4222"
+
 
 # Mysql
-MysqlServer_UrlPort = "127.0.0.1:40306"
+MysqlServer_UrlPort = "192.168.0.88:3306"
 MysqlServer_Database = "cold"
 MysqlServer_Username = "cold"
 MysqlServer_Password = "yjwyEckZS7rE5H!"
@@ -20,8 +21,8 @@ MysqlServer_Debug = true
 
 
 # Redis
-Redis_address = "127.0.0.1:43379"
-Redis_password = ""
+Redis_address = "192.168.0.5:6379"
+Redis_password = "redis_wsxaMH"
 Redis_dbNum = "1"
 
 

+ 70 - 0
conf/app_test.conf

@@ -0,0 +1,70 @@
+appname = Cold_Api
+HTTPPort = 6200
+runmode = dev
+EnableDocs = true
+copyrequestbody = true
+
+Version = "/v3"
+
+# # Nats
+NatsServer_Url = "8.148.211.203:4222"
+
+
+# Mysql
+MysqlServer_UrlPort = "8.148.211.203:3306"
+MysqlServer_Database = "cold"
+MysqlServer_Username = "cold"
+MysqlServer_Password = "yjwyEckZS7rE5H"
+MysqlServer_MaxIdleConnections = 100
+MysqlServer_MaxOpenConnections = 200
+MysqlServer_Debug = true
+
+
+# Redis
+Redis_address = "8.148.211.203:6379"
+Redis_password = "redis_JJ56d5"
+; Redis_password = "redis_wsxaMH"
+Redis_dbNum = "1"
+
+
+# 静态资源
+Qiniu_AccessKey = "-8ezB_d-8-eUFTMvhOGbGzgeQRPeKQnaQ3DBcUxo"
+Qiniu_SecretKey = "KFhkYxTAJ2ZPN3ZS3euTsfWk8-C92rKgkhAMkDRN"
+Qiniu_BUCKET = "coldoss"
+Qiniu_Url = "https://coldoss.coldbaozhida.com/"
+
+# Panel
+Panel_url = "http://127.0.0.1:6204/Cold_Panel"
+
+# 不验证登录的接口
+FilterExcludeURL = /Login_verification,/Data/List,/WxPay/Notify,/Data/Company_key_Device_Sensor_List,/docking/Real_Data,/docking/Note_Data,/Company/Transport/List,/DeviceSensor/List_BySN,/Data/GetNewLocus
+# 只验证登录不验证权限的接口
+FilterOnlyLoginCheckURL = /Menu/List,/User/Info,/User/Home,/User/GetCompanyInfo,/User/Post,/UpFileToken,/User/WxQRCode,/Company/Get,/DataSource,/Company/Bill_Excel,/WarningSend/List,/WxPay/Get_QRCode,/WxPay/GetOrderState,/WarningHandle/List,/DeviceWarning/List_Count,/Bulletin/GetBulletinById,/Bulletin/IsReadBulletin,/Bulletin/GetBulletinList,/Data/GetNewLocusSSe,/DeviceWarning/BatchEdit,/Notice/WarinType,/Data/GetSecondaryPositioning
+
+
+MqttIds = tmqttjxit
+MqttServer_id = "mqttjxit"
+
+Weixin_PwdKey = "7Nt9sJb1Xy0PoQrEl3Df5Zv2Cg6AhRkT"
+Weixin_Notify = ""
+
+
+# 日志配置
+# 0-控制台输出 1-文件输出 2-文件和控制台输出
+adapter_type = 0
+# 文件最多保存多少天
+maxdays = 7
+# 日志级别 (0-紧急 1-报警 2-严重错误 3-错误 4-警告 5-注意 6-信息 7-调试)
+level = 7
+# SQL日志级别 (1-静音 2-错误 3-警告 4-信息). 注意: sql日志只在level大于等于5级别才会输出。
+sqlloglevel = 4
+# 慢SQL阈值(毫秒)。慢SQL会在sqlloglevel大于等于3时输出。
+slow_threshold = 200
+# 每个文件保存的最大行数
+maxlines = 10000
+
+# 使用新的告警查询方式,修改为true后弃用设备报禁信息将在弃用菜单内才能查询
+UseNewWarningQuery = false
+# 对接精创数据
+apiurl = "http://192.168.0.5:8081/jc/api/"
+jcCron = "0 */2 * * * *"

+ 3 - 0
conf/config.go

@@ -47,3 +47,6 @@ var WarningRateExcludePid, _ = beego.AppConfig.String("WarningRateExcludePid")
 var Weixin_PwdKey, _ = beego.AppConfig.String("Weixin_PwdKey")
 var Weixin_Notify, _ = beego.AppConfig.String("Weixin_Notify")
 var UseNewWarningQuery, _ = beego.AppConfig.Bool("UseNewWarningQuery")
+
+// 文件管理默认大小限制(字节)
+var DefaultFileSizeLimit, _ = beego.AppConfig.Int64("DefaultFileSizeLimit")

+ 480 - 0
controllers/AfterSales.go

@@ -0,0 +1,480 @@
+package controllers
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers/lib"
+	"Cold_Api/models/Account"
+	"Cold_Api/models/AfterSales"
+	"encoding/json"
+	"math"
+
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+type AfterSalesController struct {
+	beego.Controller
+	Admin_r Account.Admin // 登陆的用户
+}
+
+func (c *AfterSalesController) Prepare() {
+	GetCookie := c.Ctx.GetCookie("User_tokey")
+	GetString := c.GetString("User_tokey")
+	User_tokey := GetString
+	if len(User_tokey) == 0 {
+		User_tokey = GetCookie
+	}
+	if Account.Admin_r == nil {
+		return
+	}
+	c.Admin_r = *Account.Admin_r
+}
+
+// AddAfterSales 添加售后服务
+func (c *AfterSalesController) AddAfterSales() {
+	var afterSales AfterSales.AfterSales
+
+	// 获取参数
+	T_name := c.GetString("T_name")
+	if len(T_name) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "售后服务名称不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	T_category, err := c.GetInt("T_category")
+	if err != nil || T_category <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "分类参数错误,请选择有效的分类"}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查分类是否存在
+	if _, err = AfterSales.Read_AfterSalesCategory_ById(T_category); err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "所选分类不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	T_content := c.GetString("T_content")
+	T_attachments := c.GetString("T_attachments") // JSON字符串,新格式
+	T_sort, _ := c.GetInt("T_sort")
+	T_display, _ := c.GetInt("T_display") // 是否显示,默认为1(显示)
+
+	// 验证附件JSON格式
+	if len(T_attachments) > 0 {
+		var attachments []AfterSales.Attachment
+		err = json.Unmarshal([]byte(T_attachments), &attachments)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "附件格式错误,请传入有效的JSON格式:[{\"name\":\"文件名\",\"url\":\"文件地址\"}]"}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 构建数据
+	afterSales.T_name = T_name
+	afterSales.T_category = T_category
+	afterSales.T_content = T_content
+	afterSales.T_attachments = T_attachments
+	afterSales.T_sort = T_sort
+	afterSales.T_display = T_display
+	afterSales.T_State = 1
+
+	// 保存到数据库
+	id, err := AfterSales.Add_AfterSales(afterSales)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "添加失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "添加成功!", Data: map[string]interface{}{"id": id}}
+	c.ServeJSON()
+	return
+}
+
+// UpdateAfterSales 修改售后服务
+func (c *AfterSalesController) UpdateAfterSales() {
+	id, err := c.GetInt("T_id")
+	if err != nil || id <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "ID参数错误"}
+		c.ServeJSON()
+		return
+	}
+
+	// 先查询记录是否存在
+	afterSales, err := AfterSales.Read_AfterSales_ById(id)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "记录不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	// 更新字段
+	var updateCols []string
+
+	T_name := c.GetString("T_name")
+	if len(T_name) > 0 {
+		afterSales.T_name = T_name
+		updateCols = append(updateCols, "T_name")
+	}
+
+	T_category, err := c.GetInt("T_category")
+	if err == nil && T_category > 0 {
+		// 检查分类是否存在
+		if _, err := AfterSales.Read_AfterSalesCategory_ById(T_category); err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "所选分类不存在"}
+			c.ServeJSON()
+			return
+		}
+		afterSales.T_category = T_category
+		updateCols = append(updateCols, "T_category")
+	}
+
+	T_content := c.GetString("T_content")
+	if len(T_content) > 0 {
+		afterSales.T_content = T_content
+		updateCols = append(updateCols, "T_content")
+	}
+
+	T_attachments := c.GetString("T_attachments")
+	if len(T_attachments) > 0 {
+		// 验证JSON格式
+		var attachments []AfterSales.Attachment
+		err = json.Unmarshal([]byte(T_attachments), &attachments)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "附件格式错误,请传入有效的JSON格式:[{\"name\":\"文件名\",\"url\":\"文件地址\"}]"}
+			c.ServeJSON()
+			return
+		}
+		afterSales.T_attachments = T_attachments
+		updateCols = append(updateCols, "T_attachments")
+	}
+
+	T_sort, err := c.GetInt("T_sort")
+	if err == nil {
+		afterSales.T_sort = T_sort
+		updateCols = append(updateCols, "T_sort")
+	}
+
+	T_display, err := c.GetInt("T_display")
+	if err == nil {
+		afterSales.T_display = T_display
+		updateCols = append(updateCols, "T_display")
+	}
+
+	if len(updateCols) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "没有需要更新的字段"}
+		c.ServeJSON()
+		return
+	}
+
+	updateCols = append(updateCols, "UpdateTime")
+	success := AfterSales.Update_AfterSales(afterSales, updateCols...)
+	if !success {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "修改成功"}
+	c.ServeJSON()
+	return
+}
+
+// DeleteAfterSales 删除售后服务
+func (c *AfterSalesController) DeleteAfterSales() {
+	id, err := c.GetInt("T_id")
+	if err != nil || id <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "ID参数错误"}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询记录是否存在
+	_, err = AfterSales.Read_AfterSales_ById(id)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "记录不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	success := AfterSales.Delete_AfterSales_ById(id)
+	if !success {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "删除失败"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "删除成功"}
+	c.ServeJSON()
+	return
+}
+
+// GetAfterSalesById 根据ID获取售后服务详情
+func (c *AfterSalesController) GetAfterSalesById() {
+	id, err := c.GetInt("T_id")
+	if err != nil || id <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "ID参数错误"}
+		c.ServeJSON()
+		return
+	}
+
+	afterSales, err := AfterSales.Read_AfterSales_ById(id)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "记录不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	result := AfterSales.AfterSalesToAfterSales_R(afterSales)
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "查询成功", Data: result}
+	c.ServeJSON()
+	return
+}
+
+// GetAfterSalesList 获取售后服务列表
+func (c *AfterSalesController) GetAfterSalesList() {
+
+	var r_jsons lib.R_JSONS
+
+	page, _ := c.GetInt("page")
+	if page < 1 {
+		page = 1
+	}
+	page_z, _ := c.GetInt("page_z")
+	if page_z < 1 {
+		page_z = conf.Page_size
+	}
+
+	T_name := c.GetString("T_name")         // 标题搜索
+	T_category, _ := c.GetInt("T_category") // 分类筛选
+	T_display, _ := c.GetInt("T_display")   // 是否显示筛选
+
+	r_jsons.Data, r_jsons.Num = AfterSales.Read_AfterSales_List(T_name, T_category, T_display, page, page_z)
+	r_jsons.Page = page
+	r_jsons.Page_size = int(math.Ceil(float64(r_jsons.Num) / float64(page_z)))
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: r_jsons}
+	c.ServeJSON()
+	return
+}
+
+// GetCategoryList 获取分类列表
+func (c *AfterSalesController) GetCategoryList() {
+	categories := AfterSales.Read_AfterSalesCategory_All()
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: categories}
+	c.ServeJSON()
+	return
+}
+
+// GetCategoryCount 获取各分类的数量统计
+func (c *AfterSalesController) GetCategoryCount() {
+	categories := AfterSales.Read_AfterSalesCategory_All()
+	var result []AfterSales.AfterSalesCategory_Count_R
+
+	for _, category := range categories {
+		count := AfterSales.Read_AfterSales_Count_ByCategoryId(category.Id)
+		categoryWithCount := AfterSales.AfterSalesCategoryToAfterSalesCategory_Count_R(
+			AfterSales.AfterSalesCategory{
+				Id:     category.Id,
+				T_name: category.T_name,
+				T_sort: category.T_sort,
+			}, count)
+		result = append(result, categoryWithCount)
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: result}
+	c.ServeJSON()
+	return
+}
+
+// ------------- 售后服务分类管理接口 -------------
+
+// AddAfterSalesCategory 添加售后服务分类
+func (c *AfterSalesController) AddAfterSalesCategory() {
+	var category AfterSales.AfterSalesCategory
+
+	// 获取参数
+	T_name := c.GetString("T_name")
+	if len(T_name) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "分类名称不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查名称是否重复
+	if AfterSales.Check_AfterSalesCategory_Name_Exists(T_name, 0) {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "分类名称已存在"}
+		c.ServeJSON()
+		return
+	}
+
+	T_sort, _ := c.GetInt("T_sort")
+
+	// 构建数据
+	category.T_name = T_name
+	category.T_sort = T_sort
+	category.T_State = 1
+
+	// 保存到数据库
+	id, err := AfterSales.Add_AfterSalesCategory(category)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "添加失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "添加成功!", Data: map[string]interface{}{"id": id}}
+	c.ServeJSON()
+	return
+}
+
+// UpdateAfterSalesCategory 修改售后服务分类
+func (c *AfterSalesController) UpdateAfterSalesCategory() {
+	id, err := c.GetInt("T_id")
+	if err != nil || id <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "ID参数错误"}
+		c.ServeJSON()
+		return
+	}
+
+	// 先查询记录是否存在
+	category, err := AfterSales.Read_AfterSalesCategory_ById(id)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "记录不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	// 更新字段
+	var updateCols []string
+
+	T_name := c.GetString("T_name")
+	if len(T_name) > 0 {
+		// 检查名称是否重复
+		if AfterSales.Check_AfterSalesCategory_Name_Exists(T_name, id) {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "分类名称已存在"}
+			c.ServeJSON()
+			return
+		}
+		category.T_name = T_name
+		updateCols = append(updateCols, "T_name")
+	}
+
+	T_sort, err := c.GetInt("T_sort")
+	if err == nil {
+		category.T_sort = T_sort
+		updateCols = append(updateCols, "T_sort")
+	}
+
+	if len(updateCols) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "没有需要更新的字段"}
+		c.ServeJSON()
+		return
+	}
+
+	updateCols = append(updateCols, "UpdateTime")
+	success := AfterSales.Update_AfterSalesCategory(category, updateCols...)
+	if !success {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "修改成功"}
+	c.ServeJSON()
+	return
+}
+
+// DeleteAfterSalesCategory 删除售后服务分类
+func (c *AfterSalesController) DeleteAfterSalesCategory() {
+	id, err := c.GetInt("T_id")
+	if err != nil || id <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "ID参数错误"}
+		c.ServeJSON()
+		return
+	}
+
+	// 查询记录是否存在
+	_, err = AfterSales.Read_AfterSalesCategory_ById(id)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "记录不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查是否有售后服务使用此分类
+	count := AfterSales.Read_AfterSales_Count_ByCategoryId(id)
+	if count > 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "该分类下还有售后服务,无法删除"}
+		c.ServeJSON()
+		return
+	}
+
+	success := AfterSales.Delete_AfterSalesCategory_ById(id)
+	if !success {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "删除失败"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "删除成功"}
+	c.ServeJSON()
+	return
+}
+
+// GetAfterSalesCategoryById 根据ID获取售后服务分类详情
+func (c *AfterSalesController) GetAfterSalesCategoryById() {
+	id, err := c.GetInt("T_id")
+	if err != nil || id <= 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "ID参数错误"}
+		c.ServeJSON()
+		return
+	}
+
+	category, err := AfterSales.Read_AfterSalesCategory_ById(id)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "记录不存在"}
+		c.ServeJSON()
+		return
+	}
+
+	result := AfterSales.AfterSalesCategoryToAfterSalesCategory_R(category)
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "查询成功", Data: result}
+	c.ServeJSON()
+	return
+}
+
+// GetAfterSalesCategoryList 获取售后服务分类列表
+func (c *AfterSalesController) GetAfterSalesCategoryList() {
+
+	var r_jsons lib.R_JSONS
+
+	page, _ := c.GetInt("page")
+	if page < 1 {
+		page = 1
+	}
+	page_z, _ := c.GetInt("page_z")
+	if page_z < 1 {
+		page_z = conf.Page_size
+	}
+
+	T_name := c.GetString("T_name") // 名称搜索
+
+	r_jsons.Data, r_jsons.Num = AfterSales.Read_AfterSalesCategory_List(T_name, page, page_z)
+	r_jsons.Page = page
+	r_jsons.Page_size = int(math.Ceil(float64(r_jsons.Num) / float64(page_z)))
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: r_jsons}
+	c.ServeJSON()
+	return
+}
+
+// GetAfterSalesCategoryAll 获取所有售后服务分类(简化版本,用于下拉选择等)
+func (c *AfterSalesController) GetAfterSalesCategoryAll() {
+	result := AfterSales.Read_AfterSalesCategory_All()
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: result}
+	c.ServeJSON()
+	return
+}

+ 2 - 2
controllers/Data.go

@@ -1074,11 +1074,11 @@ func (c *DataController) Device_Sensor_Data_PDF() {
 			lib.RectFillColor(pdf, T_Tlu, 10, 252, y, 80, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
 			lib.RectFillColor(pdf, T_rh, 10, 202, y, 50, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
 			lib.RectFillColor(pdf, T_Rlu, 10, 332, y, 80, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
-			lib.RectFillColor(pdf, v.T_name, 10, 52, y, 100, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+			lib.RectFillColorWithWrap(pdf, v.T_name, 10, 52, y, 100, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
 
 		}
 		if T_ish == 0 {
-			lib.RectFillColor(pdf, v.T_name, 10, 52, y, 140, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
+			lib.RectFillColorWithWrap(pdf, v.T_name, 10, 52, y, 140, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
 			lib.RectFillColor(pdf, T_t, 10, 192, y, 100, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
 			lib.RectFillColor(pdf, T_Tlu, 10, 292, y, 120, 20, 255, 255, 255, lib.AlignCenter, lib.ValignMiddle)
 

+ 235 - 4
controllers/Device.go

@@ -9,12 +9,17 @@ import (
 	"Cold_Api/models/Device"
 	"Cold_Api/models/System"
 	"Cold_Api/models/Warning"
-	beego "github.com/beego/beego/v2/server/web"
-	"github.com/shopspring/decimal"
+	"fmt"
 	"math"
+	"os"
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/beego/beego/v2/core/logs"
+	beego "github.com/beego/beego/v2/server/web"
+	"github.com/shopspring/decimal"
+	"github.com/xuri/excelize/v2"
 )
 
 // Handle
@@ -1140,6 +1145,225 @@ func (c *DeviceController) DeviceSensor_Manage_List() {
 	return
 }
 
+// 导出传感器管理列表
+func (c *DeviceController) DeviceSensor_Manage_List_Excel() {
+
+	T_name := c.GetString("T_name") //  包含 T_name、T_sn
+	T_calss_id, _ := c.GetInt("T_calssid")
+	if T_calss_id > 0 {
+		R_DeviceClass, err := Company.Read_CompanyClass_ById(T_calss_id)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "T_class_id Err!"}
+			c.ServeJSON()
+			return
+		}
+		if R_DeviceClass.T_pid != c.T_pid {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "CompanyClass.T_pid != T_pid Err!"}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	T_en, T_en_err := c.GetInt("T_en") //启用停用 0 停用  1 启用  空 所有
+	if T_en_err != nil {
+		T_en = -1
+	}
+	T_free, T_free_err := c.GetInt("T_free") // 空库 0 正常  1 空库 空 所有
+	if T_free_err != nil {
+		T_free = -1
+	}
+	T_datashow, T_datashow_err := c.GetInt("T_datashow") //数据展示 空 全部  0 屏蔽数据展示  1 正常数据展示
+	if T_datashow_err != nil {
+		T_datashow = -1
+	}
+
+	T_sort, T_sort_err := c.GetInt("T_sort") //排序 空 升序 1 降序
+	if T_sort_err != nil {
+		T_sort = -1
+	}
+
+	T_abandon, _ := c.GetInt("T_abandon")
+
+	bindSN, err := Account.Read_UserDevice_List(*c.Admin_r)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "查询失败!"}
+		c.ServeJSON()
+		return
+	}
+	DeviceSensor_data, _ := Device.Read_DeviceSensorManageList(c.Admin_r, bindSN, c.T_pid, T_name, T_calss_id, T_en, T_free, T_datashow, T_sort, T_abandon, 0, 9999)
+
+	f := excelize.NewFile() //设置单元格值
+	// 这里设置表头ÒÒ
+	Style1, _ := f.NewStyle(
+		&excelize.Style{
+			Font:      &excelize.Font{Size: 20, Family: "宋体"},
+			Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
+		})
+	Style2, _ := f.NewStyle(
+		&excelize.Style{
+			Font:      &excelize.Font{Size: 15, Family: "宋体"},
+			Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
+			Border: []excelize.Border{
+				{Type: "left", Color: "000000", Style: 1},
+				{Type: "top", Color: "000000", Style: 1},
+				{Type: "bottom", Color: "000000", Style: 1},
+				{Type: "right", Color: "000000", Style: 1},
+			},
+			Fill: excelize.Fill{Type: "pattern", Color: []string{"D9D9D9"}, Pattern: 1},
+		})
+	Style3, _ := f.NewStyle(
+		&excelize.Style{
+			Font:      &excelize.Font{Size: 15, Family: "宋体"},
+			Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
+			Border: []excelize.Border{
+				{Type: "left", Color: "000000", Style: 1},
+				{Type: "top", Color: "000000", Style: 1},
+				{Type: "bottom", Color: "000000", Style: 1},
+				{Type: "right", Color: "000000", Style: 1},
+			},
+		})
+
+	f.MergeCell("Sheet1", "A1", "T1")
+	f.SetCellValue("Sheet1", "A1", "设备管理")
+	f.SetCellStyle("Sheet1", "A1", "T1", Style1)
+	// 写入表头
+	headers := []string{"序号", "公司名称", "SN", "编号", "分类", "规格", "传感器名称", "传感器类型", "启/停", "空库", "温度范围(℃)",
+		"湿度范围(%)", "预警", "预警温度范围", "预警湿度范围", "数据展示",
+		"验证日期", "验证到期时间", "校准日期", "校准到期时间"}
+	for col, header := range headers {
+		cell := fmt.Sprintf("%c%d", rune('A'+col), 2)
+		f.SetCellValue("Sheet1", cell, header)
+	}
+	// 这里设置表头
+	f.SetCellStyle("Sheet1", "A2", "T2", Style2)
+	f.SetRowHeight("Sheet1", 2, 25)
+
+	colsWidth := []float64{6, 30, 22, 10, 15, 15, 25, 15, 12, 12, 15, 15, 12, 15, 15, 15, 15, 15, 15, 15}
+
+	for col, width := range colsWidth {
+		cell := fmt.Sprintf("%c", rune('A'+col))
+		f.SetColWidth("Sheet1", cell, cell, width)
+	}
+
+	Company_r, err := Account.Read_Company_ById(c.T_pid)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "获取公司信息失败!"}
+		c.ServeJSON()
+		return
+	}
+
+	line := 2
+	for i, v := range DeviceSensor_data {
+		// 写入数据
+
+		line++
+		f.SetCellValue("Sheet1", fmt.Sprintf("A%d", line), i+1)
+		f.SetCellValue("Sheet1", fmt.Sprintf("B%d", line), Company_r.T_name) // 公司名称
+
+		f.SetCellValue("Sheet1", fmt.Sprintf("C%d", line), v.T_sn)
+		f.SetCellValue("Sheet1", fmt.Sprintf("D%d", line), v.T_id)
+
+		classList, specList := []string{}, []string{}
+		for _, r := range v.T_CompanyClassList {
+			if len(r.T_name) > 0 {
+				classList = append(classList, r.T_name)
+			}
+			if len(r.T_spec) > 0 {
+				specList = append(specList, r.T_spec)
+			}
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("E%d", line), strings.Join(classList, "/")) // 分类
+		f.SetCellValue("Sheet1", fmt.Sprintf("F%d", line), strings.Join(specList, "/"))  // 规格
+
+		f.SetCellValue("Sheet1", fmt.Sprintf("G%d", line), v.T_name)
+		f.SetCellValue("Sheet1", fmt.Sprintf("H%d", line), v.T_type_name)
+
+		T_en_str := "启用"
+		if v.T_en != nil && *v.T_en == 0 {
+			T_en_str = "停用"
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("I%d", line), T_en_str)
+
+		T_free_str := "正常"
+		if v.T_free != nil && *v.T_free == 1 {
+			T_free_str = "停用"
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("J%d", line), T_free_str)
+		T_Tlower, T_Tupper, T_RHlower, T_RHupper := "0", "0", "0", "0"
+		if v.T_Tlower != nil {
+			T_Tlower = fmt.Sprintf("%.0f", *v.T_Tlower)
+		}
+		if v.T_Tupper != nil {
+			T_Tupper = fmt.Sprintf("%.0f", *v.T_Tupper)
+		}
+		if v.T_RHlower != nil {
+			T_RHlower = fmt.Sprintf("%.0f", *v.T_RHlower)
+		}
+		if v.T_RHupper != nil {
+			T_RHupper = fmt.Sprintf("%.0f", *v.T_RHupper)
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("K%d", line), fmt.Sprintf("%s ~ %s", T_Tlower, T_Tupper))
+		f.SetCellValue("Sheet1", fmt.Sprintf("L%d", line), fmt.Sprintf("%s ~ %s", T_RHlower, T_RHupper))
+
+		T_enprel_str := "开启"
+		if v.T_enprel != nil && *v.T_enprel == 0 {
+			T_enprel_str = "关闭"
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("M%d", line), T_enprel_str)
+		T_tprel, T_tpreu, T_hprel, T_hpreu := "0", "0", "0", "0"
+		if v.T_tprel != nil {
+			T_tprel = fmt.Sprintf("%.0f", *v.T_tprel)
+		}
+		if v.T_tpreu != nil {
+			T_tpreu = fmt.Sprintf("%.0f", *v.T_tpreu)
+		}
+		if v.T_hprel != nil {
+			T_hprel = fmt.Sprintf("%.0f", *v.T_hprel)
+		}
+		if v.T_hpreu != nil {
+			T_hpreu = fmt.Sprintf("%.0f", *v.T_hpreu)
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("N%d", line), fmt.Sprintf("%s ~ %s", T_tprel, T_tpreu))
+		f.SetCellValue("Sheet1", fmt.Sprintf("O%d", line), fmt.Sprintf("%s ~ %s", T_hprel, T_hpreu))
+
+		T_datashow_str := "屏蔽"
+		if v.T_datashow == 1 {
+			T_datashow_str = "显示"
+		}
+		f.SetCellValue("Sheet1", fmt.Sprintf("P%d", line), T_datashow_str)
+		f.SetCellValue("Sheet1", fmt.Sprintf("Q%d", line), v.T_Device.T_VerifyTime)
+		f.SetCellValue("Sheet1", fmt.Sprintf("R%d", line), v.T_Device.T_VerifyEndTime)
+		f.SetCellValue("Sheet1", fmt.Sprintf("S%d", line), v.T_Device.T_CalibrationTime)
+		f.SetCellValue("Sheet1", fmt.Sprintf("T%d", line), v.T_Device.T_CalibrationEndTime)
+
+	}
+
+	f.SetCellStyle("Sheet1", "A3", fmt.Sprintf("T%d", line), Style3)
+
+	timeStr := Company_r.T_name + "设备管理(" + lib.GetRandstring(6, "0123456789", 0) + ")"
+	// 保存文件
+	if err = f.SaveAs("ofile/" + timeStr + ".xlsx"); err != nil {
+		logs.Error(err)
+	}
+
+	// 上传 OSS
+	url, is := NatsServer.Qiniu_UploadFile(lib.GetCurrentDirectory()+"/ofile/"+timeStr+".xlsx", "ofile/"+timeStr+".xlsx")
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "oss!"}
+		c.ServeJSON()
+		return
+	}
+	//删除目录
+	err = os.Remove("ofile/" + timeStr + ".xlsx")
+	if err != nil {
+		logs.Error(err)
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: url}
+	c.ServeJSON()
+	return
+}
+
 // 传感器列表-报警策略
 func (c *DeviceController) DeviceSensor_Notice_UnbindList() {
 	type R_JSONS struct {
@@ -1371,10 +1595,12 @@ func (c *DeviceController) CompanyClass_Get() {
 }
 func (c *DeviceController) CompanyClass_Add() {
 	T_name := c.GetString("T_name")
+	T_spec := c.GetString("T_spec")
 	t_c := Company.CompanyClass{
 		T_pid:   c.T_pid,
 		T_name:  T_name,
 		T_State: 1,
+		T_spec:  T_spec,
 	}
 
 	Id, err := Company.Add_CompanyClass(t_c)
@@ -1392,6 +1618,8 @@ func (c *DeviceController) CompanyClass_Add() {
 func (c *DeviceController) CompanyClass_Edit() {
 	T_name := c.GetString("T_name")
 	id, _ := c.GetInt("T_id")
+	T_spec := c.GetString("T_spec")
+
 	R_DeviceClass, err := Company.Read_CompanyClass_ById(id)
 	if err != nil {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "T_id Err!"}
@@ -1407,8 +1635,11 @@ func (c *DeviceController) CompanyClass_Edit() {
 	if len(T_name) > 0 {
 		R_DeviceClass.T_name = T_name
 	}
+	if len(T_name) > 0 {
+		R_DeviceClass.T_spec = T_spec
+	}
 
-	if is := Company.Update_CompanyClass(R_DeviceClass, "T_name"); !is {
+	if is := Company.Update_CompanyClass(R_DeviceClass, "T_name", "T_spec"); !is {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败"}
 		c.ServeJSON()
 		return
@@ -1530,7 +1761,7 @@ func (c *DeviceController) ClassBind_Add() {
 
 	DeviceSensor_class_list := Device.Read_DeviceSensor_ALL_T_sn_T_id_T_Class(T_sn, T_id, T_class_id)
 	if len(DeviceSensor_class_list) > 0 {
-		c.Data["json"] = lib.JSONS{Code: 202, Msg: "重复绑定!"}
+		c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!"}
 		c.ServeJSON()
 		return
 	}

+ 542 - 0
controllers/FileManager.go

@@ -0,0 +1,542 @@
+package controllers
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers/lib"
+	"Cold_Api/models/Account"
+	"Cold_Api/models/FileManager"
+	"encoding/json"
+	"fmt"
+	"math"
+	"path/filepath"
+	"strings"
+
+	"github.com/beego/beego/v2/adapter/orm"
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+type FileManagerController struct {
+	beego.Controller
+	Admin_r Account.Admin // 登陆的用户
+	T_pid   int           // 公司id
+}
+
+func (c *FileManagerController) Prepare() {
+	GetCookie := c.Ctx.GetCookie("User_tokey")
+	GetString := c.GetString("User_tokey")
+	User_tokey := GetString
+	if len(User_tokey) == 0 {
+		User_tokey = GetCookie
+	}
+	if Account.Admin_r == nil {
+		return
+	}
+	c.Admin_r = *Account.Admin_r
+	T_pid := c.T_pid
+	EntryPid, _ := Account.Redis_Tokey_T_pid_Get(User_tokey)
+	if EntryPid > 0 {
+		T_pid = EntryPid
+	}
+	c.T_pid = T_pid
+}
+
+// AddFileManager 添加文件记录(使用form-data格式)
+func (c *FileManagerController) AddFileManager() {
+	// 获取form-data参数
+	filesParam := c.GetString("files")
+	overwriteParam := c.GetString("overwrite")
+	pathParam := c.GetString("path")
+
+	// 定义文件信息结构体
+	type FileInfo struct {
+		Name string `json:"name"` // 文件名
+		Url  string `json:"url"`  // 文件URL
+		Size int64  `json:"size"` // 文件大小
+	}
+
+	// 参数验证
+	if len(filesParam) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件参数不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	// 解析文件列表JSON
+	var files []FileInfo
+	err := json.Unmarshal([]byte(filesParam), &files)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件参数格式错误"}
+		c.ServeJSON()
+		return
+	}
+
+	// 验证文件列表
+	if len(files) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件列表不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	// 解析覆盖参数
+	overwrite := false
+	if overwriteParam == "true" || overwriteParam == "1" {
+		overwrite = true
+	}
+
+	// 验证每个文件的参数
+	var totalSize int64
+	for i, file := range files {
+		if len(file.Name) == 0 {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("第%d个文件名不能为空", i+1)}
+			c.ServeJSON()
+			return
+		}
+		if len(file.Url) == 0 {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("第%d个文件链接不能为空", i+1)}
+			c.ServeJSON()
+			return
+		}
+		if file.Size <= 0 {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("第%d个文件大小必须大于0", i+1)}
+			c.ServeJSON()
+			return
+		}
+		totalSize += file.Size
+	}
+
+	// 检查公司存储空间是否足够
+	if c.T_pid > 0 {
+		available, usedStorage, storageLimit, err := FileManager.Check_Company_Storage_Available(c.T_pid, totalSize)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("检查存储空间失败: %v", err)}
+			c.ServeJSON()
+			return
+		}
+
+		if !available {
+			c.Data["json"] = lib.JSONS{
+				Code: 202,
+				Msg: fmt.Sprintf("存储空间不足,已使用: %s,限制: %s,当前批量文件: %s",
+					FileManager.FormatFileSize(usedStorage),
+					FileManager.FormatFileSize(storageLimit),
+					FileManager.FormatFileSize(totalSize)),
+			}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 处理上传路径
+	T_path := pathParam
+	if len(T_path) == 0 {
+		T_path = "/"
+	}
+	if !strings.HasSuffix(T_path, "/") {
+		T_path += "/"
+	}
+
+	// 自动创建路径中不存在的文件夹
+	if T_path != "/" {
+		err := FileManager.Auto_Create_Folders(c.T_pid, T_path, c.Admin_r.T_uuid)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("创建路径失败: %v", err)}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 批量处理文件
+	var successFiles []FileManager.FileManager_R
+	var failedFiles []map[string]interface{}
+	var successCount int
+
+	for _, fileInfo := range files {
+		// 构建完整路径
+		fullPath := T_path + fileInfo.Name
+
+		// 检查是否存在同名文件
+		if !overwrite && FileManager.Check_FileManager_Name_Exists(c.T_pid, T_path, fileInfo.Name, 0) {
+			failedFiles = append(failedFiles, map[string]interface{}{
+				"name":   fileInfo.Name,
+				"reason": "文件已存在",
+			})
+			continue
+		}
+
+		// 如果覆盖模式,先删除旧文件
+		if overwrite {
+			o := orm.NewOrm()
+			var existingFile FileManager.FileManager
+			err := o.QueryTable(new(FileManager.FileManager)).Filter("T_pid", c.T_pid).Filter("T_path", fullPath).Filter("T_state", 1).One(&existingFile)
+			if err == nil {
+				FileManager.Delete_FileManager_ByUuid(existingFile.T_uuid)
+			}
+		}
+
+		// 保存文件信息到数据库
+		fileManager := FileManager.FileManager{
+			T_pid:         c.T_pid,
+			T_name:        fileInfo.Name,
+			T_path:        fullPath,
+			T_url:         fileInfo.Url,
+			T_size:        fileInfo.Size,
+			T_type:        FileManager.GenerateFileType(fileInfo.Name, fullPath), // 生成文件类型
+			T_uploaded_by: c.Admin_r.T_uuid,
+			T_state:       1,
+		}
+
+		id, err := FileManager.Add_FileManager(fileManager)
+		if err != nil {
+			failedFiles = append(failedFiles, map[string]interface{}{
+				"name":   fileInfo.Name,
+				"reason": fmt.Sprintf("保存失败: %v", err),
+			})
+			continue
+		}
+
+		fileManager.Id = int(id)
+		uploadedFile := FileManager.FileManagerToFileManager_R(fileManager)
+		successFiles = append(successFiles, uploadedFile)
+		successCount++
+	}
+
+	// 构建返回结果
+	response := map[string]interface{}{
+		"success_count": successCount,
+		"failed_count":  len(failedFiles),
+		"total_count":   len(files),
+		"success_files": successFiles,
+		"failed_files":  failedFiles,
+	}
+
+	// 根据结果返回不同的消息
+	if len(failedFiles) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 200, Msg: fmt.Sprintf("所有文件添加成功,共%d个文件", successCount), Data: response}
+	} else if successCount == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "所有文件添加失败", Data: response}
+	} else {
+		c.Data["json"] = lib.JSONS{Code: 200, Msg: fmt.Sprintf("部分文件添加成功,成功%d个,失败%d个", successCount, len(failedFiles)), Data: response}
+	}
+	c.ServeJSON()
+	return
+}
+
+// DeleteFileManager 删除文件(支持递归删除文件夹及其子文件)
+func (c *FileManagerController) DeleteFileManager() {
+	T_path := c.GetString("path")
+	if len(T_path) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件路径不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	// 使用递归删除方法(基于路径)
+	deletedCount, err := FileManager.Delete_FileManager_Recursive_ByPath(c.T_pid, T_path)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("删除失败: %v", err)}
+		c.ServeJSON()
+		return
+	}
+
+	// 根据删除数量返回不同的成功消息
+	var msg string
+	if deletedCount > 1 {
+		msg = fmt.Sprintf("删除成功,共删除 %d 个文件/文件夹", deletedCount)
+	} else {
+		msg = "删除成功"
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: msg, Data: map[string]interface{}{"deletedCount": deletedCount}}
+	c.ServeJSON()
+	return
+}
+
+// CheckFileExists 检查文件列表是否存在
+func (c *FileManagerController) CheckFileExists() {
+	// 获取路径列表参数
+	pathsParam := c.GetString("paths")
+	if len(pathsParam) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "路径列表参数不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	// 解析路径列表(支持JSON数组格式)
+	var paths []string
+	if err := json.Unmarshal([]byte(pathsParam), &paths); err != nil {
+		// 如果不是JSON格式,按逗号分割
+		paths = strings.Split(pathsParam, ",")
+	}
+
+	// 存储已存在的路径信息
+	var existingFiles []map[string]interface{}
+
+	// 检查每个路径
+	for _, path := range paths {
+		path = strings.TrimSpace(path)
+		if len(path) == 0 {
+			continue
+		}
+
+		// 检查路径是否存在
+		if exists := FileManager.Check_FileManager_Exists_ByPath(c.T_pid, path); exists {
+			// 获取文件详细信息
+			if file, err := FileManager.Read_FileManager_ByPath(c.T_pid, path); err == nil {
+				// 构建返回格式
+				fileInfo := map[string]interface{}{
+					"name":    file.T_name,
+					"path":    file.T_path,
+					"size":    file.T_size, // 使用实际文件大小
+					"modTime": file.CreateTime.Format("2006-01-02 15:04:05"),
+					"isDir":   strings.HasSuffix(file.T_path, "/"),
+				}
+				existingFiles = append(existingFiles, fileInfo)
+			}
+		}
+	}
+
+	// 返回已存在的文件列表
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "success", Data: existingFiles}
+	c.ServeJSON()
+	return
+}
+
+// GetFileManagerList 获取文件列表
+func (c *FileManagerController) GetFileManagerList() {
+
+	page, _ := c.GetInt("page")
+
+	if page < 1 {
+		page = 1
+	}
+	page_z, _ := c.GetInt("page_z")
+	if page_z < 1 || page_z > 1000 {
+		page_z = conf.Page_size
+	}
+
+	// 获取查询参数
+	path := c.GetString("path", "/")
+	search := c.GetString("search", "")                // 搜索关键词
+	sortBy := c.GetString("sortBy", "name")            // 排序字段
+	sortOrder := c.GetString("sortOrder", "ascending") // 排序方向
+
+	// 使用简化版查询方法(只查询当前路径直接子项)
+	files, total := FileManager.Read_FileManager_List_Simple(
+		c.T_pid,
+		path,
+		search,
+		sortBy,
+		sortOrder,
+		page,
+		page_z,
+	)
+
+	// 计算分页信息
+	totalPages := int(math.Ceil(float64(total) / float64(page_z)))
+
+	// 构建响应数据
+	response := map[string]interface{}{
+		"Data":       files,
+		"Num":        total,
+		"page":       page,
+		"Page_size":  page_z,
+		"totalPages": totalPages,
+		"path":       path,
+		"sortBy":     sortBy,
+		"sortOrder":  sortOrder,
+		"breadcrumb": FileManager.Get_FileManager_Breadcrumb(path),
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "success", Data: response}
+	c.ServeJSON()
+	return
+}
+
+// ------------ 文件夹相关接口 ------------
+
+// AddFolder 创建文件夹
+func (c *FileManagerController) AddFolder() {
+	T_name := c.GetString("T_name")
+	if len(T_name) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件夹名称不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	T_path := c.GetString("path", "/")
+	if !strings.HasSuffix(T_path, "/") {
+		T_path += "/"
+	}
+
+	// 自动创建父级路径中不存在的文件夹
+	if T_path != "/" {
+		err := FileManager.Auto_Create_Folders(c.T_pid, T_path, c.Admin_r.T_uuid)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("创建父级路径失败: %v", err)}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 检查同级目录下是否存在同名文件夹
+	if FileManager.Check_FileManager_Name_Exists(c.T_pid, T_path, T_name, 0) {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "该目录下已存在同名文件夹"}
+		c.ServeJSON()
+		return
+	}
+
+	// 构建文件夹完整路径
+	folderPath := T_path + T_name + "/"
+
+	// 创建文件夹
+	id, err := FileManager.Add_FileManager_Folder(c.T_pid, T_name, folderPath, c.Admin_r.T_uuid)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "创建文件夹失败"}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "创建成功", Data: map[string]interface{}{"id": id, "path": folderPath}}
+	c.ServeJSON()
+	return
+}
+
+// GetBreadcrumb 获取路径面包屑
+func (c *FileManagerController) GetBreadcrumb() {
+	T_path := c.GetString("path", "/")
+	breadcrumb := FileManager.Get_FileManager_Breadcrumb(T_path)
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "success", Data: breadcrumb}
+	c.ServeJSON()
+	return
+}
+
+// RenameFile 重命名文件或文件夹
+func (c *FileManagerController) RenameFile() {
+	// 支持两种参数格式:
+	// 1. 传统的id + T_name格式
+	// 2. 新的oldName + newName格式(参考API)
+
+	oldName := c.GetString("oldName")
+	newName := c.GetString("newName")
+	path := c.GetString("path")
+
+	// 如果使用新格式
+	if len(oldName) > 0 && len(newName) > 0 {
+		c.renameByPath(oldName, newName, path)
+		return
+	}
+}
+
+// renameByPath 基于路径的重命名方法
+func (c *FileManagerController) renameByPath(oldPath, newPath, parentPath string) {
+	// 参数验证
+	if len(oldPath) == 0 || len(newPath) == 0 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "路径参数不能为空"}
+		c.ServeJSON()
+		return
+	}
+
+	// 从完整路径中提取文件名
+	newName := filepath.Base(newPath)
+
+	// 确保路径以/开头
+	if !strings.HasPrefix(oldPath, "/") {
+		oldPath = "/" + oldPath
+	}
+	if !strings.HasPrefix(newPath, "/") {
+		newPath = "/" + newPath
+	}
+
+	// 查找要重命名的文件/文件夹
+	file, err := FileManager.Read_FileManager_ByPath(c.T_pid, oldPath)
+	if err != nil {
+		// 如果是文件夹,尝试添加/后缀
+		if !strings.HasSuffix(oldPath, "/") {
+			file, err = FileManager.Read_FileManager_ByPath(c.T_pid, oldPath+"/")
+			if err != nil {
+				c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件或文件夹不存在"}
+				c.ServeJSON()
+				return
+			}
+			oldPath += "/"
+			newPath += "/"
+		} else {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件或文件夹不存在"}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	// 验证权限
+	if file.T_pid != c.T_pid {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "权限不足"}
+		c.ServeJSON()
+		return
+	}
+
+	// 检查新路径是否已存在
+	exists := FileManager.Check_FileManager_Exists_ByPath(c.T_pid, newPath)
+	if exists {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "目标路径已存在"}
+		c.ServeJSON()
+		return
+	}
+
+	// 执行重命名
+	if strings.HasSuffix(oldPath, "/") { // 文件夹
+		err = FileManager.Update_Folder_And_Children_Path(c.T_pid, oldPath, newPath, newName)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("重命名文件夹失败: %v", err)}
+			c.ServeJSON()
+			return
+		}
+	} else { // 文件
+		file.T_name = newName
+		file.T_path = newPath
+		success := FileManager.Update_FileManager(file, "T_name", "T_path", "UpdateTime")
+		if !success {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "重命名失败"}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "重命名成功"}
+	c.ServeJSON()
+	return
+}
+
+// GetStorageInfo 获取公司存储使用情况
+func (c *FileManagerController) GetStorageInfo() {
+	// 获取存储信息
+	storageInfo, err := FileManager.Get_Company_Storage_Info(c.T_pid)
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("获取存储信息失败: %v", err)}
+		c.ServeJSON()
+		return
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "success", Data: storageInfo}
+	c.ServeJSON()
+	return
+}
+
+// GetUploadToken 获取七牛云上传Token
+func (c *FileManagerController) GetUploadToken() {
+	// 获取文件后缀参数
+	fileSuffix := c.GetString("suffix", "")
+
+	// 生成上传Token
+	uploadToken := lib.UploadToken(fileSuffix)
+
+	c.Data["json"] = lib.JSONS{
+		Code: 200,
+		Msg:  "success",
+		Data: map[string]interface{}{
+			"token":  uploadToken,
+			"domain": "", // 七牛云访问域名(如需要可从配置获取)
+		},
+	}
+	c.ServeJSON()
+	return
+}

+ 58 - 1
controllers/UpFile.go

@@ -2,11 +2,28 @@ package controllers
 
 import (
 	"Cold_Api/controllers/lib"
+	"Cold_Api/models/Account"
+	"fmt"
+
 	beego "github.com/beego/beego/v2/server/web"
 )
 
 type UpFileController struct {
 	beego.Controller
+	Admin_r Account.Admin // 登陆的用户
+}
+
+func (c *UpFileController) Prepare() {
+	GetCookie := c.Ctx.GetCookie("User_tokey")
+	GetString := c.GetString("User_tokey")
+	User_tokey := GetString
+	if len(User_tokey) == 0 {
+		User_tokey = GetCookie
+	}
+	if Account.Admin_r == nil {
+		return
+	}
+	c.Admin_r = *Account.Admin_r
 }
 
 //func (c *UpFileController) UpFile() {
@@ -49,9 +66,49 @@ type UpFileController struct {
 //	c.Ctx.WriteString("{\n  \"code\": 0\n  ,\"msg\": \"ok!\"\n  ,\"data\": {\n    \"src\": \"" + conf.Oss + fileName + "\"\n  }\n}       ")
 //}
 
-// 列表 -
+// ConfigUpFileToken 获取七牛云上传Token
 func (c *UpFileController) ConfigUpFileToken() {
 	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: lib.UploadToken(c.GetString("T_suffix"))}
 	c.ServeJSON()
 	return
 }
+
+// UploadFile 公共文件上传接口,上传文件到七牛云并返回七牛云文件路径
+func (c *UpFileController) UploadFile() {
+	// 获取上传的文件(单文件模式)
+	file, fileHeader, err := c.GetFile("file")
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "请选择要上传的文件"}
+		c.ServeJSON()
+		return
+	}
+	defer file.Close()
+
+	// 验证文件大小(500M限制)
+	if fileHeader.Size > 1024*1024*500 {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "文件大小超过500M限制"}
+		c.ServeJSON()
+		return
+	}
+
+	// 上传到七牛云
+	fileUrl, err := lib.UploadFileToQiniu(fileHeader, "uploads/")
+	if err != nil {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("上传文件 %s 到七牛云失败: %v", fileHeader.Filename, err)}
+		c.ServeJSON()
+		return
+	}
+
+	// 返回成功结果
+	c.Data["json"] = lib.JSONS{
+		Code: 200,
+		Msg:  "上传成功!",
+		Data: map[string]interface{}{
+			"url":  fileUrl,
+			"name": fileHeader.Filename,
+			"size": fileHeader.Size,
+		},
+	}
+	c.ServeJSON()
+	return
+}

+ 118 - 31
controllers/User.go

@@ -11,17 +11,18 @@ import (
 	"Cold_Api/models/Warning"
 	"encoding/json"
 	"fmt"
+	"math"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
 	"github.com/beego/beego/v2/adapter/orm"
 	"github.com/beego/beego/v2/core/logs"
 	beego "github.com/beego/beego/v2/server/web"
 	uuid "github.com/satori/go.uuid"
 	"github.com/shopspring/decimal"
 	"github.com/xuri/excelize/v2"
-	"math"
-	"os"
-	"strconv"
-	"strings"
-	"time"
 )
 
 type UserController struct {
@@ -195,6 +196,7 @@ func (c *UserController) Company_Edit() {
 	T_type, _ := c.GetInt("T_type")
 	T_Address := c.GetString("T_Address")
 	T_coordinate := c.GetString("T_coordinate")
+	T_file_size_limit, _ := c.GetInt64("T_file_size_limit")
 	if len(T_name) > 0 {
 		Company_r.T_name = T_name
 	}
@@ -207,8 +209,11 @@ func (c *UserController) Company_Edit() {
 	if len(T_coordinate) > 0 {
 		Company_r.T_coordinate = T_coordinate
 	}
+	if T_file_size_limit >= 0 {
+		Company_r.T_file_size_limit = T_file_size_limit
+	}
 
-	is := Account.Update_Company(Company_r, "T_name", "T_type", "T_Address", "T_coordinate")
+	is := Account.Update_Company(Company_r, "T_name", "T_type", "T_Address", "T_coordinate", "T_file_size_limit")
 	if !is {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败!"}
 		c.ServeJSON()
@@ -597,7 +602,7 @@ func (c *UserController) User_Get() {
 		return
 	}
 
-	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: Account.AdminToAdmin_R(admin_r)}
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: Account.AdminToAdmin_Details(admin_r)}
 	c.ServeJSON()
 	return
 }
@@ -624,6 +629,7 @@ func (c *UserController) User_Post() {
 	admin_r := c.Admin_r
 	T_name := c.GetString("T_name")
 	T_pass := c.GetString("T_pass")
+	T_pass_str := c.GetString("T_pass_str")
 	T_phone := c.GetString("T_phone")
 	T_mail := c.GetString("T_mail")
 	T_wx := c.GetString("T_wx")
@@ -638,6 +644,9 @@ func (c *UserController) User_Post() {
 	if len(T_pass) >= 8 {
 		admin_r.T_pass = T_pass
 	}
+	if len(T_pass_str) > 0 {
+		admin_r.T_pass_str = T_pass_str
+	}
 	if len(T_phone) > 0 {
 		admin_r.T_phone = T_phone
 	}
@@ -650,7 +659,7 @@ func (c *UserController) User_Post() {
 		}
 		admin_r.T_wx = T_wx
 	}
-	is := Account.Update_Admin(admin_r, "T_name", "T_pass", "T_phone", "T_mail", "T_wx")
+	is := Account.Update_Admin(admin_r, "T_name", "T_pass", "T_pass_str", "T_phone", "T_mail", "T_wx")
 	if !is {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败!"}
 		c.ServeJSON()
@@ -669,21 +678,23 @@ func (c *UserController) User_Add() {
 	T_name := c.GetString("T_name")
 	T_user := c.GetString("T_user")
 	T_pass := c.GetString("T_pass")
+	T_pass_str := c.GetString("T_pass_str")
 	T_phone := c.GetString("T_phone")
 	T_mail := c.GetString("T_mail")
 	T_wx := c.GetString("T_wx")
 	T_uuid := uuid.NewV4().String()
 	var_ := Account.Admin{
-		T_uuid:  T_uuid,
-		T_pid:   c.T_pid,
-		T_name:  T_name,
-		T_user:  T_user,
-		T_phone: T_phone,
-		T_mail:  T_mail,
-		T_wx:    T_wx,
-		T_pass:  T_pass,
-		T_power: T_power,
-		T_State: 1,
+		T_uuid:     T_uuid,
+		T_pid:      c.T_pid,
+		T_name:     T_name,
+		T_user:     T_user,
+		T_phone:    T_phone,
+		T_mail:     T_mail,
+		T_wx:       T_wx,
+		T_pass:     T_pass,
+		T_pass_str: T_pass_str,
+		T_power:    T_power,
+		T_State:    1,
 	}
 	if len(T_pass) < 8 {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "密码异常!"}
@@ -730,6 +741,7 @@ func (c *UserController) User_Edit() {
 	T_power, T_power_err := c.GetInt("T_power")
 	T_name := c.GetString("T_name")
 	T_pass := c.GetString("T_pass")
+	T_pass_str := c.GetString("T_pass_str")
 	T_phone := c.GetString("T_phone")
 	T_mail := c.GetString("T_mail")
 	if T_power_err == nil {
@@ -748,6 +760,9 @@ func (c *UserController) User_Edit() {
 	if len(T_pass) >= 8 {
 		r.T_pass = T_pass
 	}
+	if len(T_pass_str) > 0 {
+		r.T_pass_str = T_pass_str
+	}
 	if len(T_phone) > 0 {
 		r.T_phone = T_phone
 	}
@@ -755,7 +770,7 @@ func (c *UserController) User_Edit() {
 		r.T_mail = T_mail
 	}
 
-	is := Account.Update_Admin(r, "T_power", "T_pid", "T_name", "T_pass", "T_phone", "T_mail")
+	is := Account.Update_Admin(r, "T_power", "T_pid", "T_name", "T_pass", "T_pass_str", "T_phone", "T_mail")
 	if !is {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败!"}
 		c.ServeJSON()
@@ -1022,7 +1037,7 @@ func (c *UserController) Admin_Get() {
 		return
 	}
 
-	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: Account.AdminToAdmin_R(admin_r)}
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: Account.AdminToAdmin_Details(admin_r)}
 	c.ServeJSON()
 	return
 }
@@ -1032,21 +1047,23 @@ func (c *UserController) Admin_Add() {
 	T_name := c.GetString("T_name")
 	T_user := c.GetString("T_user")
 	T_pass := c.GetString("T_pass")
+	T_pass_str := c.GetString("T_pass_str")
 	T_phone := c.GetString("T_phone")
 	T_mail := c.GetString("T_mail")
 	T_wx := c.GetString("T_wx")
 
 	var_ := Account.Admin{
-		T_uuid:  uuid.NewV4().String(),
-		T_pid:   0,
-		T_name:  T_name,
-		T_user:  T_user,
-		T_phone: T_phone,
-		T_mail:  T_mail,
-		T_wx:    T_wx,
-		T_pass:  T_pass,
-		T_power: T_power,
-		T_State: 1,
+		T_uuid:     uuid.NewV4().String(),
+		T_pid:      0,
+		T_name:     T_name,
+		T_user:     T_user,
+		T_phone:    T_phone,
+		T_mail:     T_mail,
+		T_wx:       T_wx,
+		T_pass:     T_pass,
+		T_pass_str: T_pass_str,
+		T_power:    T_power,
+		T_State:    1,
 	}
 
 	if len(T_pass) < 8 {
@@ -1094,6 +1111,7 @@ func (c *UserController) Admin_Edit() {
 	T_user := c.GetString("T_user")
 	T_name := c.GetString("T_name")
 	T_pass := c.GetString("T_pass")
+	T_pass_str := c.GetString("T_pass_str")
 	T_phone := c.GetString("T_phone")
 	T_mail := c.GetString("T_mail")
 	T_wx := c.GetString("T_wx")
@@ -1122,6 +1140,9 @@ func (c *UserController) Admin_Edit() {
 	if len(T_pass) >= 8 {
 		r.T_pass = T_pass
 	}
+	if len(T_pass_str) > 0 {
+		r.T_pass_str = T_pass_str
+	}
 	if len(T_phone) > 0 {
 		r.T_phone = T_phone
 	}
@@ -1132,7 +1153,7 @@ func (c *UserController) Admin_Edit() {
 		r.T_wx = T_wx
 	}
 
-	is := Account.Update_Admin(r, "T_user", "T_power", "T_name", "T_pass", "T_phone", "T_mail", "T_wx")
+	is := Account.Update_Admin(r, "T_user", "T_power", "T_name", "T_pass", "T_pass_str", "T_phone", "T_mail", "T_wx")
 	if !is {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "修改失败!"}
 		c.ServeJSON()
@@ -1425,6 +1446,70 @@ func (c *UserController) Power_List_All() {
 	return
 }
 
+// removeIncompleteParentMenus 移除不完整的父菜单ID
+// 递归检查多级菜单的完整性,确保父菜单只有在所有子孙菜单都被勾选时才保留
+func removeIncompleteParentMenus(checkedIds []int, allMenus []Account.Menu) []int {
+	// 创建已勾选菜单ID的映射,便于快速查找
+	checkedMap := make(map[int]bool)
+	for _, id := range checkedIds {
+		checkedMap[id] = true
+	}
+
+	// 递归获取所有菜单及其父子关系
+	parentChildrenMap := make(map[int][]int)
+	buildMenuParentChildrenMap(allMenus, parentChildrenMap)
+
+	// 递归检查每个菜单的完整性,从叶子节点开始向上检查
+	result := make([]int, 0)
+	for _, menuId := range checkedIds {
+		if shouldKeepMenu(menuId, checkedMap, parentChildrenMap) {
+			result = append(result, menuId)
+		}
+	}
+
+	return result
+}
+
+// shouldKeepMenu 递归检查菜单是否应该保留
+// 对于父菜单,只有当其所有子菜单(及子孙菜单)都满足条件时才保留
+func shouldKeepMenu(menuId int, checkedMap map[int]bool, parentChildrenMap map[int][]int) bool {
+	// 如果菜单没有被勾选,直接返回false
+	if !checkedMap[menuId] {
+		return false
+	}
+
+	// 如果是叶子菜单(没有子菜单),直接保留
+	children, hasChildren := parentChildrenMap[menuId]
+	if !hasChildren {
+		return true
+	}
+
+	// 如果是父菜单,检查所有子菜单是否都满足条件
+	for _, childId := range children {
+		// 递归检查每个子菜单
+		if !shouldKeepMenu(childId, checkedMap, parentChildrenMap) {
+			return false // 只要有一个子菜单不满足条件,父菜单就不保留
+		}
+	}
+
+	// 所有子菜单都满足条件,保留父菜单
+	return true
+}
+
+// buildMenuParentChildrenMap 构建父子关系映射
+func buildMenuParentChildrenMap(menus []Account.Menu, parentChildrenMap map[int][]int) {
+	for _, menu := range menus {
+		// 如果有子菜单,建立父子关系
+		if len(menu.Children) > 0 {
+			for _, child := range menu.Children {
+				parentChildrenMap[menu.Id] = append(parentChildrenMap[menu.Id], child.Id)
+			}
+			// 递归处理子菜单
+			buildMenuParentChildrenMap(menu.Children, parentChildrenMap)
+		}
+	}
+}
+
 func (c *UserController) Power_Get() {
 	type Data struct {
 		Power        Account.Power_
@@ -1460,6 +1545,8 @@ func (c *UserController) Power_Get() {
 
 	if power.T_menu != "*" {
 		data.Menu_checked = lib.SplitStringToIntIds(power.T_menu, "M")
+		// 检查父菜单完整性,移除不完整的父菜单ID
+		data.Menu_checked = removeIncompleteParentMenus(data.Menu_checked, data.Menu)
 	}
 
 	if err != nil {

+ 54 - 13
controllers/Warning.go

@@ -11,14 +11,15 @@ import (
 	"Cold_Api/models/Warning"
 	"encoding/json"
 	"fmt"
-	"github.com/beego/beego/v2/core/logs"
-	"github.com/robfig/cron/v3"
-	"github.com/xuri/excelize/v2"
 	"math"
 	"net/http"
 	"os"
 	"strconv"
 	"time"
+
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/robfig/cron/v3"
+	"github.com/xuri/excelize/v2"
 )
 
 // 设备告警 ------------------------------------------
@@ -541,13 +542,25 @@ func (c *DeviceController) DeviceWarning_Post() {
 	id, _ := c.GetInt("T_id")
 	T_Text := c.GetString("T_Text")
 	T_time := c.GetString("T_time")
-	T_history, _ := c.GetInt("T_history") // TODO 后期废弃
+	T_attachments := c.GetString("T_attachments") // 处理凭证(图片链接)
+	T_history, _ := c.GetInt("T_history")         // TODO 后期废弃
 	if len(T_time) == 0 {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "时间格式错误"}
 		c.ServeJSON()
 		return
 	}
 
+	// 解析附件数据
+	var attachments []Warning.Attachment
+	if len(T_attachments) > 0 {
+		err := json.Unmarshal([]byte(T_attachments), &attachments)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "附件数据格式错误"}
+			c.ServeJSON()
+			return
+		}
+	}
+
 	var T_year, T_month string
 	var warning Warning.Warning
 	Wtab := "warning"
@@ -566,7 +579,8 @@ func (c *DeviceController) DeviceWarning_Post() {
 			c.ServeJSON()
 			return
 		}
-		text := Warning.AddHandlingRecord(warning, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid)
+		// 使用带附件的处理记录方法
+		text := Warning.AddHandlingRecordWithAttachments(warning, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid, attachments)
 		warning.T_Text = text
 		warning.T_State = 2
 		warning.UpdateTime = time.Now()
@@ -578,7 +592,8 @@ func (c *DeviceController) DeviceWarning_Post() {
 
 		warning = Warning.Read_Warning_ById(int64(id))
 		if warning.Id > 0 {
-			text = Warning.AddHandlingRecord(warning, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid)
+			// 使用带附件的处理记录方法
+			text = Warning.AddHandlingRecordWithAttachments(warning, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid, attachments)
 			warning.T_Text = text
 			warning.T_State = 2
 			warning.UpdateTime = time.Now()
@@ -594,7 +609,8 @@ func (c *DeviceController) DeviceWarning_Post() {
 
 		warning = Warning.Read_Warning_ById(int64(id))
 		if warning.Id > 0 {
-			text := Warning.AddHandlingRecord(warning, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid)
+			// 使用带附件的处理记录方法
+			text := Warning.AddHandlingRecordWithAttachments(warning, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid, attachments)
 			warning.T_Text = text
 			warning.T_State = 2
 			warning.UpdateTime = time.Now()
@@ -616,7 +632,8 @@ func (c *DeviceController) DeviceWarning_Post() {
 			return
 		}
 		if warningBackups.Id > 0 {
-			text := Warning.AddHandlingRecord(warningBackups, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid)
+			// 使用带附件的处理记录方法
+			text := Warning.AddHandlingRecordWithAttachments(warningBackups, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid, attachments)
 			warningBackups.T_Text = text
 			warningBackups.T_State = 2
 			warningBackups.UpdateTime = time.Now()
@@ -632,7 +649,12 @@ func (c *DeviceController) DeviceWarning_Post() {
 	Warning.Redis_WarningCount_DelK(c.T_pid)
 	Warning.Redis_WarningCount_DelK(c.Admin_r.Id)
 
-	System.Add_UserLogs(c.Admin_r.T_uuid, "设备管理", "报警处理操作", Wtab+":"+strconv.Itoa(id)+"->"+T_Text)
+	// 记录操作日志,包含附件信息
+	logMsg := Wtab + ":" + strconv.Itoa(id) + "->" + T_Text
+	if len(attachments) > 0 {
+		logMsg += fmt.Sprintf(", 附件数量: %d", len(attachments))
+	}
+	System.Add_UserLogs(c.Admin_r.T_uuid, "设备管理", "报警处理操作", logMsg)
 	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!"}
 	c.ServeJSON()
 	return
@@ -640,8 +662,9 @@ func (c *DeviceController) DeviceWarning_Post() {
 
 // AppBatchWarning APP批量处理报警信息
 func (c *DeviceController) AppBatchWarning() {
-	T_Text := c.GetString("T_Text") //处理措施
-	T_tp, _ := c.GetInt("T_tp")     //处理类型
+	T_Text := c.GetString("T_Text")               //处理措施
+	T_tp, _ := c.GetInt("T_tp")                   //处理类型
+	T_attachments := c.GetString("T_attachments") // 处理凭证(图片链接)
 	if len(T_Text) == 0 {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "处理措施不能为空"}
 		c.ServeJSON()
@@ -652,7 +675,19 @@ func (c *DeviceController) AppBatchWarning() {
 		c.ServeJSON()
 		return
 	}
-	handle := Warning.BatchHandle(c.T_pid, T_tp, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid)
+
+	// 解析附件数据
+	var attachments []Warning.Attachment
+	if len(T_attachments) > 0 {
+		err := json.Unmarshal([]byte(T_attachments), &attachments)
+		if err != nil {
+			c.Data["json"] = lib.JSONS{Code: 202, Msg: "附件数据格式错误"}
+			c.ServeJSON()
+			return
+		}
+	}
+
+	handle := Warning.BatchHandleWithAttachments(c.T_pid, T_tp, T_Text, c.Admin_r.T_name, c.Admin_r.T_uuid, attachments)
 	if !handle {
 		c.Data["json"] = lib.JSONS{Code: 202, Msg: "处理报警信息失败"}
 		c.ServeJSON()
@@ -661,7 +696,13 @@ func (c *DeviceController) AppBatchWarning() {
 	// 删除报警列表统计的缓存
 	Warning.Redis_WarningCount_DelK(c.T_pid)
 	Warning.Redis_WarningCount_DelK(c.Admin_r.Id)
-	System.Add_UserLogs(c.Admin_r.T_uuid, "设备管理", "app批量处理报警", T_Text)
+
+	// 记录操作日志,包含附件信息
+	logMsg := T_Text
+	if len(attachments) > 0 {
+		logMsg += fmt.Sprintf(", 附件数量: %d", len(attachments))
+	}
+	System.Add_UserLogs(c.Admin_r.T_uuid, "设备管理", "app批量处理报警", logMsg)
 	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!"}
 	c.ServeJSON()
 	return

+ 107 - 2
controllers/lib/GoPDF.go

@@ -14,14 +14,119 @@ const (
 	AlignRight  = 6
 )
 
-func RectFillColor(pdf *gopdf.GoPdf,
+// RectFillColorWithWrap 支持文字换行的矩形填充函数
+func RectFillColorWithWrap(pdf *gopdf.GoPdf,
 	text string,
 	fontSize int,
 	x, y, w, h float64,
 	r, g, b uint8,
 	align, valign int,
 ) {
+	// 绘制背景矩形
+	pdf.SetLineWidth(0.1)
+	pdf.SetFillColor(r, g, b) //setup fill color
+	pdf.SetLineType("")       // 线条样式
+	pdf.RectFromUpperLeftWithStyle(x, y, w, h, "FD")
+	pdf.SetFillColor(0, 0, 0)
+
+	// 测量文字宽度,判断是否需要换行
+	textw, _ := pdf.MeasureTextWidth(text)
+
+	// 如果文字宽度超过单元格宽度,进行换行处理
+	if textw > w-4 { // 预留一些边距
+		// 换行时使用较小的字体,为上下留出空白
+		adjustedFontSize := fontSize - 2
+		if adjustedFontSize < 8 {
+			adjustedFontSize = 8 // 最小字体大小
+		}
+
+		// 设置调整后的字体大小
+		currentFont := "wts" // 假设当前使用的字体名称
+		pdf.SetFont(currentFont, "", adjustedFontSize)
+
+		// 将文字按字符分割,逐个测量直到超出宽度
+		runes := []rune(text)
+		lines := []string{}
+		currentLine := ""
+
+		for _, r := range runes {
+			testLine := currentLine + string(r)
+			testWidth, _ := pdf.MeasureTextWidth(testLine)
+
+			if testWidth > w-4 && currentLine != "" {
+				// 当前行已满,开始新行
+				lines = append(lines, currentLine)
+				currentLine = string(r)
+			} else {
+				currentLine = testLine
+			}
+		}
+
+		// 添加最后一行
+		if currentLine != "" {
+			lines = append(lines, currentLine)
+		}
+
+		// 计算行高和总高度,为上下留出空白
+		lineHeight := float64(adjustedFontSize) + 1 // 行间距稍微小一点
+		totalTextHeight := float64(len(lines)) * lineHeight
+		padding := 2.0 // 上下各留2个单位的空白
+		startY := y + padding
+
+		if valign == ValignMiddle {
+			startY = y + (h / 2) - (totalTextHeight / 2)
+		} else if valign == ValignBottom {
+			startY = y + h - totalTextHeight - padding
+		}
 
+		// 绘制每一行
+		for i, line := range lines {
+			lineY := startY + float64(i)*lineHeight
+			lineX := x + 2 // 预留左边距
+
+			if align == AlignCenter {
+				lineWidth, _ := pdf.MeasureTextWidth(line)
+				lineX = x + (w / 2) - (lineWidth / 2)
+			} else if align == AlignRight {
+				lineWidth, _ := pdf.MeasureTextWidth(line)
+				lineX = x + w - lineWidth - 2
+			}
+
+			pdf.SetX(lineX)
+			pdf.SetY(lineY)
+			pdf.Cell(nil, line)
+		}
+
+		// 恢复原来的字体大小
+		pdf.SetFont(currentFont, "", fontSize)
+	} else {
+		// 文字不需要换行,使用原来的逻辑
+		if align == AlignCenter {
+			x = x + (w / 2) - (textw / 2)
+		} else if align == AlignRight {
+			x = x + w - textw
+		}
+
+		pdf.SetX(x)
+
+		if valign == ValignMiddle {
+			y = y + (h / 2) - (float64(fontSize) / 2)
+		} else if valign == ValignBottom {
+			y = y + h - float64(fontSize)
+		}
+
+		pdf.SetY(y)
+		pdf.Cell(nil, text)
+	}
+}
+
+func RectFillColor(pdf *gopdf.GoPdf,
+	text string,
+	fontSize int,
+	x, y, w, h float64,
+	r, g, b uint8,
+	align, valign int,
+) {
 
 	pdf.SetLineWidth(0.1)
 	pdf.SetFillColor(r, g, b) //setup fill color
@@ -47,4 +152,4 @@ func RectFillColor(pdf *gopdf.GoPdf,
 
 	pdf.SetY(y)
 	pdf.Cell(nil, text)
-}
+}

+ 88 - 4
controllers/lib/Qiniu.go

@@ -4,12 +4,18 @@ package lib
 import (
 	"Cold_Api/conf"
 	"context"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"os"
+	"path/filepath"
+	"strconv"
+	"time"
+
 	"github.com/beego/beego/v2/core/logs"
 	"github.com/qiniu/go-sdk/v7/auth/qbox"
 	"github.com/qiniu/go-sdk/v7/storage"
 	uuid "github.com/satori/go.uuid"
-	"strconv"
-	"time"
 )
 
 var Qiniu *qbox.Mac
@@ -82,10 +88,88 @@ func UploadToken(T_suffix string) string {
 		//{"key":"` + conf.Oss + `/$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}
 		ForceSaveKey: true,
 		SaveKey:      "UpImage/" + Tokey + "." + T_suffix,
-		FsizeLimit:   1024 * 1024 * 100,
-		MimeLimit:    "image/*;application/pdf;application/octet-stream;application/zip;application/x-tar;application/msword",
+		FsizeLimit:   1024 * 1024 * 500, // 500M 文件大小限制,后续可注释此行不限制大小
+		// MimeLimit:    "image/*;application/pdf;application/octet-stream;application/zip;application/x-tar;application/msword;video/mp4", // 增加MP4格式支持,后续可注释此行不限制类型
 	}
 
 	upToken := putPolicy.UploadToken(Qiniu)
 	return upToken
 }
+
+// UploadFileToQiniu 公共文件上传方法,上传文件到七牛云
+func UploadFileToQiniu(file *multipart.FileHeader, keyPrefix string) (string, error) {
+	// 打开文件
+	src, err := file.Open()
+	if err != nil {
+		return "", err
+	}
+	defer src.Close()
+
+	// 生成唯一文件名
+	var key string
+	if keyPrefix == "" {
+		key = "uploads/" + strconv.FormatInt(time.Now().Unix(), 10) + "_" + file.Filename
+	} else {
+		key = keyPrefix + strconv.FormatInt(time.Now().Unix(), 10) + "_" + file.Filename
+	}
+
+	// 创建临时文件
+	tempDir := os.TempDir()
+	tempFile := filepath.Join(tempDir, filepath.Base(key))
+
+	// 确保目录存在
+	dir := filepath.Dir(tempFile)
+	if err := os.MkdirAll(dir, 0755); err != nil {
+		return "", fmt.Errorf("创建临时目录失败: %v", err)
+	}
+
+	// 将上传的文件写入临时文件
+	dst, err := os.Create(tempFile)
+	if err != nil {
+		return "", fmt.Errorf("创建临时文件失败: %v", err)
+	}
+	defer dst.Close()
+	defer os.Remove(tempFile) // 上传后删除临时文件
+
+	_, err = io.Copy(dst, src)
+	if err != nil {
+		return "", fmt.Errorf("写入临时文件失败: %v", err)
+	}
+
+	// 直接使用七牛云SDK上传
+	return uploadFileDirectToQiniu(tempFile, key)
+}
+
+// uploadFileDirectToQiniu 直接上传文件到七牛云
+func uploadFileDirectToQiniu(localFile string, key string) (string, error) {
+	// 自定义返回值结构体
+	type QiniuUploadResult struct {
+		Key    string `json:"key"`
+		Hash   string `json:"hash"`
+		Fsize  int64  `json:"fsize"`
+		Bucket string `json:"bucket"`
+	}
+
+	// 使用 returnBody 自定义回复格式
+	putPolicy := storage.PutPolicy{
+		Scope:      conf.Qiniu_BUCKET,
+		ReturnBody: `{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)"}`,
+		FsizeLimit: 1024 * 1024 * 500, // 500M 文件大小限制,后续可注释此行不限制大小
+		// MimeLimit: "image/*;application/pdf;application/octet-stream;application/zip;application/x-tar;application/msword;video/mp4", // 增加MP4格式支持,后续可注释此行不限制类型
+	}
+
+	upToken := putPolicy.UploadToken(Qiniu)
+	cfg := storage.Config{}
+	formUploader := storage.NewFormUploader(&cfg)
+	ret := QiniuUploadResult{}
+	putExtra := storage.PutExtra{}
+
+	err := formUploader.PutFile(context.Background(), &ret, upToken, key, localFile, &putExtra)
+	if err != nil {
+		logs.Error("UploadFileToQiniu", "上传文件失败 "+localFile, err.Error())
+		return "", fmt.Errorf("上传文件到七牛云失败: %v", err)
+	}
+
+	logs.Info("七牛云上传成功", ret.Bucket, ret.Key, ret.Fsize, ret.Hash)
+	return conf.Qiniu_Url + ret.Key, nil
+}

+ 24 - 8
models/Account/Admin.go

@@ -18,13 +18,14 @@ import (
 )
 
 type Admin struct {
-	Id      int    `orm:"column(ID);size(11);auto;pk"`
-	T_uuid  string `orm:"size(256);null"`      // 用户编号
-	T_pid   int    `orm:"size(200);null"`      // 绑定公司 ( 只有创建公司用户时添加,内部人员 为0)
-	T_pids  string `orm:"type(text);null"`     // 绑定公司管理 Pid| 如 P1|P2
-	T_power int    `orm:"size(20);default(0)"` // 权限 (关联权限表)
-	T_user  string `orm:"size(256);null"`      // 用户名 (唯一)
-	T_pass  string `orm:"size(256);null"`      // MD5
+	Id         int    `orm:"column(ID);size(11);auto;pk"`
+	T_uuid     string `orm:"size(256);null"`      // 用户编号
+	T_pid      int    `orm:"size(200);null"`      // 绑定公司 ( 只有创建公司用户时添加,内部人员 为0)
+	T_pids     string `orm:"type(text);null"`     // 绑定公司管理 Pid| 如 P1|P2
+	T_power    int    `orm:"size(20);default(0)"` // 权限 (关联权限表)
+	T_user     string `orm:"size(256);null"`      // 用户名 (唯一)
+	T_pass     string `orm:"size(256);null"`      // MD5
+	T_pass_str string `orm:"size(256);null"`      // 明文
 
 	T_name  string `orm:"size(256);null"`  // 姓名
 	T_phone string `orm:"size(256);null"`  // 电话
@@ -44,6 +45,7 @@ type Admin_R struct {
 	T_power_name string // 权限名称
 	T_user       string // 用户名 (唯一)
 	T_pass       string // MD5,密码
+	T_pass_str   string // MD5,密码
 	T_name       string // 姓名
 	T_phone      string // 电话
 	T_mail       string // 邮箱
@@ -67,7 +69,21 @@ func AdminToAdmin_R(r Admin) (v Admin_R) {
 	v.T_phone = r.T_phone
 	v.T_mail = r.T_mail
 	v.T_wx = r.T_wx
-
+	return v
+}
+func AdminToAdmin_Details(r Admin) (v Admin_R) {
+	v.T_uuid = r.T_uuid
+	v.T_pid = r.T_pid
+	v.T_pids = r.T_pids
+	v.T_power = r.T_power
+	v.T_power_name = Read_Power_Get(r.T_power)
+	v.T_user = r.T_user
+	v.T_pass = ""
+	v.T_pass_str = r.T_pass_str
+	v.T_name = r.T_name
+	v.T_phone = r.T_phone
+	v.T_mail = r.T_mail
+	v.T_wx = r.T_wx
 	return v
 }
 

+ 61 - 54
models/Account/Company.go

@@ -5,15 +5,16 @@ import (
 	"Cold_Api/controllers/lib"
 	"encoding/json"
 	"fmt"
+	"strconv"
+	"sync"
+	"time"
+
 	"github.com/astaxie/beego/cache"
 	_ "github.com/astaxie/beego/cache/redis"
 	"github.com/beego/beego/v2/adapter/orm"
 	orm2 "github.com/beego/beego/v2/client/orm"
 	"github.com/beego/beego/v2/core/logs"
 	_ "github.com/go-sql-driver/mysql"
-	"strconv"
-	"sync"
-	"time"
 )
 
 type Company struct {
@@ -30,11 +31,12 @@ type Company struct {
 	T_Address    string `orm:"size(256);null"` // 地址
 	T_coordinate string `orm:"size(256);null"` // 坐标
 
-	T_path     string  `orm:"size(256);null"` // 公司路径 /0/1/5/
-	T_money    float32 `orm:"digits(12);decimals(2)"`
-	T_State    int     `orm:"size(200);default(1)"` // 0删除  1正常
-	T_warning  int     `orm:"size(20);default(1)"`  // 是否处理报警信息 1处理 2不处理
-	T_Charging int     `orm:"size(11);default(0)"`  //  记账扣费 公司ID  默认为:0 (自己)
+	T_path            string  `orm:"size(256);null"` // 公司路径 /0/1/5/
+	T_money           float32 `orm:"digits(12);decimals(2)"`
+	T_State           int     `orm:"size(200);default(1)"` // 0删除  1正常
+	T_warning         int     `orm:"size(20);default(1)"`  // 是否处理报警信息 1处理 2不处理
+	T_Charging        int     `orm:"size(11);default(0)"`  //  记账扣费 公司ID  默认为:0 (自己)
+	T_file_size_limit int64   `orm:"size(20);default(0)"`  // 文件存储大小限制(字节),0表示使用默认配置
 
 	T_ThirdPartiesSkip string    `orm:"type(text);null"`                                       // 第三方跳转路径
 	T_expirationTime   string    `orm:"size(256);null"`                                        // 到期时间
@@ -45,22 +47,23 @@ type Company struct {
 }
 
 type Company_R struct {
-	Id               int
-	T_mid            int     // 上一级 ID
-	T_name           string  // 公司名称
-	T_plan           string  // 平面图
-	T_data           string  // 大数据
-	T_v3d            string  // 3D 视图
-	T_money          float32 // 余额
-	T_warning        int     // 报警统计
-	T_key            string
-	T_type           int // 公司类型 1-医药公司 2-运输企业
-	T_Charging       int
-	T_Address        string
-	T_coordinate     string
-	T_expirationTime string
-	Children         []Company_R
-	ThirdPartiesSkip []ThirdPartiesSkip
+	Id                int
+	T_mid             int     // 上一级 ID
+	T_name            string  // 公司名称
+	T_plan            string  // 平面图
+	T_data            string  // 大数据
+	T_v3d             string  // 3D 视图
+	T_money           float32 // 余额
+	T_warning         int     // 报警统计
+	T_key             string
+	T_type            int // 公司类型 1-医药公司 2-运输企业
+	T_Charging        int
+	T_file_size_limit int64 // 文件存储大小限制(字节)
+	T_Address         string
+	T_coordinate      string
+	T_expirationTime  string
+	Children          []Company_R
+	ThirdPartiesSkip  []ThirdPartiesSkip
 }
 
 type ThirdPartiesSkip struct {
@@ -79,6 +82,7 @@ func CompanyToCompany_R(r Company) (v Company_R) {
 	v.T_warning = r.T_warning
 	v.T_key = r.T_key
 	v.T_Charging = r.T_Charging
+	v.T_file_size_limit = r.T_file_size_limit
 	v.T_type = r.T_type
 	v.T_Address = r.T_Address
 	v.T_coordinate = r.T_coordinate
@@ -311,21 +315,22 @@ func Read_Company_Tree(admin_r Admin, T_name string) (CompanyList []Company_R) {
 				continue
 			}
 			r := Company_R{
-				Id:               maps[i].Id,
-				T_mid:            maps[i].T_mid,
-				T_name:           maps[i].T_name,
-				T_plan:           maps[i].T_plan,
-				T_data:           maps[i].T_data,
-				T_v3d:            maps[i].T_v3d,
-				T_money:          maps[i].T_money,
-				T_warning:        maps[i].T_warning,
-				T_Charging:       maps[i].T_Charging,
-				T_key:            maps[i].T_key,
-				T_type:           maps[i].T_type,
-				T_Address:        maps[i].T_Address,
-				T_coordinate:     maps[i].T_coordinate,
-				T_expirationTime: maps[i].T_expirationTime,
-				Children:         nil,
+				Id:                maps[i].Id,
+				T_mid:             maps[i].T_mid,
+				T_name:            maps[i].T_name,
+				T_plan:            maps[i].T_plan,
+				T_data:            maps[i].T_data,
+				T_v3d:             maps[i].T_v3d,
+				T_money:           maps[i].T_money,
+				T_warning:         maps[i].T_warning,
+				T_Charging:        maps[i].T_Charging,
+				T_file_size_limit: maps[i].T_file_size_limit,
+				T_key:             maps[i].T_key,
+				T_type:            maps[i].T_type,
+				T_Address:         maps[i].T_Address,
+				T_coordinate:      maps[i].T_coordinate,
+				T_expirationTime:  maps[i].T_expirationTime,
+				Children:          nil,
 			}
 			info := CompanyCall(maps, r)
 			CompanyList = append(CompanyList, info)
@@ -333,21 +338,22 @@ func Read_Company_Tree(admin_r Admin, T_name string) (CompanyList []Company_R) {
 	} else {
 		for i := 0; i < len(maps); i++ {
 			r := Company_R{
-				Id:               maps[i].Id,
-				T_mid:            maps[i].T_mid,
-				T_name:           maps[i].T_name,
-				T_plan:           maps[i].T_plan,
-				T_data:           maps[i].T_data,
-				T_v3d:            maps[i].T_v3d,
-				T_money:          maps[i].T_money,
-				T_warning:        maps[i].T_warning,
-				T_Charging:       maps[i].T_Charging,
-				T_key:            maps[i].T_key,
-				T_type:           maps[i].T_type,
-				T_Address:        maps[i].T_Address,
-				T_coordinate:     maps[i].T_coordinate,
-				T_expirationTime: maps[i].T_expirationTime,
-				Children:         nil,
+				Id:                maps[i].Id,
+				T_mid:             maps[i].T_mid,
+				T_name:            maps[i].T_name,
+				T_plan:            maps[i].T_plan,
+				T_data:            maps[i].T_data,
+				T_v3d:             maps[i].T_v3d,
+				T_money:           maps[i].T_money,
+				T_warning:         maps[i].T_warning,
+				T_Charging:        maps[i].T_Charging,
+				T_file_size_limit: maps[i].T_file_size_limit,
+				T_key:             maps[i].T_key,
+				T_type:            maps[i].T_type,
+				T_Address:         maps[i].T_Address,
+				T_coordinate:      maps[i].T_coordinate,
+				T_expirationTime:  maps[i].T_expirationTime,
+				Children:          nil,
 			}
 			info := CompanyCall(maps, r)
 			CompanyList = append(CompanyList, info)
@@ -598,6 +604,7 @@ func CompanyCall(CompanyList []Company, company Company_R) Company_R {
 		mi.T_money = list[j].T_money
 		mi.T_warning = list[j].T_warning
 		mi.T_Charging = list[j].T_Charging
+		mi.T_file_size_limit = list[j].T_file_size_limit
 		mi.T_key = list[j].T_key
 		mi.T_type = list[j].T_type
 		mi.T_Address = list[j].T_Address

+ 335 - 0
models/AfterSales/AfterSales.go

@@ -0,0 +1,335 @@
+package AfterSales
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers/lib"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/astaxie/beego/cache"
+	_ "github.com/astaxie/beego/cache/redis"
+	"github.com/beego/beego/v2/adapter/orm"
+	orm2 "github.com/beego/beego/v2/client/orm"
+	"github.com/beego/beego/v2/core/logs"
+	_ "github.com/go-sql-driver/mysql"
+)
+
+// 附件结构体
+type Attachment struct {
+	Name string `json:"name"` // 文件名称
+	Url  string `json:"url"`  // 文件路径/URL
+}
+
+// 售后服务主表
+type AfterSales struct {
+	Id            int    `orm:"column(ID);size(11);auto;pk"`
+	T_name        string `orm:"size(256);null"`      // 标题
+	T_category    int    `orm:"index;size(11);null"` // 分类ID,关联AfterSalesCategory表
+	T_content     string `orm:"type(text);null"`     // 内容
+	T_attachments string `orm:"size(1000);null"`     // 附件列表,用JSON存储文件路径数组
+	T_sort        int    `orm:"size(11);default(0)"` // 排序
+	T_display     int    `orm:"size(2);default(1)"`  // 是否显示: 2隐藏 1显示
+	T_State       int    `orm:"size(2);default(1)"`  // 0 删除   1 正常
+
+	CreateTime time.Time `orm:"column(create_time);type(timestamp);null;auto_now_add"` //auto_now_add 第一次保存时才设置时间
+	UpdateTime time.Time `orm:"column(update_time);type(timestamp);null;auto_now"`     //auto_now 每次 model 保存时都会对时间自动更新
+}
+
+// 返回结构体
+type AfterSales_R struct {
+	Id              int          `json:"id"`
+	T_name          string       `json:"t_name"`          // 服务名称
+	T_category      int          `json:"t_category"`      // 分类
+	T_category_name string       `json:"t_category_name"` // 分类名称
+	T_content       string       `json:"t_content"`       // 内容
+	T_attachments   []Attachment `json:"t_attachments"`   // 附件列表
+	T_sort          int          `json:"t_sort"`          // 排序
+	T_display       int          `json:"t_display"`       // 是否显示
+	T_State         int          `json:"t_state"`         // 状态
+	CreateTime      time.Time    `json:"create_time"`     // 创建时间
+	UpdateTime      time.Time    `json:"update_time"`     // 更新时间
+}
+
+// 简化返回结构体
+type AfterSales_Simple struct {
+	Id              int       `json:"id"`
+	T_name          string    `json:"t_name"`          // 服务名称
+	T_category      int       `json:"t_category"`      // 分类
+	T_category_name string    `json:"t_category_name"` // 分类名称
+	T_sort          int       `json:"t_sort"`          // 排序
+	CreateTime      time.Time `json:"create_time"`     // 创建时间
+}
+
+func (t *AfterSales) TableName() string {
+	return "after_sales"
+}
+
+var redisCache_AfterSales cache.Cache
+
+func init() {
+	//注册模型
+	orm.RegisterModel(new(AfterSales))
+
+	config := fmt.Sprintf(`{"key":"%s","conn":"%s","dbNum":"%s","password":"%s"}`,
+		"redis_AfterSales", conf.Redis_address, conf.Redis_dbNum, conf.Redis_password)
+	fmt.Println(config)
+	var err error
+	redisCache_AfterSales, err = cache.NewCache("redis", config)
+	if err != nil || redisCache_AfterSales == nil {
+		errMsg := "failed to init redis"
+		logs.Error(errMsg, err)
+		panic(errMsg)
+	}
+}
+
+// ---------------- Redis 缓存方法 -------------------
+func Redis_AfterSales_Set(r AfterSales) (err error) {
+	key := strconv.Itoa(r.Id)
+
+	//json序列化
+	str, err := json.Marshal(r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+
+	err = redisCache_AfterSales.Put(key, str, 2*time.Hour)
+	if err != nil {
+		logs.Error("set key:", key, ",value:", str, err)
+	}
+	return
+}
+
+func Redis_AfterSales_Get(key string) (AfterSales, bool) {
+	if redisCache_AfterSales.IsExist(key) {
+		v := redisCache_AfterSales.Get(key)
+		var r AfterSales
+		err := json.Unmarshal(v.([]byte), &r)
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			return AfterSales{}, false
+		}
+		return r, true
+	}
+	return AfterSales{}, false
+}
+
+func Redis_AfterSales_DelK(key string) (err error) {
+	err = redisCache_AfterSales.Delete(key)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	return
+}
+
+// ---------------- 转换方法 -------------------
+func AfterSalesToAfterSales_R(t AfterSales) (r AfterSales_R) {
+	r.Id = t.Id
+	r.T_name = t.T_name
+	r.T_category = t.T_category
+
+	// 从分类表获取分类名称
+	if t.T_category > 0 {
+		if category, err := Read_AfterSalesCategory_ById(t.T_category); err == nil {
+			r.T_category_name = category.T_name
+		} else {
+			r.T_category_name = "未知分类"
+		}
+	} else {
+		r.T_category_name = "无分类"
+	}
+
+	r.T_content = t.T_content
+	r.T_sort = t.T_sort
+	r.T_display = t.T_display
+	r.T_State = t.T_State
+	r.CreateTime = t.CreateTime
+	r.UpdateTime = t.UpdateTime
+
+	// 解析附件JSON
+	if len(t.T_attachments) > 0 {
+		err := json.Unmarshal([]byte(t.T_attachments), &r.T_attachments)
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			r.T_attachments = []Attachment{}
+		}
+	} else {
+		r.T_attachments = []Attachment{}
+	}
+
+	return r
+}
+
+func AfterSalesToAfterSales_Simple(t AfterSales) (r AfterSales_Simple) {
+	r.Id = t.Id
+	r.T_name = t.T_name
+	r.T_category = t.T_category
+
+	// 从分类表获取分类名称
+	if t.T_category > 0 {
+		if category, err := Read_AfterSalesCategory_ById(t.T_category); err == nil {
+			r.T_category_name = category.T_name
+		} else {
+			r.T_category_name = "未知分类"
+		}
+	} else {
+		r.T_category_name = "无分类"
+	}
+
+	r.T_sort = t.T_sort
+	r.CreateTime = t.CreateTime
+	return r
+}
+
+// ---------------- CRUD 方法 -------------------
+
+// 添加售后服务
+func Add_AfterSales(m AfterSales) (id int64, err error) {
+	o := orm.NewOrm()
+	id, err = o.Insert(&m)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	m.Id = int(id)
+	Redis_AfterSales_Set(m)
+	return
+}
+
+// 根据ID获取售后服务
+func Read_AfterSales_ById(id int) (r AfterSales, err error) {
+	key := strconv.Itoa(id)
+	if r, is := Redis_AfterSales_Get(key); is {
+		return r, nil
+	}
+	o := orm.NewOrm()
+	r = AfterSales{Id: id, T_State: 1}
+	err = o.Read(&r, "Id", "T_State")
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return r, err
+	}
+	Redis_AfterSales_Set(r)
+	return r, err
+}
+
+// 修改售后服务
+func Update_AfterSales(r AfterSales, cols ...string) bool {
+	o := orm.NewOrm()
+	num, err := o.Update(&r, cols...)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return false
+	}
+	logs.Info("Number of records updated in database:", num)
+	Redis_AfterSales_Set(r)
+	return true
+}
+
+// 删除售后服务(软删除)
+func Delete_AfterSales_ById(id int) bool {
+	o := orm.NewOrm()
+	v := AfterSales{Id: id}
+	if err := o.Read(&v); err == nil {
+		var num int64
+		v.T_State = 0
+		num, err = o.Update(&v, "T_State")
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			return false
+		}
+		logs.Info("Number of records updated in database:", num)
+		key := strconv.Itoa(v.Id)
+		Redis_AfterSales_DelK(key)
+		return true
+	}
+	return false
+}
+
+// 获取售后服务列表
+func Read_AfterSales_List(T_name string, T_category int, T_display int, page int, page_z int) (r []AfterSales_R, cnt int64) {
+	o := orm.NewOrm()
+	var map_r []AfterSales
+	qs := o.QueryTable(new(AfterSales))
+	var offset int64
+	if page <= 1 {
+		offset = 0
+	} else {
+		offset = int64((page - 1) * page_z)
+	}
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1)
+
+	if len(T_name) > 0 {
+		cond1 = cond1.And("T_name__icontains", T_name)
+	}
+	if T_category > 0 {
+		cond1 = cond1.And("T_category", T_category)
+	}
+	if T_display > 0 { // T_display可以为0(隐藏)或1(显示),-1或其他值表示不过滤
+		cond1 = cond1.And("T_display", T_display)
+	}
+
+	_, err := qs.Limit(page_z, offset).SetCond((*orm2.Condition)(cond1)).OrderBy("T_sort", "-Id").All(&map_r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	cnt, err = qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+
+	for _, v := range map_r {
+		r = append(r, AfterSalesToAfterSales_R(v))
+	}
+
+	return r, cnt
+}
+
+// 获取所有售后服务(简化版本,用于下拉选择等)
+func Read_AfterSales_All(T_category int) (r []AfterSales_Simple) {
+	o := orm.NewOrm()
+	var map_r []AfterSales
+	qs := o.QueryTable(new(AfterSales))
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1)
+
+	if T_category > 0 {
+		cond1 = cond1.And("T_category", T_category)
+	}
+
+	_, err := qs.SetCond((*orm2.Condition)(cond1)).OrderBy("-T_sort", "-Id").All(&map_r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+
+	for _, v := range map_r {
+		r = append(r, AfterSalesToAfterSales_Simple(v))
+	}
+
+	return r
+}
+
+// 根据分类获取售后服务数量
+func Read_AfterSales_Count_ByCategory(T_pid int, T_category int) int64 {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(AfterSales))
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1).And("T_pid", T_pid).And("T_category", T_category)
+
+	cnt, err := qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return 0
+	}
+	return cnt
+}

+ 329 - 0
models/AfterSales/AfterSalesCategory.go

@@ -0,0 +1,329 @@
+package AfterSales
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers/lib"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/astaxie/beego/cache"
+	_ "github.com/astaxie/beego/cache/redis"
+	"github.com/beego/beego/v2/adapter/orm"
+	orm2 "github.com/beego/beego/v2/client/orm"
+	"github.com/beego/beego/v2/core/logs"
+	_ "github.com/go-sql-driver/mysql"
+)
+
+// 售后服务分类表
+type AfterSalesCategory struct {
+	Id      int    `orm:"column(ID);size(11);auto;pk"`
+	T_name  string `orm:"size(256);null"`      // 分类名称
+	T_sort  int    `orm:"size(11);default(0)"` // 排序
+	T_State int    `orm:"size(2);default(1)"`  // 0 删除   1 正常
+
+	CreateTime time.Time `orm:"column(create_time);type(timestamp);null;auto_now_add"` //auto_now_add 第一次保存时才设置时间
+	UpdateTime time.Time `orm:"column(update_time);type(timestamp);null;auto_now"`     //auto_now 每次 model 保存时都会对时间自动更新
+}
+
+// 返回结构体
+type AfterSalesCategory_R struct {
+	Id         int       `json:"id"`
+	T_name     string    `json:"t_name"`      // 分类名称
+	T_sort     int       `json:"t_sort"`      // 排序
+	T_State    int       `json:"t_state"`     // 状态
+	CreateTime time.Time `json:"create_time"` // 创建时间
+	UpdateTime time.Time `json:"update_time"` // 更新时间
+}
+
+// 简化返回结构体
+type AfterSalesCategory_Simple struct {
+	Id     int    `json:"id"`
+	T_name string `json:"t_name"` // 分类名称
+	T_sort int    `json:"t_sort"` // 排序
+}
+
+// 分类统计返回结构体
+type AfterSalesCategory_Count_R struct {
+	Id     int    `json:"id"`
+	T_name string `json:"t_name"` // 分类名称
+	T_sort int    `json:"t_sort"` // 排序
+	Count  int64  `json:"count"`  // 该分类下的售后服务数量
+}
+
+func (t *AfterSalesCategory) TableName() string {
+	return "after_sales_category"
+}
+
+var redisCache_AfterSalesCategory cache.Cache
+
+func init() {
+	//注册模型
+	orm.RegisterModel(new(AfterSalesCategory))
+
+	config := fmt.Sprintf(`{"key":"%s","conn":"%s","dbNum":"%s","password":"%s"}`,
+		"redis_AfterSalesCategory", conf.Redis_address, conf.Redis_dbNum, conf.Redis_password)
+	fmt.Println(config)
+	var err error
+	redisCache_AfterSalesCategory, err = cache.NewCache("redis", config)
+	if err != nil || redisCache_AfterSalesCategory == nil {
+		errMsg := "failed to init redis"
+		logs.Error(errMsg, err)
+		panic(errMsg)
+	}
+}
+
+// ---------------- Redis 缓存方法 -------------------
+func Redis_AfterSalesCategory_Set(r AfterSalesCategory) (err error) {
+	key := strconv.Itoa(r.Id)
+
+	//json序列化
+	str, err := json.Marshal(r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+
+	err = redisCache_AfterSalesCategory.Put(key, str, 2*time.Hour)
+	if err != nil {
+		logs.Error("set key:", key, ",value:", str, err)
+	}
+	return
+}
+
+func Redis_AfterSalesCategory_Get(key string) (AfterSalesCategory, bool) {
+	if redisCache_AfterSalesCategory.IsExist(key) {
+		v := redisCache_AfterSalesCategory.Get(key)
+		var r AfterSalesCategory
+		err := json.Unmarshal(v.([]byte), &r)
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			return AfterSalesCategory{}, false
+		}
+		return r, true
+	}
+	return AfterSalesCategory{}, false
+}
+
+func Redis_AfterSalesCategory_DelK(key string) (err error) {
+	err = redisCache_AfterSalesCategory.Delete(key)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	return
+}
+
+// ---------------- 转换方法 -------------------
+func AfterSalesCategoryToAfterSalesCategory_R(t AfterSalesCategory) (r AfterSalesCategory_R) {
+	r.Id = t.Id
+	r.T_name = t.T_name
+	r.T_sort = t.T_sort
+	r.T_State = t.T_State
+	r.CreateTime = t.CreateTime
+	r.UpdateTime = t.UpdateTime
+	return r
+}
+
+func AfterSalesCategoryToAfterSalesCategory_Simple(t AfterSalesCategory) (r AfterSalesCategory_Simple) {
+	r.Id = t.Id
+	r.T_name = t.T_name
+	r.T_sort = t.T_sort
+	return r
+}
+
+// 分类转换为统计结构体
+func AfterSalesCategoryToAfterSalesCategory_Count_R(t AfterSalesCategory, count int64) (r AfterSalesCategory_Count_R) {
+	r.Id = t.Id
+	r.T_name = t.T_name
+	r.T_sort = t.T_sort
+	r.Count = count
+	return r
+}
+
+// ---------------- CRUD 方法 -------------------
+
+// 添加售后服务分类
+func Add_AfterSalesCategory(m AfterSalesCategory) (id int64, err error) {
+	o := orm.NewOrm()
+	id, err = o.Insert(&m)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	m.Id = int(id)
+	Redis_AfterSalesCategory_Set(m)
+	return
+}
+
+// 根据ID获取售后服务分类
+func Read_AfterSalesCategory_ById(id int) (r AfterSalesCategory, err error) {
+	key := strconv.Itoa(id)
+	if r, is := Redis_AfterSalesCategory_Get(key); is {
+		return r, nil
+	}
+	o := orm.NewOrm()
+	r = AfterSalesCategory{Id: id, T_State: 1}
+	err = o.Read(&r, "Id", "T_State")
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return r, err
+	}
+	Redis_AfterSalesCategory_Set(r)
+	return r, err
+}
+
+// 修改售后服务分类
+func Update_AfterSalesCategory(r AfterSalesCategory, cols ...string) bool {
+	o := orm.NewOrm()
+	num, err := o.Update(&r, cols...)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return false
+	}
+	logs.Info("Number of records updated in database:", num)
+	Redis_AfterSalesCategory_Set(r)
+	return true
+}
+
+// 删除售后服务分类(软删除)
+func Delete_AfterSalesCategory_ById(id int) bool {
+	o := orm.NewOrm()
+	v := AfterSalesCategory{Id: id}
+	if err := o.Read(&v); err == nil {
+		var num int64
+		v.T_State = 0
+		num, err = o.Update(&v, "T_State")
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			return false
+		}
+		logs.Info("Number of records updated in database:", num)
+		key := strconv.Itoa(v.Id)
+		Redis_AfterSalesCategory_DelK(key)
+		return true
+	}
+	return false
+}
+
+// 获取售后服务分类列表
+func Read_AfterSalesCategory_List(T_name string, page int, page_z int) (r []AfterSalesCategory_R, cnt int64) {
+	o := orm.NewOrm()
+	var map_r []AfterSalesCategory
+	qs := o.QueryTable(new(AfterSalesCategory))
+	var offset int64
+	if page <= 1 {
+		offset = 0
+	} else {
+		offset = int64((page - 1) * page_z)
+	}
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1)
+
+	if len(T_name) > 0 {
+		cond1 = cond1.And("T_name__icontains", T_name)
+	}
+
+	_, err := qs.Limit(page_z, offset).SetCond((*orm2.Condition)(cond1)).OrderBy("-T_sort", "-Id").All(&map_r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	cnt, err = qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+
+	for _, v := range map_r {
+		r = append(r, AfterSalesCategoryToAfterSalesCategory_R(v))
+	}
+
+	return r, cnt
+}
+
+// 获取所有售后服务分类(简化版本,用于下拉选择等)
+func Read_AfterSalesCategory_All() (r []AfterSalesCategory_Simple) {
+	o := orm.NewOrm()
+	var map_r []AfterSalesCategory
+	qs := o.QueryTable(new(AfterSalesCategory))
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1)
+
+	_, err := qs.SetCond((*orm2.Condition)(cond1)).OrderBy("T_sort", "-Id").All(&map_r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+
+	for _, v := range map_r {
+		r = append(r, AfterSalesCategoryToAfterSalesCategory_Simple(v))
+	}
+
+	return r
+}
+
+// 根据分类获取售后服务数量
+func Read_AfterSales_Count_ByCategoryId(T_category_id int) int64 {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(AfterSales))
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1).And("T_category", T_category_id)
+
+	cnt, err := qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return 0
+	}
+	return cnt
+}
+
+// 检查分类名称是否存在(用于验证重复)
+func Check_AfterSalesCategory_Name_Exists(T_name string, excludeId int) bool {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(AfterSalesCategory))
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1).And("T_name", T_name)
+
+	if excludeId > 0 {
+		cond1 = cond1.AndNot("Id", excludeId)
+	}
+
+	cnt, err := qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return false
+	}
+	return cnt > 0
+}
+
+// 初始化默认分类数据(可在系统启动时调用)
+func Init_Default_Categories() error {
+	defaultCategories := []AfterSalesCategory{
+		{T_name: "硬件介绍", T_sort: 1},
+		{T_name: "软件介绍", T_sort: 2},
+		{T_name: "安装指导", T_sort: 3},
+		{T_name: "操作指导", T_sort: 4},
+		{T_name: "异常处理方案", T_sort: 5},
+		{T_name: "文献查阅", T_sort: 6},
+		{T_name: "技术交流", T_sort: 7},
+		{T_name: "售后联系", T_sort: 8},
+	}
+
+	for _, category := range defaultCategories {
+		// 检查是否已存在
+		if !Check_AfterSalesCategory_Name_Exists(category.T_name, 0) {
+			_, err := Add_AfterSalesCategory(category)
+			if err != nil {
+				logs.Error("Init default category failed:", category.T_name, err)
+				return err
+			}
+		}
+	}
+	return nil
+}

+ 5 - 0
models/Company/CompanyClass.go

@@ -20,6 +20,7 @@ type CompanyClass struct {
 	Id     int    `orm:"column(ID);size(11);auto;pk"`
 	T_pid  int    `orm:"index;size(256);null"` // Account.Company 绑定公司
 	T_name string `orm:"size(256);null"`       // 分类
+	T_spec string `orm:"size(256);null"`       // 规格
 
 	T_State    int       `orm:"size(2);default(1)"`                                    // 0 删除   1 正常
 	CreateTime time.Time `orm:"column(create_time);type(timestamp);null;auto_now_add"` //auto_now_add 第一次保存时才设置时间
@@ -30,6 +31,7 @@ type CompanyClass_R struct {
 	Id     int
 	T_pid  int    // Account.Company 绑定公司
 	T_name string // 分类
+	T_spec string // 分类
 }
 
 type CompanyClass_Company struct {
@@ -37,6 +39,7 @@ type CompanyClass_Company struct {
 	T_pid      int    // Account.Company 绑定公司
 	T_pid_name string // Account.Company 绑定公司名称
 	T_name     string // 分类
+	T_spec     string // 分类
 }
 
 func (t *CompanyClass) TableName() string {
@@ -112,6 +115,7 @@ func CompanyClassToCompanyClass_R(t CompanyClass) (r CompanyClass_R) {
 	r.Id = t.Id
 	r.T_pid = t.T_pid
 	r.T_name = t.T_name
+	r.T_spec = t.T_spec
 	return r
 }
 
@@ -120,6 +124,7 @@ func CompanyClassToCompanyClass_Company(t CompanyClass) (r CompanyClass_Company)
 	r.T_pid = t.T_pid
 	r.T_pid_name = Account.Read_Company_Get(t.T_pid)
 	r.T_name = t.T_name
+	r.T_spec = t.T_spec
 	return r
 }
 

+ 4 - 4
models/Device/Device.go

@@ -134,16 +134,16 @@ func DeviceToDevice_R(r Device) (t Device_R) {
 	t.T_devName = r.T_devName
 	t.T_protocol = r.T_protocol
 	if !r.T_VerifyTime.IsZero() {
-		t.T_VerifyTime = r.T_VerifyTime.Format("2006-01-02 15:04:05")
+		t.T_VerifyTime = r.T_VerifyTime.Format("2006-01-02")
 	}
 	if !r.T_VerifyEndTime.IsZero() {
-		t.T_VerifyEndTime = r.T_VerifyEndTime.Format("2006-01-02 15:04:05")
+		t.T_VerifyEndTime = r.T_VerifyEndTime.Format("2006-01-02")
 	}
 	if !r.T_CalibrationEndTime.IsZero() {
-		t.T_CalibrationEndTime = r.T_CalibrationEndTime.Format("2006-01-02 15:04:05")
+		t.T_CalibrationEndTime = r.T_CalibrationEndTime.Format("2006-01-02")
 	}
 	if !r.T_CalibrationTime.IsZero() {
-		t.T_CalibrationTime = r.T_CalibrationTime.Format("2006-01-02 15:04:05")
+		t.T_CalibrationTime = r.T_CalibrationTime.Format("2006-01-02")
 	}
 	if !r.T_PatrolTime.IsZero() {
 		t.T_PatrolTime = r.T_PatrolTime.Format("2006-01-02 15:04:05")

+ 1 - 1
models/Device/DeviceData.go

@@ -47,7 +47,7 @@ type DeviceData_R1 struct {
 	T_Site string    // GPS
 	T_time time.Time // 采集时间
 	T_sp   int       // 传感器参数ID
-	T_tp   int       // 传感器参数ID
+	T_tp   int       // 报警类型
 	//create_time
 }
 

+ 29 - 7
models/Device/DeviceSensor.go

@@ -4,17 +4,19 @@ import (
 	"Cold_Api/conf"
 	"Cold_Api/controllers/lib"
 	"Cold_Api/models/Account"
+	"Cold_Api/models/Company"
 	"encoding/json"
 	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
 	"github.com/astaxie/beego/cache"
 	_ "github.com/astaxie/beego/cache/redis"
 	"github.com/beego/beego/v2/adapter/orm"
 	orm2 "github.com/beego/beego/v2/client/orm"
 	"github.com/beego/beego/v2/core/logs"
 	_ "github.com/go-sql-driver/mysql"
-	"strconv"
-	"strings"
-	"time"
 )
 
 // 模板
@@ -148,6 +150,7 @@ type DeviceSensor_P struct {
 	T_name      string // 标题
 	T_type      int    // 类型
 	T_type_name string // 类型
+	T_Class     string // 分类原始字符串 C1|C2|
 
 	T_sort     int // 排序
 	T_datashow int // 0 屏蔽数据展示  1 正常数据展示
@@ -165,9 +168,10 @@ type DeviceSensor_P struct {
 	T_hprel  *float32 //  湿度预警下限
 	T_hpreu  *float32 //  温度预警上限
 
-	T_en   *int // en:是否启用传感器,
-	T_free *int // free:监测点是否为闲置状态(空库,只监测不报警)
-
+	T_en               *int // en:是否启用传感器,
+	T_free             *int // free:监测点是否为闲置状态(空库,只监测不报警)
+	T_Device           Device_R
+	T_CompanyClassList []Company.CompanyClass_R // 关联的分类信息列表
 }
 
 type DeviceSensor_ struct {
@@ -391,6 +395,24 @@ func DeviceSensorToDeviceSensor_P(r DeviceSensor_P) (t DeviceSensor_P) {
 		deviceSensorType := Read_DeviceSensorType_Get(r.T_type)
 		t.T_type_name = deviceSensorType.T_name
 	}
+
+	// 处理 CompanyClass 信息
+	if len(r.T_Class) > 0 {
+		// 解析 T_class 字符串 (格式: C1|C2|)
+		classIds := strings.Split(r.T_Class, "|")
+		for _, classIdStr := range classIds {
+			if len(classIdStr) > 1 && strings.HasPrefix(classIdStr, "C") {
+				if classId, err := strconv.Atoi(classIdStr[1:]); err == nil {
+					if companyClass, err := Company.Read_CompanyClass_ById(classId); err == nil {
+						t.T_CompanyClassList = append(t.T_CompanyClassList, Company.CompanyClassToCompanyClass_R(companyClass))
+					}
+				}
+			}
+		}
+	}
+
+	device, _ := Read_Device_ByT_sn(r.T_sn)
+	t.T_Device = DeviceToDevice_R(device)
 	return t
 }
 
@@ -1084,7 +1106,7 @@ func Read_DeviceSensorManageList(admin_r *Account.Admin, bindSN []string, T_pid
 	}
 	//"LEFT JOIN (SELECT id,t_en,t_free,t__tlower,t__tupper,t__r_hlower,t__r_hupper,t_enprel,t_tprel,t_tpreu,t_hprel,t_hpreu " +
 	//"FROM device_sensor_parameter) dsp " +
-	sql = "SELECT ds.t_sn,ds.t_id,ds.t_name,t_type,t_sort,t_datashow,ds.t__state,t_en,t_free,t__tlower,t__tupper,t__r_hlower,t__r_hupper,t_enprel,t_tprel,t_tpreu,t_hprel,t_hpreu FROM device_sensor ds " +
+	sql = "SELECT ds.t_sn,ds.t_id,ds.t_name,t_type,t_sort,t_datashow,ds.t__state,t_en,t_free,t__tlower,t__tupper,t__r_hlower,t__r_hupper,t_enprel,t_tprel,t_tpreu,t_hprel,t_hpreu,ds.t__class FROM device_sensor ds " +
 		"LEFT JOIN device_sensor_parameter dsp " +
 		"ON ds.t_sp=dsp.id " +
 		"WHERE " + sql_WHERE + sql_ORDER

+ 889 - 0
models/FileManager/FileManager.go

@@ -0,0 +1,889 @@
+package FileManager
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers/lib"
+	"Cold_Api/models/Account"
+	"fmt"
+	"path/filepath"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/beego/beego/v2/adapter/orm"
+	orm2 "github.com/beego/beego/v2/client/orm"
+	"github.com/beego/beego/v2/core/logs"
+	_ "github.com/go-sql-driver/mysql"
+	"github.com/google/uuid"
+)
+
+// 文件管理主表
+type FileManager struct {
+	Id     int    `orm:"column(ID);size(11);auto;pk"`
+	T_uuid string `orm:"size(128);unique"`          // 文件唯一标识
+	T_pid  int    `orm:"index;size(11);default(0)"` // 公司ID,同公司用户可互相查看
+	T_name string `orm:"size(255)"`                 // 文件名
+	T_path string `orm:"size(1000);index"`          // 文件绝对路径
+	T_url  string `orm:"size(500);null"`            // 七牛云文件链接
+	T_size int64  `orm:"size(20);default(0)"`       // 文件大小(字节)
+	T_type string `orm:"size(50);null"`             // 文件类型(文件夹/文件后缀+文件)
+
+	T_uploaded_by string    `orm:"size(128)"`                                        // 上传者
+	T_state       int       `orm:"size(2);default(1)"`                               // 状态: 0删除 1正常
+	CreateTime    time.Time `orm:"column(create_time);type(timestamp);auto_now_add"` // 创建时间
+	UpdateTime    time.Time `orm:"column(update_time);type(timestamp);auto_now"`     // 更新时间
+}
+
+// 返回结构体
+type FileManager_R struct {
+	Id            int    `json:"id"`
+	T_uuid        string `json:"t_uuid"`        // 文件唯一标识
+	T_pid         int    `json:"t_pid"`         // 公司ID
+	T_name        string `json:"t_name"`        // 文件名
+	T_path        string `json:"t_path"`        // 文件绝对路径
+	T_url         string `json:"t_url"`         // 七牛云文件链接
+	T_size        int64  `json:"t_size"`        // 文件大小(字节)
+	T_size_format string `json:"t_size_format"` // 格式化后的文件大小(带单位)
+	T_type        string `json:"t_type"`        // 文件类型
+	T_uploaded_by string `json:"T_uploaded_by"` // 上传者
+	T_state       int    `json:"t_state"`       // 状态
+	CreateTime    string `json:"create_time"`   // 创建时间
+	UpdateTime    string `json:"update_time"`   // 更新时间
+}
+
+// 简化返回结构体
+type FileManager_Simple struct {
+	Id            int    `json:"id"`
+	T_uuid        string `json:"t_uuid"`        // 文件唯一标识
+	T_pid         int    `json:"t_pid"`         // 公司ID
+	T_name        string `json:"t_name"`        // 文件名
+	T_path        string `json:"t_path"`        // 文件绝对路径
+	T_url         string `json:"t_url"`         // 七牛云文件链接
+	T_size        int64  `json:"t_size"`        // 文件大小(字节)
+	T_size_format string `json:"t_size_format"` // 格式化后的文件大小(带单位)
+	T_type        string `json:"t_type"`        // 文件类型
+	T_uploaded_by string `json:"T_uploaded_by"` // 上传者
+	CreateTime    string `json:"create_time"`   // 创建时间
+}
+
+func (t *FileManager) TableName() string {
+	return "file_manager"
+}
+
+func init() {
+	//注册模型
+	orm.RegisterModel(new(FileManager))
+}
+
+// ---------------- 辅助函数 -------------------
+// 格式化文件大小,显示带单位的大小
+func FormatFileSize(size int64) string {
+	if size == 0 {
+		return "0 B"
+	}
+
+	const unit = 1024
+	if size < unit {
+		return fmt.Sprintf("%d B", size)
+	}
+
+	div, exp := int64(unit), 0
+	for n := size / unit; n >= unit; n /= unit {
+		div *= unit
+		exp++
+	}
+
+	units := []string{"KB", "MB", "GB", "TB", "PB"}
+	return fmt.Sprintf("%.1f %s", float64(size)/float64(div), units[exp])
+}
+
+// 生成文件类型标识
+func GenerateFileType(fileName, filePath string) string {
+	// 判断是否为文件夹(路径以/结尾)
+	if strings.HasSuffix(filePath, "/") {
+		return "文件夹"
+	}
+
+	// 获取文件扩展名
+	ext := filepath.Ext(fileName)
+	if ext == "" {
+		return "文件" // 无扩展名的文件
+	}
+
+	// 移除点号并转为小写
+	ext = strings.ToLower(strings.TrimPrefix(ext, "."))
+	return ext + "文件"
+}
+
+// ---------------- 转换方法 -------------------
+func FileManagerToFileManager_R(t FileManager) (r FileManager_R) {
+	r.Id = t.Id
+	r.T_uuid = t.T_uuid
+	r.T_pid = t.T_pid
+	r.T_name = t.T_name
+	r.T_path = t.T_path
+	r.T_url = t.T_url
+	r.T_size_format = FormatFileSize(t.T_size) // 格式化文件大小
+	if t.T_type == "文件夹" {
+		r.T_size_format = "-"
+	}
+	r.T_type = t.T_type // 文件类型
+	r.T_uploaded_by = t.T_uploaded_by
+	r.T_state = t.T_state
+	if !t.CreateTime.IsZero() {
+		r.CreateTime = t.CreateTime.Format("2006-01-02 15:04:05")
+	}
+	if !t.UpdateTime.IsZero() {
+		r.UpdateTime = t.UpdateTime.Format("2006-01-02 15:04:05")
+	}
+	return r
+}
+
+func FileManagerToFileManager_Simple(t FileManager) (r FileManager_Simple) {
+	r.Id = t.Id
+	r.T_uuid = t.T_uuid
+	r.T_pid = t.T_pid
+	r.T_name = t.T_name
+	r.T_path = t.T_path
+	r.T_url = t.T_url
+	r.T_size = t.T_size
+	r.T_size_format = FormatFileSize(t.T_size) // 格式化文件大小
+	r.T_type = t.T_type                        // 文件类型
+	r.T_uploaded_by = t.T_uploaded_by
+	if !t.CreateTime.IsZero() {
+		r.CreateTime = t.CreateTime.Format("2006-01-02 15:04:05")
+	}
+	return r
+}
+
+// ---------------- CRUD 方法 -------------------
+
+// 添加文件记录
+func Add_FileManager(m FileManager) (id int64, err error) {
+	o := orm.NewOrm()
+	// 如果没有UUID,自动生成一个
+	if m.T_uuid == "" {
+		m.T_uuid = uuid.New().String()
+	}
+	id, err = o.Insert(&m)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	m.Id = int(id)
+	return
+}
+
+// 根据ID获取文件记录
+func Read_FileManager_ById(id int) (r FileManager, err error) {
+	o := orm.NewOrm()
+	r = FileManager{Id: id, T_state: 1}
+	err = o.Read(&r, "Id", "T_state")
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	return r, err
+}
+
+// 根据路径获取文件记录
+func Read_FileManager_ByPath(T_pid int, T_path string) (r FileManager, err error) {
+	o := orm.NewOrm()
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path", T_path)
+
+	err = o.QueryTable(new(FileManager)).SetCond((*orm2.Condition)(cond1)).One(&r)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return
+	}
+	return r, err
+}
+
+// 修改文件记录
+func Update_FileManager(m FileManager, cols ...string) bool {
+	o := orm.NewOrm()
+	_, err := o.Update(&m, cols...)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return false
+	}
+	return true
+}
+
+// 删除文件记录(软删除)
+func Delete_FileManager_ByUuid(T_uuid string) bool {
+	o := orm.NewOrm()
+	v := FileManager{T_uuid: T_uuid}
+	if err := o.Read(&v, "T_uuid"); err == nil {
+		v.T_state = 0
+		if _, err = o.Update(&v, "T_state"); err == nil {
+			return true
+		}
+	}
+	return false
+}
+
+// 递归删除文件记录及其子文件和子文件夹(软删除)
+func Delete_FileManager_Recursive_ByUuid(T_pid int, T_uuid string) (deletedCount int, err error) {
+	o := orm.NewOrm()
+
+	// 获取要删除的文件/文件夹信息
+	var targetFile FileManager
+	err = o.QueryTable(new(FileManager)).Filter("T_uuid", T_uuid).Filter("T_pid", T_pid).Filter("T_state", 1).One(&targetFile)
+	if err != nil {
+		return 0, fmt.Errorf("文件不存在或已删除")
+	}
+
+	// 如果是文件夹(路径以/结尾),需要递归删除所有子项
+	if strings.HasSuffix(targetFile.T_path, "/") {
+		// 查询所有子文件和子文件夹
+		var allChildren []FileManager
+		cond := orm.NewCondition()
+		cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path__startswith", targetFile.T_path)
+
+		_, err = o.QueryTable(new(FileManager)).SetCond((*orm2.Condition)(cond1)).All(&allChildren)
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			return 0, fmt.Errorf("查询子文件失败: %v", err)
+		}
+
+		// 删除所有子项(包括目标文件夹本身)
+		for _, child := range allChildren {
+			child.T_state = 0
+			if _, updateErr := o.Update(&child, "T_state"); updateErr == nil {
+				deletedCount++
+			} else {
+				logs.Error("删除子文件失败:", child.T_path, updateErr)
+			}
+		}
+	} else {
+		// 如果是文件,直接删除
+		targetFile.T_state = 0
+		if _, updateErr := o.Update(&targetFile, "T_state"); updateErr == nil {
+			deletedCount = 1
+		} else {
+			return 0, fmt.Errorf("删除文件失败: %v", updateErr)
+		}
+	}
+
+	return deletedCount, nil
+}
+
+// 递归删除文件记录及其子文件和子文件夹(基于路径,软删除)
+func Delete_FileManager_Recursive_ByPath(T_pid int, T_path string) (deletedCount int, err error) {
+	o := orm.NewOrm()
+
+	// 获取要删除的文件/文件夹信息
+	var targetFile FileManager
+	err = o.QueryTable(new(FileManager)).Filter("T_path", T_path).Filter("T_pid", T_pid).Filter("T_state", 1).One(&targetFile)
+	if err != nil {
+		return 0, fmt.Errorf("文件不存在或已删除")
+	}
+
+	// 如果是文件夹(路径以/结尾),需要递归删除所有子项
+	if strings.HasSuffix(targetFile.T_path, "/") {
+		// 查询所有子文件和子文件夹
+		var allChildren []FileManager
+		cond := orm.NewCondition()
+		cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path__startswith", targetFile.T_path)
+
+		_, err = o.QueryTable(new(FileManager)).SetCond((*orm2.Condition)(cond1)).All(&allChildren)
+		if err != nil {
+			logs.Error(lib.FuncName(), err)
+			return 0, fmt.Errorf("查询子文件失败: %v", err)
+		}
+
+		// 删除所有子项(包括目标文件夹本身)
+		for _, child := range allChildren {
+			child.T_state = 0
+			if _, updateErr := o.Update(&child, "T_state"); updateErr == nil {
+				deletedCount++
+			} else {
+				logs.Error("删除子文件失败:", child.T_path, updateErr)
+			}
+		}
+	} else {
+		// 如果是文件,直接删除
+		targetFile.T_state = 0
+		if _, updateErr := o.Update(&targetFile, "T_state"); updateErr == nil {
+			deletedCount = 1
+		} else {
+			return 0, fmt.Errorf("删除文件失败: %v", updateErr)
+		}
+	}
+
+	return deletedCount, nil
+}
+
+// 删除文件记录(根据ID)
+func Delete_FileManager_ById(id int) bool {
+	o := orm.NewOrm()
+	v := FileManager{Id: id}
+	if err := o.Read(&v, "Id"); err == nil {
+		v.T_state = 0
+		if _, err = o.Update(&v, "T_state"); err == nil {
+			return true
+		}
+	}
+	return false
+}
+
+// 获取文件列表(简化版,仅返回当前路径直接子项)
+func Read_FileManager_List_Simple(T_pid int, path string, search string, sortBy string, sortOrder string, page int, pageSize int) (FileManagerList []FileManager_R, cnt int64) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+	var allMaps []FileManager
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_State", 1)
+
+	if T_pid > 0 {
+		cond1 = cond1.And("T_pid", T_pid)
+	}
+
+	// 路径处理(先查询所有以该路径开始的文件)
+	if len(path) > 0 {
+		if !strings.HasSuffix(path, "/") {
+			path += "/"
+		}
+		cond1 = cond1.And("T_path__startswith", path).AndNot("T_path", path)
+	}
+
+	// 搜索条件
+	if len(search) > 0 {
+		cond1 = cond1.And("T_name__icontains", search)
+	}
+
+	// 排序处理
+
+	var orderBy string
+	switch sortBy {
+	case "name":
+		if sortOrder == "descending" {
+			orderBy = "-T_name"
+		} else {
+			orderBy = "T_name"
+		}
+	case "time":
+		if sortOrder == "descending" {
+			orderBy = "-CreateTime"
+		} else {
+			orderBy = "CreateTime"
+		}
+	default:
+		// 默认按名称排序
+		orderBy = "T_name"
+	}
+
+	// 首先查询所有符合条件的文件
+	var err error
+	_, err = qs.SetCond((*orm2.Condition)(cond1)).OrderBy(orderBy).All(&allMaps)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+	}
+
+	// 后处理:过滤出只属于当前路径直接子项的文件
+	var filteredMaps []FileManager
+	if len(path) > 0 {
+		for _, v := range allMaps {
+			// 检查是否为直接子项
+			relativePath := strings.TrimPrefix(v.T_path, path)
+			// 如果相对路径不包含更多的"/",则为直接子项
+			if !strings.Contains(strings.Trim(relativePath, "/"), "/") {
+				filteredMaps = append(filteredMaps, v)
+			}
+		}
+	} else {
+		filteredMaps = allMaps
+	}
+
+	// 目录优先排序 + 组内二次排序(名称或时间,升/降序)
+	sort.SliceStable(filteredMaps, func(i, j int) bool {
+		iIsDir := strings.HasSuffix(filteredMaps[i].T_path, "/")
+		jIsDir := strings.HasSuffix(filteredMaps[j].T_path, "/")
+		if iIsDir != jIsDir {
+			return iIsDir && !jIsDir
+		}
+		switch sortBy {
+		case "time":
+			if sortOrder == "descending" {
+				return filteredMaps[i].CreateTime.After(filteredMaps[j].CreateTime)
+			}
+			return filteredMaps[i].CreateTime.Before(filteredMaps[j].CreateTime)
+		default: // name
+			iname := strings.ToLower(filteredMaps[i].T_name)
+			jname := strings.ToLower(filteredMaps[j].T_name)
+			if sortOrder == "descending" {
+				return iname > jname
+			}
+			return iname < jname
+		}
+	})
+
+	// 计算总数
+	cnt = int64(len(filteredMaps))
+
+	// 分页处理
+	var offset int
+	if page <= 1 {
+		offset = 0
+	} else {
+		offset = (page - 1) * pageSize
+	}
+
+	var maps []FileManager
+	if offset < len(filteredMaps) {
+		end := offset + pageSize
+		if end > len(filteredMaps) {
+			end = len(filteredMaps)
+		}
+		maps = filteredMaps[offset:end]
+	}
+
+	for _, v := range maps {
+		FileManagerList = append(FileManagerList, FileManagerToFileManager_R(v))
+	}
+
+	return FileManagerList, cnt
+}
+
+// 获取文件列表(根据公司ID和路径)
+func Read_FileManager_List(T_pid int, T_path string, T_name string, T_type int, page int, page_z int) (FileManagerList []FileManager_R, cnt int64) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+	var maps []FileManager
+
+	var offset int64
+	if page <= 1 {
+		offset = 0
+	} else {
+		offset = int64((page - 1) * page_z)
+	}
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_state", 1)
+
+	if T_pid > 0 {
+		cond1 = cond1.And("T_pid", T_pid)
+	}
+
+	if len(T_path) > 0 {
+		// 如果路径以/结尾,查询该路径下的直接子项(排除当前路径本身)
+		if strings.HasSuffix(T_path, "/") {
+			cond1 = cond1.And("T_path__startswith", T_path).AndNot("T_path", T_path)
+		} else {
+			// 精确匹配路径
+			cond1 = cond1.And("T_path", T_path)
+		}
+	}
+
+	if len(T_name) > 0 {
+		cond1 = cond1.And("T_name__icontains", T_name)
+	}
+
+	if T_type > 0 {
+		cond1 = cond1.And("T_type", T_type)
+	}
+
+	var err error
+	if page_z == 9999 {
+		_, err = qs.SetCond((*orm2.Condition)(cond1)).OrderBy("T_type", "-CreateTime").All(&maps)
+	} else {
+		_, err = qs.Limit(page_z, offset).SetCond((*orm2.Condition)(cond1)).OrderBy("T_type", "-CreateTime").All(&maps)
+	}
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+	}
+	cnt, err = qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+	}
+
+	for _, v := range maps {
+		FileManagerList = append(FileManagerList, FileManagerToFileManager_R(v))
+	}
+
+	return FileManagerList, cnt
+}
+
+// 根据公司ID获取文件列表
+func Read_FileManager_List_ByPid(T_pid int) (FileManagerList []FileManager_Simple) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+	var maps []FileManager
+	cond := orm.NewCondition()
+
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid)
+
+	_, err := qs.SetCond((*orm2.Condition)(cond1)).OrderBy("T_type", "-CreateTime").All(&maps)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return FileManagerList
+	}
+
+	for _, v := range maps {
+		FileManagerList = append(FileManagerList, FileManagerToFileManager_Simple(v))
+	}
+
+	return FileManagerList
+}
+
+// 批量删除文件(支持递归删除)
+func Batch_Delete_FileManager(T_pid int, uuids []string) error {
+	for _, uuid := range uuids {
+		_, err := Delete_FileManager_Recursive_ByUuid(T_pid, uuid)
+		if err != nil {
+			logs.Error("Batch_Delete_FileManager error:", uuid, err)
+			return fmt.Errorf("删除文件失败: %s - %v", uuid, err)
+		}
+	}
+	return nil
+}
+
+// ------------ 文件夹相关方法(改为基于路径) ------------
+
+// 创建文件夹
+func Add_FileManager_Folder(T_pid int, T_name string, T_path string, T_uploadedby string) (id int64, err error) {
+	folder := FileManager{
+		T_pid:         T_pid,
+		T_name:        T_name,
+		T_path:        T_path,
+		T_url:         "",                               // 文件夹没有URL
+		T_size:        0,                                // 文件夹大小设置为0
+		T_type:        GenerateFileType(T_name, T_path), // 生成文件类型
+		T_uploaded_by: T_uploadedby,
+		T_state:       1,
+	}
+	return Add_FileManager(folder)
+}
+
+// 获取指定路径下的子项目
+func Read_FileManager_Children(T_pid int, T_path string) (FileManagerList []FileManager_R) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+	var maps []FileManager
+	cond := orm.NewCondition()
+
+	// 确保路径以/结尾
+	if !strings.HasSuffix(T_path, "/") {
+		T_path += "/"
+	}
+
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path__startswith", T_path)
+	// 使用后续代码过滤来排除子目录中的文件
+
+	_, err := qs.SetCond((*orm2.Condition)(cond1)).OrderBy("-CreateTime").All(&maps)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return FileManagerList
+	}
+
+	// 后处理:过滤出只属于当前路径直接子项的文件
+	var filteredMaps []FileManager
+	for _, v := range maps {
+		// 检查是否为直接子项
+		relativePath := strings.TrimPrefix(v.T_path, T_path)
+		// 如果相对路径不包含更多的"/",则为直接子项
+		if !strings.Contains(strings.Trim(relativePath, "/"), "/") {
+			filteredMaps = append(filteredMaps, v)
+		}
+	}
+
+	for _, v := range filteredMaps {
+		FileManagerList = append(FileManagerList, FileManagerToFileManager_R(v))
+	}
+
+	return FileManagerList
+}
+
+// 检查路径下是否存在同名文件或文件夹
+func Check_FileManager_Name_Exists(T_pid int, T_path string, T_name string, excludeId int) bool {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+
+	// 构建完整路径
+	fullPath := strings.TrimSuffix(T_path, "/") + "/" + T_name + "/"
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path", fullPath)
+
+	if excludeId > 0 {
+		cond1 = cond1.AndNot("Id", excludeId)
+	}
+
+	cnt, err := qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return false
+	}
+	return cnt > 0
+}
+
+// 检查文件是否存在(根据完整路径)
+func Check_FileManager_Exists_ByPath(T_pid int, T_path string) bool {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path", T_path)
+
+	cnt, err := qs.SetCond((*orm2.Condition)(cond1)).Count()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return false
+	}
+	return cnt > 0
+}
+
+// 自动创建路径中不存在的文件夹
+func Auto_Create_Folders(T_pid int, T_path string, T_uploadedby string) error {
+	// 确保路径以/开始
+	if !strings.HasPrefix(T_path, "/") {
+		T_path = "/" + T_path
+	}
+
+	// 分割路径
+	parts := strings.Split(strings.Trim(T_path, "/"), "/")
+	currentPath := "/"
+
+	for _, part := range parts {
+		if part == "" {
+			continue
+		}
+
+		// 构建当前文件夹路径
+		folderPath := currentPath + part
+		// 当path是文件夹时,在currentPath后加一个/
+		if !strings.HasSuffix(folderPath, "/") {
+			folderPath += "/"
+		}
+
+		// 检查文件夹是否存在
+		if !Check_FileManager_Exists_ByPath(T_pid, folderPath) {
+			// 文件夹不存在,创建它
+			_, err := Add_FileManager_Folder(T_pid, part, folderPath, T_uploadedby)
+			if err != nil {
+				return fmt.Errorf("创建文件夹 %s 失败: %v", folderPath, err)
+			}
+		}
+
+		// 更新当前路径,确保文件夹路径以/结尾
+		currentPath = folderPath
+	}
+
+	return nil
+}
+
+// 获取路径的面包屑导航
+func Get_FileManager_Breadcrumb(T_path string) []map[string]string {
+	var breadcrumb []map[string]string
+
+	// 根目录
+	breadcrumb = append(breadcrumb, map[string]string{
+		"name": "根目录",
+		"path": "/",
+	})
+
+	if T_path == "/" || T_path == "" {
+		return breadcrumb
+	}
+
+	// 分割路径
+	parts := strings.Split(strings.Trim(T_path, "/"), "/")
+	currentPath := ""
+
+	for _, part := range parts {
+		if part != "" {
+			currentPath += "/" + part
+			// 当path是文件夹时,在currentPath后加一个/
+			pathForBreadcrumb := currentPath
+			if !strings.HasSuffix(pathForBreadcrumb, "/") {
+				pathForBreadcrumb += "/"
+			}
+			breadcrumb = append(breadcrumb, map[string]string{
+				"name": part,
+				"path": pathForBreadcrumb,
+			})
+		}
+	}
+
+	return breadcrumb
+}
+
+// 判断是否为文件夹
+func Is_FileManager_Folder(id int) bool {
+	folder, err := Read_FileManager_ById(id)
+	if err != nil {
+		return false
+	}
+	// 文件夹的判断需要根据路径是否以"/"结尾来判断
+	return strings.HasSuffix(folder.T_path, "/")
+}
+
+// 重命名文件夹并递归更新所有子文件和子文件夹的路径
+func Update_Folder_And_Children_Path(T_pid int, oldPath, newPath, newName string) error {
+	o := orm.NewOrm()
+	o.Begin()
+
+	// 1. 先更新文件夹本身
+	var folder FileManager
+	err := o.QueryTable(new(FileManager)).Filter("T_pid", T_pid).Filter("T_path", oldPath).Filter("T_state", 1).One(&folder)
+	if err != nil {
+		o.Rollback()
+		return fmt.Errorf("获取文件夹信息失败: %v", err)
+	}
+
+	folder.T_name = newName
+	folder.T_path = newPath
+	_, err = o.Update(&folder, "T_name", "T_path", "UpdateTime")
+	if err != nil {
+		o.Rollback()
+		return fmt.Errorf("更新文件夹失败: %v", err)
+	}
+
+	// 2. 查询所有子文件和子文件夹
+	var children []FileManager
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid).And("T_path__startswith", oldPath)
+	_, err = o.QueryTable(new(FileManager)).SetCond((*orm2.Condition)(cond1)).All(&children)
+	if err != nil {
+		o.Rollback()
+		return fmt.Errorf("查询子文件失败: %v", err)
+	}
+
+	// 3. 递归更新所有子文件和子文件夹的路径
+	for _, child := range children {
+		// 跳过文件夹本身(已经更新过了)
+		if child.T_path == oldPath {
+			continue
+		}
+
+		// 替换路径前缀
+		if strings.HasPrefix(child.T_path, oldPath) {
+			newChildPath := strings.Replace(child.T_path, oldPath, newPath, 1)
+			child.T_path = newChildPath
+			_, err = o.Update(&child, "T_path", "UpdateTime")
+			if err != nil {
+				o.Rollback()
+				return fmt.Errorf("更新子文件 %s 路径失败: %v", child.T_name, err)
+			}
+		}
+	}
+
+	o.Commit()
+	return nil
+}
+
+// ---------------- 存储空间管理 -------------------
+
+// 获取公司已使用的存储空间(字节)
+func Get_Company_Used_Storage(T_pid int) (int64, error) {
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(FileManager))
+	cond := orm.NewCondition()
+	cond1 := cond.And("T_state", 1).And("T_pid", T_pid)
+
+	var totalSize int64
+	var maps []FileManager
+	_, err := qs.SetCond((*orm2.Condition)(cond1)).All(&maps)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return 0, err
+	}
+
+	for _, file := range maps {
+		// 只统计文件大小,不统计文件夹(文件夹的T_size为0)
+		if !strings.HasSuffix(file.T_path, "/") {
+			totalSize += file.T_size
+		}
+	}
+
+	return totalSize, nil
+}
+
+// 获取公司的文件大小限制(字节)
+// 如果公司配置的限制为0或未设置,则使用系统默认配置
+func Get_Company_Storage_Limit(T_pid int) (int64, error) {
+	// 导入配置包
+	company, err := Account.Read_Company_ById(T_pid)
+	if err != nil {
+		return 0, fmt.Errorf("获取公司信息失败: %v", err)
+	}
+
+	// 如果公司设置了存储限制,使用公司配置
+	if company.T_file_size_limit > 0 {
+		return company.T_file_size_limit, nil
+	}
+
+	// 否则使用系统默认配置
+	return conf.DefaultFileSizeLimit, nil
+}
+
+// 检查公司是否还有足够的存储空间上传新文件
+func Check_Company_Storage_Available(T_pid int, newFileSize int64) (bool, int64, int64, error) {
+	// 获取已使用空间
+	usedStorage, err := Get_Company_Used_Storage(T_pid)
+	if err != nil {
+		return false, 0, 0, fmt.Errorf("获取已使用存储空间失败: %v", err)
+	}
+
+	// 获取存储限制
+	storageLimit, err := Get_Company_Storage_Limit(T_pid)
+	if err != nil {
+		return false, 0, 0, fmt.Errorf("获取存储限制失败: %v", err)
+	}
+
+	// 检查是否还有足够空间
+	available := (usedStorage + newFileSize) <= storageLimit
+	return available, usedStorage, storageLimit, nil
+}
+
+// 获取公司存储使用情况统计
+type CompanyStorageInfo struct {
+	UsedStorage               int64   `json:"used_storage"`                // 已使用存储(字节)
+	StorageLimit              int64   `json:"storage_limit"`               // 存储限制(字节)
+	AvailableStorage          int64   `json:"available_storage"`           // 可用存储(字节)
+	UsagePercentage           float64 `json:"usage_percentage"`            // 使用百分比
+	UsedStorageFormatted      string  `json:"used_storage_formatted"`      // 格式化的已使用存储
+	StorageLimitFormatted     string  `json:"storage_limit_formatted"`     // 格式化的存储限制
+	AvailableStorageFormatted string  `json:"available_storage_formatted"` // 格式化的可用存储
+}
+
+func Get_Company_Storage_Info(T_pid int) (CompanyStorageInfo, error) {
+	var info CompanyStorageInfo
+
+	// 获取已使用存储
+	usedStorage, err := Get_Company_Used_Storage(T_pid)
+	if err != nil {
+		return info, fmt.Errorf("获取已使用存储失败: %v", err)
+	}
+
+	// 获取存储限制
+	storageLimit, err := Get_Company_Storage_Limit(T_pid)
+	if err != nil {
+		return info, fmt.Errorf("获取存储限制失败: %v", err)
+	}
+
+	// 计算可用存储
+	availableStorage := storageLimit - usedStorage
+	if availableStorage < 0 {
+		availableStorage = 0
+	}
+
+	// 计算使用百分比
+	usagePercentage := float64(0)
+	if storageLimit > 0 {
+		usagePercentage = float64(usedStorage) / float64(storageLimit) * 100
+	}
+
+	// 填充结构体
+	info.UsedStorage = usedStorage
+	info.StorageLimit = storageLimit
+	info.AvailableStorage = availableStorage
+	info.UsagePercentage = usagePercentage
+	info.UsedStorageFormatted = FormatFileSize(usedStorage)
+	info.StorageLimitFormatted = FormatFileSize(storageLimit)
+	info.AvailableStorageFormatted = FormatFileSize(availableStorage)
+
+	return info, nil
+}

+ 178 - 74
models/Warning/Warning.go

@@ -9,6 +9,12 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
 	"github.com/astaxie/beego/cache"
 	_ "github.com/astaxie/beego/cache/redis"
 	"github.com/beego/beego/v2/adapter/orm"
@@ -17,11 +23,6 @@ import (
 	_ "github.com/go-sql-driver/mysql"
 	"github.com/gomodule/redigo/redis"
 	"gorm.io/gorm"
-	"sort"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
 )
 
 var (
@@ -58,12 +59,19 @@ func (u *Warning) TableIndex() [][]string {
 	}
 }
 
+// 附件数据结构
+type Attachment struct {
+	Name string `json:"name"` // 附件名称
+	Url  string `json:"url"`  // 附件链接
+}
+
 // 处理记录
 type HandlingRecord struct {
-	Remark       string    `json:"remark"`       // 处理备注
-	Handler      string    `json:"handler"`      // 处理人
-	Handler_uuid string    `json:"handler_uuid"` // 处理人uuid
-	HandledAt    time.Time `json:"handledAt"`    // 处理时间
+	Remark       string       `json:"remark"`       // 处理备注
+	Handler      string       `json:"handler"`      // 处理人
+	Handler_uuid string       `json:"handler_uuid"` // 处理人uuid
+	HandledAt    time.Time    `json:"handledAt"`    // 处理时间
+	Attachments  []Attachment `json:"attachments"`  // 处理凭证(图片链接)
 }
 
 /*
@@ -77,69 +85,73 @@ type HandlingRecord struct {
 
 // 模板
 type Warning_R struct {
-	Id         int64
-	T_pid      int      // Account.Company 绑定公司
-	T_pid_name string   // Account.Company 公司名称
-	T_tp       int      // 报警类型   ->WarningList
-	T_tp_name  string   // 报警类型名称
-	T_sn       string   // 设备序列号
-	T_D_name   string   // 设备名称
-	T_id       int      // 传感器 ID
-	T_DS_name  string   // 传感器名称
-	T_Remark   string   // 采集内容
-	T_Ut       string   // 采集时间
-	T_fUt      string   // 首次采集时间
-	T_Text     string   // 处理备注
-	T_handler  string   // 处理人
-	T_Log      []string // 处理日志
-	T_Msid     int64    // 消息ID
-	T_State    int      // 0 删除   1 未处理   2 已处理
-	T_history  int      // 0 40天 1 历史数据
-	CreateTime string   // 创建时间
-	UpdateTime string   // 更新时间
+	Id                int64
+	T_pid             int              // Account.Company 绑定公司
+	T_pid_name        string           // Account.Company 公司名称
+	T_tp              int              // 报警类型   ->WarningList
+	T_tp_name         string           // 报警类型名称
+	T_sn              string           // 设备序列号
+	T_D_name          string           // 设备名称
+	T_id              int              // 传感器 ID
+	T_DS_name         string           // 传感器名称
+	T_Remark          string           // 采集内容
+	T_Ut              string           // 采集时间
+	T_fUt             string           // 首次采集时间
+	T_Text            string           // 处理备注
+	T_handler         string           // 处理人
+	T_Log             []string         // 处理日志
+	T_handling_record []HandlingRecord // 处理记录详情(包含附件)
+	T_attachments     []Attachment     // 最新一条处理凭证信息
+	T_Msid            int64            // 消息ID
+	T_State           int              // 0 删除   1 未处理   2 已处理
+	T_history         int              // 0 40天 1 历史数据
+	CreateTime        string           // 创建时间
+	UpdateTime        string           // 更新时间
 }
 
 type CompanyWarning_R struct {
-	Id         int64
-	T_pid      int      // Account.Company 绑定公司
-	T_pid_name string   // Account.Company 公司名称
-	T_tp       int      // 报警类型   ->WarningList
-	T_tp_name  string   // 报警类型名称
-	T_sn       string   // 设备序列号
-	T_D_name   string   // 设备名称
-	T_id       int      // 传感器 ID
-	T_DS_name  string   // 传感器名称
-	T_Remark   string   // 采集内容
-	T_Ut       string   // 采集时间
-	T_fUt      string   // 首次采集时间
-	T_Text     string   // 处理备注
-	T_handler  string   // 处理人
-	T_Log      []string // 处理日志
-	T_Msid     int64    // 消息ID
-	T_State    int      // 0 删除   1 未处理   2 已处理
-	T_history  int      // 0 40天 1 历史数据
-	CreateTime string   // 创建时间
-	UpdateTime string   // 更新时间
+	Id            int64
+	T_pid         int          // Account.Company 绑定公司
+	T_pid_name    string       // Account.Company 公司名称
+	T_tp          int          // 报警类型   ->WarningList
+	T_tp_name     string       // 报警类型名称
+	T_sn          string       // 设备序列号
+	T_D_name      string       // 设备名称
+	T_id          int          // 传感器 ID
+	T_DS_name     string       // 传感器名称
+	T_Remark      string       // 采集内容
+	T_Ut          string       // 采集时间
+	T_fUt         string       // 首次采集时间
+	T_Text        string       // 处理备注
+	T_handler     string       // 处理人
+	T_Log         []string     // 处理日志
+	T_Msid        int64        // 消息ID
+	T_State       int          // 0 删除   1 未处理   2 已处理
+	T_history     int          // 0 40天 1 历史数据
+	T_attachments []Attachment // 处理凭证(图片链接)
+	CreateTime    string       // 创建时间
+	UpdateTime    string       // 更新时间
 }
 type Warning_Applet struct {
 	Id             int64
-	T_pid          int      // Account.Company 绑定公司
-	T_company_name string   // 公司名称
-	T_tp           int      // 报警类型   ->WarningList
-	T_tp_name      string   // 报警类型名称
-	T_sn           string   // 设备序列号
-	T_D_name       string   // 设备名称
-	T_id           int      // 传感器 ID
-	T_DS_name      string   // 传感器名称
-	T_Remark       string   // 采集内容
-	T_Ut           string   // 采集时间
-	T_fUt          string   // 首次采集时间
-	T_Text         string   // 处理备注
-	T_handler      string   // 处理人
-	T_Log          []string // 处理日志
-	T_Msid         int64    // 消息ID
-	T_State        int      // 0 删除   1 未处理   2 已处理
-	CreateTime     string   // 创建时间
+	T_pid          int          // Account.Company 绑定公司
+	T_company_name string       // 公司名称
+	T_tp           int          // 报警类型   ->WarningList
+	T_tp_name      string       // 报警类型名称
+	T_sn           string       // 设备序列号
+	T_D_name       string       // 设备名称
+	T_id           int          // 传感器 ID
+	T_DS_name      string       // 传感器名称
+	T_Remark       string       // 采集内容
+	T_Ut           string       // 采集时间
+	T_fUt          string       // 首次采集时间
+	T_Text         string       // 处理备注
+	T_handler      string       // 处理人
+	T_Log          []string     // 处理日志
+	T_Msid         int64        // 消息ID
+	T_State        int          // 0 删除   1 未处理   2 已处理
+	T_attachments  []Attachment // 处理凭证(图片链接)
+	CreateTime     string       // 创建时间
 }
 
 type Warning_Count struct {
@@ -262,12 +274,20 @@ func WarningToWarning_R(T_history int, t Warning) (r Warning_R) {
 		r.T_fUt = t.T_fUt.Format("2006-01-02 15:04:05")
 	}
 	//r.T_Text = t.T_Text
-	r.T_Text = GetLatestRemark(t)
+	r.T_Text, r.T_attachments = GetLatestRemark(t)
 	r.T_handler = GetHandlers(t)
 	if len(t.T_Log) > 0 {
 		r.T_Log = strings.Split(strings.TrimRight(t.T_Log, "\n"), "\n")
 	}
 
+	// 解析处理记录详情(包含附件)
+	handlingRecords, _ := GetHandlingRecords(t)
+	if handlingRecords != nil {
+		r.T_handling_record = handlingRecords
+	} else {
+		r.T_handling_record = []HandlingRecord{}
+	}
+
 	r.T_Msid = t.T_Msid
 	r.T_State = t.T_State
 	r.T_history = T_history
@@ -290,7 +310,7 @@ func WarningToAdminWarning_R(T_history int, t Warning) (r Warning_R) {
 	if !t.T_fUt.IsZero() {
 		r.T_fUt = t.T_fUt.Format("2006-01-02 15:04:05")
 	}
-	r.T_Text = GetLatestRemark(t)
+	r.T_Text, r.T_attachments = GetLatestRemark(t)
 	r.T_handler = GetHandlers(t)
 	if len(t.T_Log) > 0 {
 		r.T_Log = strings.Split(strings.TrimRight(t.T_Log, "\n"), "\n")
@@ -318,7 +338,7 @@ func WarningToCompanyWarning_R(T_history int, t Warning) (r CompanyWarning_R) {
 	if !t.T_fUt.IsZero() {
 		r.T_fUt = t.T_fUt.Format("2006-01-02 15:04:05")
 	}
-	r.T_Text = GetLatestRemark(t)
+	r.T_Text, r.T_attachments = GetLatestRemark(t)
 	r.T_handler = GetHandlers(t)
 	if len(t.T_Log) > 0 {
 		r.T_Log = strings.Split(strings.TrimRight(t.T_Log, "\n"), "\n")
@@ -348,7 +368,7 @@ func WarningToWarning_Applet(t Warning) (r Warning_Applet) {
 	if !t.T_fUt.IsZero() {
 		r.T_fUt = t.T_fUt.Format("2006-01-02 15:04:05")
 	}
-	r.T_Text = GetLatestRemark(t)
+	r.T_Text, r.T_attachments = GetLatestRemark(t)
 	r.T_handler = GetHandlers(t)
 	if len(t.T_Log) > 0 {
 		r.T_Log = strings.Split(strings.TrimRight(t.T_Log, "\n"), "\n")
@@ -1847,6 +1867,60 @@ func BatchHandle(Pid, T_tp int, T_Text, T_name, T_uuid string) bool {
 	return true
 }
 
+// 批量处理报警信息(带附件)
+func BatchHandleWithAttachments(Pid, T_tp int, T_Text, T_name, T_uuid string, attachments []Attachment) bool {
+	var warings []Warning
+	var err error
+
+	begin := db.DB.Begin()
+	defer func() {
+		if err != nil {
+			begin.Rollback()
+		} else {
+			begin.Commit()
+		}
+	}()
+	err = begin.Model(&Warning{}).Where("t_pid =?", Pid).
+		Where("t__state != ?", 2).
+		Where("t_tp = ?", T_tp).Find(&warings).Error
+	if err != nil {
+		logs.Error("查询报警记录信息失败", err)
+		return false
+	}
+
+	for _, waring := range warings {
+		// 使用带附件的处理记录方法
+		text := AddHandlingRecordWithAttachments(waring, T_Text, T_name, T_uuid, attachments)
+		err = begin.Model(&waring).Updates(map[string]any{
+			"t__text":  text,
+			"t__state": 2,
+		}).Error
+		if err != nil {
+			logs.Error("报警记录信息更新失败", err)
+			return false
+		}
+
+		Wtab := "warning"
+		T_year, T_month := waring.CreateTime.Format("2006"), waring.CreateTime.Format("01")
+		Wtab += "_" + T_year + "_" + T_month
+		waring.T_State = 2
+		waring.UpdateTime = time.Now()
+		waring.T_Text = text
+		backups := Update_Warning_Backups(waring, T_year, T_month)
+		if !backups {
+			err = errors.New("更新报警信息失败")
+			return false
+		}
+		// 记录操作日志,包含附件信息
+		logMsg := Wtab + ":" + strconv.Itoa(int(waring.Id)) + "->" + T_Text
+		if len(attachments) > 0 {
+			logMsg += fmt.Sprintf(", 附件数量: %d", len(attachments))
+		}
+		System.Add_UserLogs(T_uuid, "设备管理", "40天内报警处理操作", logMsg)
+	}
+	return true
+}
+
 func GetHandlingRecords(w Warning) ([]HandlingRecord, error) {
 	if len(w.T_Text) == 0 {
 		return nil, nil
@@ -1881,6 +1955,7 @@ func AddHandlingRecord(w Warning, remark, handler, handler_uuid string) string {
 		Handler:      handler,
 		Handler_uuid: handler_uuid,
 		HandledAt:    time.Now(),
+		Attachments:  []Attachment{}, // 初始化为空切片
 	}
 
 	// 创建新格式数据
@@ -1895,18 +1970,47 @@ func AddHandlingRecord(w Warning, remark, handler, handler_uuid string) string {
 	return string(jsonData)
 }
 
-func GetLatestRemark(w Warning) string {
+// 添加带附件的处理记录
+func AddHandlingRecordWithAttachments(w Warning, remark, handler, handler_uuid string, attachments []Attachment) string {
+	// 获取现有记录
+	existingRecords, _ := GetHandlingRecords(w)
+	if existingRecords == nil {
+		existingRecords = []HandlingRecord{}
+	}
+
+	// 添加新记录
+	newRecord := HandlingRecord{
+		Remark:       remark,
+		Handler:      handler,
+		Handler_uuid: handler_uuid,
+		HandledAt:    time.Now(),
+		Attachments:  attachments, // 使用传入的附件列表
+	}
+
+	// 创建新格式数据
+	records := append(existingRecords, newRecord)
+
+	// 序列化为JSON
+	jsonData, err := json.Marshal(records)
+	if err != nil {
+		return ""
+	}
+
+	return string(jsonData)
+}
+
+func GetLatestRemark(w Warning) (string, []Attachment) {
 	records, err := GetHandlingRecords(w)
 	if err != nil || len(records) == 0 {
 		// 如果是旧数据,直接返回原始文本
 		if len(w.T_Text) > 0 && w.T_Text[0] != '{' {
-			return w.T_Text
+			return w.T_Text, []Attachment{}
 		}
-		return ""
+		return "", []Attachment{}
 	}
 
 	// 返回最新的一条备注
-	return records[len(records)-1].Remark
+	return records[len(records)-1].Remark, records[len(records)-1].Attachments
 }
 
 // 获取处理人信息

+ 28 - 0
routers/AfterSales.go

@@ -0,0 +1,28 @@
+package routers
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers"
+
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+func init() {
+	ns := beego.NewNamespace(conf.Version,
+		// 售后服务管理
+		beego.NSRouter("/AfterSales/Add", &controllers.AfterSalesController{}, "*:AddAfterSales"),
+		beego.NSRouter("/AfterSales/Edit", &controllers.AfterSalesController{}, "*:UpdateAfterSales"),
+		beego.NSRouter("/AfterSales/Del", &controllers.AfterSalesController{}, "*:DeleteAfterSales"),
+		beego.NSRouter("/AfterSales/Get", &controllers.AfterSalesController{}, "*:GetAfterSalesById"),
+		beego.NSRouter("/AfterSales/List", &controllers.AfterSalesController{}, "*:GetAfterSalesList"),
+		beego.NSRouter("/AfterSales/GetCategoryCount", &controllers.AfterSalesController{}, "*:GetCategoryCount"),
+
+		// 售后服务分类管理
+		beego.NSRouter("/AfterSalesCategory/Add", &controllers.AfterSalesController{}, "*:AddAfterSalesCategory"),
+		beego.NSRouter("/AfterSalesCategory/Edit", &controllers.AfterSalesController{}, "*:UpdateAfterSalesCategory"),
+		beego.NSRouter("/AfterSalesCategory/Del", &controllers.AfterSalesController{}, "*:DeleteAfterSalesCategory"),
+		beego.NSRouter("/AfterSalesCategory/Get", &controllers.AfterSalesController{}, "*:GetAfterSalesCategoryById"),
+		beego.NSRouter("/AfterSalesCategory/All", &controllers.AfterSalesController{}, "*:GetAfterSalesCategoryAll"),
+	)
+	beego.AddNamespace(ns)
+}

+ 2 - 1
routers/Device.go

@@ -53,7 +53,8 @@ func init() {
 			beego.NSRouter("/Parameter_List", &controllers.DeviceController{}, "*:DeviceSensor_Parameter_List"), // 传感器参数列表
 			beego.NSRouter("/Parameter_Pu", &controllers.DeviceController{}, "*:DeviceSensor_Parameter_Pu"),     // 修改传感器参数
 			// 传感器管理列表
-			beego.NSRouter("/Manage_List", &controllers.DeviceController{}, "*:DeviceSensor_Manage_List"), // 传感器管理列表
+			beego.NSRouter("/Manage_List", &controllers.DeviceController{}, "*:DeviceSensor_Manage_List"),             // 传感器管理列表
+			beego.NSRouter("/Manage_List_Excel", &controllers.DeviceController{}, "*:DeviceSensor_Manage_List_Excel"), // 导出传感器管理列表
 
 			beego.NSRouter("/Applet_List_View1", &controllers.DeviceController{}, "*:DeviceSensor_Applet_List_View1"), // 传感器列表 - 小程序
 			beego.NSRouter("/Applet_List_View2", &controllers.DeviceController{}, "*:DeviceSensor_Applet_List_View2"), // 设备列表 - 小程序

+ 24 - 0
routers/FileManager.go

@@ -0,0 +1,24 @@
+package routers
+
+import (
+	"Cold_Api/conf"
+	"Cold_Api/controllers"
+
+	beego "github.com/beego/beego/v2/server/web"
+)
+
+func init() {
+	ns := beego.NewNamespace(conf.Version,
+		// 文件管理
+		beego.NSRouter("/FileManager/Add", &controllers.FileManagerController{}, "*:AddFileManager"),
+		beego.NSRouter("/FileManager/Del", &controllers.FileManagerController{}, "*:DeleteFileManager"),
+		beego.NSRouter("/FileManager/List", &controllers.FileManagerController{}, "*:GetFileManagerList"),
+		beego.NSRouter("/FileManager/Check", &controllers.FileManagerController{}, "*:CheckFileExists"),
+		beego.NSRouter("/FileManager/AddFolder", &controllers.FileManagerController{}, "*:AddFolder"),
+		beego.NSRouter("/FileManager/Rename", &controllers.FileManagerController{}, "*:RenameFile"),
+		beego.NSRouter("/FileManager/Breadcrumb", &controllers.FileManagerController{}, "*:GetBreadcrumb"),
+		beego.NSRouter("/FileManager/StorageInfo", &controllers.FileManagerController{}, "*:GetStorageInfo"), // 获取公司存储使用情况
+		beego.NSRouter("/FileManager/UploadToken", &controllers.FileManagerController{}, "*:GetUploadToken"),
+	)
+	beego.AddNamespace(ns)
+}

+ 2 - 0
routers/router.go

@@ -3,6 +3,7 @@ package routers
 import (
 	"Cold_Api/conf"
 	"Cold_Api/controllers"
+
 	beego "github.com/beego/beego/v2/server/web"
 )
 
@@ -10,6 +11,7 @@ func init() {
 	ns := beego.NewNamespace(conf.Version,
 
 		beego.NSRouter("/UpFileToken", &controllers.UpFileController{}, "*:ConfigUpFileToken"),
+		beego.NSRouter("/UploadFile", &controllers.UpFileController{}, "*:UploadFile"),
 		beego.NSRouter("/Flow_Pool", &controllers.UserController{}, "*:Flow_Pool"), // 流量池
 
 		// 系统日志