Parcourir la source

add:增加设备导出和设备盘点

zoie il y a 2 mois
Parent
commit
f4e7845149

+ 361 - 35
controllers/Stock.go

@@ -68,6 +68,149 @@ func (c *StockController) Device_List() {
 	return
 }
 
+func (c *StockController) Device_Excel() {
+
+	T_state, _ := c.GetInt("T_state")
+	T_name := c.GetString("T_name")
+	T_product_name := c.GetString("T_product_name")   // 产品名称
+	T_product_model := c.GetString("T_product_model") // 产品型号
+
+	DeviceDao := Stock.NewDevice(orm.NewOrm())
+	R_List, _ := DeviceDao.Read_Device_List(T_name, T_product_name, T_product_model, T_state, 0, 9999)
+
+	// 生成Excel文件名
+	filename := fmt.Sprintf("设备列表(%s)", lib.GetRandstring(6, "0123456789", 0))
+
+	// 创建Excel文件
+	f := excelize.NewFile()
+
+	// 设置样式
+	Style1, _ := f.NewStyle(
+		&excelize.Style{
+			Font:      &excelize.Font{Bold: true, Size: 14, Family: "宋体"},
+			Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
+		})
+
+	Style2, _ := f.NewStyle(
+		&excelize.Style{
+			Font:      &excelize.Font{Bold: true, Size: 10, 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},
+			},
+		})
+
+	Style3, _ := f.NewStyle(
+		&excelize.Style{
+			Font:      &excelize.Font{Size: 10, 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.SetSheetName("Sheet1", "设备列表")
+
+	// 设置标题
+	f.MergeCell("设备列表", "A1", "M1")
+	f.SetRowStyle("设备列表", 1, 1, Style1)
+	f.SetCellValue("设备列表", "A1", "宝智达科技设备列表")
+	f.SetRowHeight("设备列表", 1, 30)
+
+	// 设置表头
+	f.SetCellStyle("设备列表", "A2", "M2", Style2)
+	f.SetRowHeight("设备列表", 2, 20)
+
+	// 设置表头内容
+	f.SetCellValue("设备列表", "A2", "序号")
+	f.SetCellValue("设备列表", "B2", "合同编号")
+	f.SetCellValue("设备列表", "C2", "出库单号")
+	f.SetCellValue("设备列表", "D2", "关联项目")
+	f.SetCellValue("设备列表", "E2", "产品名称")
+	f.SetCellValue("设备列表", "F2", "产品分类")
+	f.SetCellValue("设备列表", "G2", "产品型号")
+	f.SetCellValue("设备列表", "H2", "产品规格")
+	f.SetCellValue("设备列表", "I2", "设备SN")
+	f.SetCellValue("设备列表", "J2", "模组imei")
+	f.SetCellValue("设备列表", "K2", "物联网卡号")
+	f.SetCellValue("设备列表", "L2", "设备编号")
+	f.SetCellValue("设备列表", "M2", "状态")
+
+	// 设置列宽
+	f.SetColWidth("设备列表", "A", "A", 8)  // 序号
+	f.SetColWidth("设备列表", "B", "B", 15) // 合同编号
+	f.SetColWidth("设备列表", "C", "C", 15) // 出库单号
+	f.SetColWidth("设备列表", "D", "D", 20) // 关联项目
+	f.SetColWidth("设备列表", "E", "E", 20) // 产品名称
+	f.SetColWidth("设备列表", "F", "F", 15) // 产品分类
+	f.SetColWidth("设备列表", "G", "G", 15) // 产品型号
+	f.SetColWidth("设备列表", "H", "H", 15) // 产品规格
+	f.SetColWidth("设备列表", "I", "I", 20) // 设备SN
+	f.SetColWidth("设备列表", "J", "J", 15) // 模组imei
+	f.SetColWidth("设备列表", "K", "K", 20) // 物联网卡号
+	f.SetColWidth("设备列表", "L", "L", 18) // 设备编号
+	f.SetColWidth("设备列表", "M", "M", 10) // 状态
+
+	// 填充数据
+	for i, device := range R_List {
+		row := i + 3 // 从第3行开始填充数据
+
+		// 状态转换
+		stateText := "未出库"
+		if device.T_State == 1 {
+			stateText = "已出库"
+		}
+
+		f.SetCellStyle("设备列表", fmt.Sprintf("A%d", row), fmt.Sprintf("M%d", row), Style3)
+		//f.SetRowHeight("设备列表", row, 20)
+
+		f.SetCellValue("设备列表", fmt.Sprintf("A%d", row), i+1)                         // 序号
+		f.SetCellValue("设备列表", fmt.Sprintf("B%d", row), device.T_contract_number)    // 合同编号
+		f.SetCellValue("设备列表", fmt.Sprintf("C%d", row), device.T_out_number)         // 出库单号
+		f.SetCellValue("设备列表", fmt.Sprintf("D%d", row), device.T_project)            // 关联项目
+		f.SetCellValue("设备列表", fmt.Sprintf("E%d", row), device.T_product_name)       // 产品名称
+		f.SetCellValue("设备列表", fmt.Sprintf("F%d", row), device.T_product_class_name) // 产品分类
+		f.SetCellValue("设备列表", fmt.Sprintf("G%d", row), device.T_product_model)      // 产品型号
+		f.SetCellValue("设备列表", fmt.Sprintf("H%d", row), device.T_product_spec)       // 产品规格
+		f.SetCellValue("设备列表", fmt.Sprintf("I%d", row), device.T_sn)                 // 设备SN
+		f.SetCellValue("设备列表", fmt.Sprintf("J%d", row), device.T_imei)               // 模组imei
+		f.SetCellValue("设备列表", fmt.Sprintf("K%d", row), device.T_iccid)              // 物联网卡号
+		f.SetCellValue("设备列表", fmt.Sprintf("L%d", row), device.T_device_number)      // 设备编号
+		f.SetCellValue("设备列表", fmt.Sprintf("M%d", row), stateText)                   // 状态
+	}
+
+	// 保存文件
+	if err := f.SaveAs("ofile/" + filename + ".xlsx"); err != nil {
+		fmt.Println(err)
+	}
+	var url string
+	//// 上传 OSS
+	nats := natslibs.NewNats(Nats.Nats, conf.NatsSubj_Prefix)
+	url, is := nats.Qiniu_UploadFile(lib.GetCurrentDirectory()+"/ofile/"+filename+".xlsx", "ofile/"+filename+".xlsx")
+	if !is {
+		c.Data["json"] = lib.JSONS{Code: 202, Msg: "oss!"}
+		c.ServeJSON()
+		return
+	}
+	//删除目录
+	err := os.Remove("ofile/" + filename + ".xlsx")
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "ok!", Data: url}
+	c.ServeJSON()
+	return
+
+}
+
 func (c *StockController) Device_Check() {
 
 	T_sn := c.GetString("T_sn")
@@ -120,6 +263,162 @@ func (c *StockController) Device_Check() {
 	return
 }
 
+func (c *StockController) Device_Take_Stock() {
+
+	T_product_name := c.GetString("T_product_name")   // 产品名称
+	T_product_model := c.GetString("T_product_model") // 产品型号
+	T_sn_list := c.GetString("T_sn_list")             // sn列表
+	DeviceDao := Stock.NewDevice(orm.NewOrm())
+
+	// 获取库存中的所有设备(如果有产品名称和型号过滤)
+	R_List, _ := DeviceDao.Read_Device_List("", T_product_name, T_product_model, 0, 0, 9999)
+
+	// 创建设备SN到设备信息的映射,用于快速查找
+	deviceMap := make(map[string]Stock.Device_R)
+	// 保存在库设备列表
+	var inStockDevices []Stock.Device_R
+
+	for _, device := range R_List {
+		deviceMap[device.T_sn] = device
+		// 收集所有在库设备(状态为2)
+		if device.T_State == 2 {
+			inStockDevices = append(inStockDevices, device)
+		}
+	}
+
+	// 创建结果数据结构
+	type DeviceCheckResult struct {
+		T_sn            string `json:"t_sn"`            // 设备SN
+		T_exists        bool   `json:"t_exists"`        // 是否存在于库存
+		T_state         int    `json:"t_state"`         // 设备状态:1-已出库 2-未出库/入库
+		T_state_desc    string `json:"t_state_desc"`    // 状态描述
+		T_product_id    int    `json:"t_product_id"`    // 产品ID
+		T_product_name  string `json:"t_product_name"`  // 产品名称
+		T_product_model string `json:"t_product_model"` // 产品型号
+		T_iccid         string `json:"t_iccid"`         // SIM卡号
+		T_imei          string `json:"t_imei"`          // 模组IMEI
+		T_in_number     string `json:"t_in_number"`     // 入库编号
+		T_out_number    string `json:"t_out_number"`    // 出库编号
+		T_from_input    bool   `json:"t_from_input"`    // 是否来自输入列表
+	}
+
+	// 处理每个输入的SN
+	var results []DeviceCheckResult
+	var processedSNs = make(map[string]bool) // 用于跟踪已处理的SN
+
+	// 解析输入的SN列表(如果有)
+	var snList []string
+	if len(T_sn_list) > 0 {
+		snList = lib.SplitString(T_sn_list, ",")
+
+		for _, sn := range snList {
+			sn = strings.TrimSpace(sn)
+			if len(sn) == 0 {
+				continue
+			}
+
+			result := DeviceCheckResult{
+				T_sn:         sn,
+				T_exists:     false,
+				T_from_input: true, // 标记为来自输入列表
+			}
+
+			// 检查设备是否存在于库存
+			if device, exists := deviceMap[sn]; exists {
+				result.T_exists = true
+				result.T_state = device.T_State
+				result.T_product_id = device.T_product_id
+				result.T_product_name = device.T_product_name
+				result.T_product_model = device.T_product_model
+				result.T_iccid = device.T_iccid
+				result.T_imei = device.T_imei
+				result.T_in_number = device.T_in_number
+				result.T_out_number = device.T_out_number
+
+				// 设置状态描述
+				if device.T_State == 1 {
+					result.T_state_desc = "已出库"
+				} else if device.T_State == 2 {
+					result.T_state_desc = "在库"
+				} else {
+					result.T_state_desc = "未知状态"
+				}
+			} else {
+				// 设备不存在于库存
+				result.T_state_desc = "未入库"
+			}
+
+			results = append(results, result)
+			processedSNs[sn] = true
+		}
+	}
+
+	// 添加所有在库设备(如果还没有处理过)
+	for _, device := range inStockDevices {
+		// 如果这个SN已经处理过(来自输入列表),则跳过
+		if _, processed := processedSNs[device.T_sn]; processed {
+			continue
+		}
+
+		result := DeviceCheckResult{
+			T_sn:            device.T_sn,
+			T_exists:        true,
+			T_state:         device.T_State,
+			T_state_desc:    "在库",
+			T_product_id:    device.T_product_id,
+			T_product_name:  device.T_product_name,
+			T_product_model: device.T_product_model,
+			T_iccid:         device.T_iccid,
+			T_imei:          device.T_imei,
+			T_in_number:     device.T_in_number,
+			T_out_number:    device.T_out_number,
+			T_from_input:    false, // 标记为不是来自输入列表
+		}
+
+		results = append(results, result)
+	}
+
+	// 统计结果
+	totalInputCount := 0
+	existsCount := 0
+	inStockCount := 0
+	outStockCount := 0
+	notExistsCount := 0
+
+	for _, result := range results {
+		if result.T_from_input {
+			totalInputCount++
+			if result.T_exists {
+				existsCount++
+				if result.T_state == 2 {
+					inStockCount++
+				} else if result.T_state == 1 {
+					outStockCount++
+				}
+			} else {
+				notExistsCount++
+			}
+		}
+	}
+
+	// 组装返回数据
+	r_jsons := map[string]interface{}{
+		"devices": results,
+		"stats": map[string]interface{}{
+			"total_input":    totalInputCount,
+			"exists":         existsCount,
+			"in_stock":       inStockCount,
+			"out_stock":      outStockCount,
+			"not_exists":     notExistsCount,
+			"total_in_stock": len(inStockDevices),
+		},
+	}
+
+	c.Data["json"] = lib.JSONS{Code: 200, Msg: "盘点完成", Data: r_jsons}
+	c.ServeJSON()
+	return
+}
+
 func (c *StockController) Stock_List() {
 
 	// 分页参数 初始化
@@ -777,6 +1076,7 @@ func (c *StockController) StockIn_Add() {
 					T_iccid:           mqtt.Iccid,
 					T_imei:            mqtt.Imei,
 					T_State:           2,
+					T_batch_number:    T_batch_number,
 					CreateTime:        date,
 				}
 				_, err = DeviceDao.AddOrUpdate_Device(device, 2)
@@ -830,6 +1130,15 @@ func (c *StockController) StockIn_Add() {
 		}
 	}
 
+	// 批量更新所有相关设备的批次号
+	err = DeviceDao.Update_Device_BatchNumber_ByInNumber(T_number, T_batch_number)
+	if err != nil {
+		o.Rollback()
+		c.Data["json"] = lib.JSONS{Code: 203, Msg: "更新设备批次号失败"}
+		c.ServeJSON()
+		return
+	}
+
 	o.Commit()
 	StockIn_Edit_StockMonth(T_date, T_depot_id, allProductList)
 
@@ -948,7 +1257,6 @@ func (c *StockController) StockIn_Edit() {
 		return
 	}
 	T_old_date := stockIn.T_date
-	T_old_batch_number := stockIn.T_batch_number
 
 	// 查询入库产品信息
 	productOldList := StockInProductDao.Read_StockInProduct_List_ByT_number(stockIn.T_number)
@@ -1057,6 +1365,7 @@ func (c *StockController) StockIn_Edit() {
 						T_iccid:           mqtt.Iccid,
 						T_imei:            mqtt.Imei,
 						T_State:           2,
+						T_batch_number:    T_batch_number,
 						CreateTime:        date,
 						UpdateTime:        stockIn.UpdateTime,
 					}
@@ -1272,10 +1581,11 @@ func (c *StockController) StockIn_Edit() {
 		StockInProductDao.Update_StockInProduct_T_date(stockIn.T_number, T_date)
 		DeviceDao.Update_Device_CreateTimeByT_in_number(stockIn.T_number, T_date)
 	}
-	if len(T_batch_number) > 0 && T_old_batch_number != T_batch_number {
-		// 修改出库产品日期
-		StockInProductDao.Update_StockInProduct_T_batch_number(stockIn.T_number, T_date)
-	}
+
+	// 修改出库产品日期
+	StockInProductDao.Update_StockInProduct_T_batch_number(stockIn.T_number, T_batch_number)
+	// 修改设备批次号
+	DeviceDao.Update_Device_BatchNumber_ByInNumber(stockIn.T_number, T_batch_number)
 
 	// 更新月份统计表
 	allProductList := []int{}
@@ -2102,12 +2412,18 @@ func (c *StockController) StockOut_Add() {
 			c.ServeJSON()
 			return
 		}
-		// 2、更新设备状态为已出库
 		if len(T_relation_sn) > 0 {
 			snList := lib.SplitString(T_relation_sn, ",")
 			num = len(snList)
 			for _, sn := range snList {
 				mqtt := Stock.Read_MqttUser(sn)
+
+				// 根据IMEI查询上次入库的批次号
+				batchNumber, err := DeviceDao.Read_Device_BatchNumber_ByImei(mqtt.Imei)
+				if err != nil {
+					logs.Error(lib.FuncName(), err)
+				}
+
 				device := Stock.Device{
 					T_contract_number: T_contract_number,
 					T_product_id:      product_id,
@@ -2117,6 +2433,7 @@ func (c *StockController) StockOut_Add() {
 					T_imei:            mqtt.Imei,
 					T_State:           1,
 					T_project:         T_project,
+					T_batch_number:    batchNumber,
 					CreateTime:        date,
 				}
 				_, err = DeviceDao.AddOrUpdate_Device(device, 1)
@@ -2134,7 +2451,7 @@ func (c *StockController) StockOut_Add() {
 			T_product_id:  product_id,
 			T_depot_id:    T_depot_id,
 			T_num:         num,    // 出库数量
-			T_date:        T_date, // 出库数量
+			T_date:        T_date, // 出库日期
 			T_relation_sn: T_relation_sn,
 			T_state:       1,
 		}
@@ -2553,6 +2870,7 @@ func (c *StockController) StockOut_Edit() {
 			if len(diff.T_add_relation_sn) > 0 {
 				for _, sn := range diff.T_add_relation_sn {
 					mqtt := Stock.Read_MqttUser(sn)
+
 					// 添加设备
 					device := Stock.Device{
 						T_contract_number: "",
@@ -3906,37 +4224,45 @@ func (c *StockController) StockOut_Warehouse() {
 			c.ServeJSON()
 			return
 		}
+		if len(T_relation_sn) > 0 {
+			snList := strings.Split(T_relation_sn, ",")
+			if product.T_relation_sn == 1 && num != len(snList) {
+				o.Rollback()
+				c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("%s SN数量与出库数量不一致,请核查!", product.T_name)}
+				c.ServeJSON()
+				return
+			}
 
-		snList := strings.Split(T_relation_sn, ",")
-		if product.T_relation_sn == 1 && num != len(snList) {
-			o.Rollback()
-			c.Data["json"] = lib.JSONS{Code: 202, Msg: fmt.Sprintf("%s SN数量与出库数量不一致,请核查!", product.T_name)}
-			c.ServeJSON()
-			return
-		}
+			if len(snList) > 0 {
 
-		// 2、更新设备状态为已出库
-		if len(snList) > 0 {
+				for _, sn := range snList {
+					mqtt := Stock.Read_MqttUser(sn)
 
-			for _, sn := range snList {
-				mqtt := Stock.Read_MqttUser(sn)
-				device := Stock.Device{
-					T_contract_number: StockOut.T_contract_number,
-					T_product_id:      product_id,
-					T_out_number:      T_number,
-					T_sn:              sn,
-					T_iccid:           mqtt.Iccid,
-					T_imei:            mqtt.Imei,
-					T_State:           1,
-					T_project:         StockOut.T_project,
-					CreateTime:        date,
-				}
-				_, err = DeviceDao.AddOrUpdate_Device(device, 1)
-				if err != nil {
-					o.Rollback()
-					c.Data["json"] = lib.JSONS{Code: 202, Msg: "出库失败"}
-					c.ServeJSON()
-					return
+					// 根据IMEI查询上次入库的批次号
+					batchNumber, err := DeviceDao.Read_Device_BatchNumber_ByImei(mqtt.Imei)
+					if err != nil {
+						logs.Error(lib.FuncName(), err)
+					}
+
+					device := Stock.Device{
+						T_contract_number: StockOut.T_contract_number,
+						T_product_id:      product_id,
+						T_out_number:      T_number,
+						T_sn:              sn,
+						T_iccid:           mqtt.Iccid,
+						T_imei:            mqtt.Imei,
+						T_State:           1,
+						T_project:         StockOut.T_project,
+						T_batch_number:    batchNumber,
+						CreateTime:        date,
+					}
+					_, err = DeviceDao.AddOrUpdate_Device(device, 1)
+					if err != nil {
+						o.Rollback()
+						c.Data["json"] = lib.JSONS{Code: 202, Msg: "出库失败"}
+						c.ServeJSON()
+						return
+					}
 				}
 			}
 		}

+ 29 - 1
models/IOTNetworkCard/IOTNetworkCard.go

@@ -3,11 +3,13 @@ package IOTNetworkCard
 import (
 	"ERP_storage/models/Account"
 	"encoding/json"
+	"time"
+
 	orm2 "github.com/beego/beego/v2/client/orm"
 	"gogs.baozhida.cn/zoie/ERP_libs/lib"
-	"time"
 
 	"ERP_storage/logs"
+
 	_ "github.com/astaxie/beego/cache/redis"
 	"github.com/beego/beego/v2/adapter/orm"
 	_ "github.com/go-sql-driver/mysql"
@@ -189,6 +191,32 @@ func Read_IOTNetworkCard_ByT_iccid(T_iccid string) (r IOTNetworkCard, err error)
 	return
 }
 
+// 批量获取物联网卡信息,避免N+1查询问题
+func Read_IOTNetworkCards_ByT_iccids(iccids []string) (map[string]IOTNetworkCard, error) {
+	if len(iccids) == 0 {
+		return make(map[string]IOTNetworkCard), nil
+	}
+
+	o := orm.NewOrm()
+	qs := o.QueryTable(new(IOTNetworkCard))
+
+	// 构建IN条件
+	var cards []IOTNetworkCard
+	_, err := qs.Filter("T_iccid__in", iccids).Filter("T_State__gt", 0).All(&cards)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return nil, err
+	}
+
+	// 构建ICCID到卡信息的映射
+	result := make(map[string]IOTNetworkCard, len(cards))
+	for _, card := range cards {
+		result[card.T_iccid] = card
+	}
+
+	return result, nil
+}
+
 func Read_IOTNetworkCard_ById(Id int) (r IOTNetworkCard, err error) {
 	o := orm.NewOrm()
 	qs := o.QueryTable(new(IOTNetworkCard))

+ 379 - 7
models/Stock/Device.go

@@ -6,12 +6,14 @@ import (
 	"ERP_storage/models/IOTNetworkCard"
 	"errors"
 	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
 	_ "github.com/astaxie/beego/cache/redis"
 	"github.com/beego/beego/v2/adapter/orm"
 	orm2 "github.com/beego/beego/v2/client/orm"
 	"gogs.baozhida.cn/zoie/ERP_libs/lib"
-	"strconv"
-	"time"
 )
 
 type Device struct {
@@ -28,6 +30,7 @@ type Device struct {
 	T_project         string    `orm:"type(text);null"`                                       // 出库项目
 	T_project_log     string    `orm:"type(text);null"`                                       // 出库项目
 	T_device_number   string    `orm:"size(256);null"`                                        // 移动端设备编号
+	T_batch_number    string    `orm:"type(256);null"`                                        // 批次号
 	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 保存时都会对时间自动更新
 }
@@ -75,6 +78,7 @@ type Device_R struct {
 	T_device_number string // 设备编号
 }
 
+// 单个设备转换
 func DeviceToDevice_R(t Device_R) (r Device_R) {
 	r.Id = t.Id
 	r.T_contract_number = t.T_contract_number
@@ -98,12 +102,87 @@ func DeviceToDevice_R(t Device_R) (r Device_R) {
 
 	r.T_product_class_name = Basic.Read_ProductClass_Get(t.T_product_class)
 
-	card, _ := IOTNetworkCard.Read_IOTNetworkCard_ByT_iccid(r.T_iccid)
-	r.T_device_number = card.T_device_number
+	// 单个查询物联网卡信息
+	if len(r.T_iccid) > 0 {
+		card, _ := IOTNetworkCard.Read_IOTNetworkCard_ByT_iccid(r.T_iccid)
+		r.T_device_number = card.T_device_number
+	}
 
 	return r
 }
 
+// 批量处理设备转换
+func DevicesToDevice_Rs(devices []Device_R) []Device_R {
+	if len(devices) == 0 {
+		return []Device_R{}
+	}
+
+	// 收集所有ICCID
+	iccids := make([]string, 0, len(devices))
+	for _, device := range devices {
+		if len(device.T_iccid) > 0 {
+			iccids = append(iccids, device.T_iccid)
+		}
+	}
+
+	// 批量查询物联网卡信息
+	cardMap, err := IOTNetworkCard.Read_IOTNetworkCards_ByT_iccids(iccids)
+	if err != nil {
+		logs.Error(lib.FuncName(), "批量查询物联网卡失败:", err)
+	}
+
+	// 批量查询产品类别信息
+	productClassMap := make(map[int]string)
+	for _, device := range devices {
+		if device.T_product_class > 0 {
+			if _, exists := productClassMap[device.T_product_class]; !exists {
+				productClassMap[device.T_product_class] = Basic.Read_ProductClass_Get(device.T_product_class)
+			}
+		}
+	}
+
+	// 转换设备信息
+	results := make([]Device_R, len(devices))
+	for i, device := range devices {
+		r := Device_R{
+			Id:                device.Id,
+			T_contract_number: device.T_contract_number,
+			T_product_id:      device.T_product_id,
+			T_in_number:       device.T_in_number,
+			T_out_number:      device.T_out_number,
+			T_sn:              device.T_sn,
+			T_iccid:           device.T_iccid,
+			T_imei:            device.T_imei,
+			T_State:           device.T_device_state,
+			T_device_state:    device.T_device_state,
+			T_remark:          device.T_remark,
+			T_project:         device.T_project,
+			T_project_log:     device.T_project_log,
+			T_product_name:    device.T_product_name,
+			T_product_class:   device.T_product_class,
+			T_product_model:   device.T_product_model,
+			T_product_spec:    device.T_product_spec,
+			T_product_img:     device.T_product_img,
+		}
+
+		// 设置产品类别名称
+		if className, exists := productClassMap[device.T_product_class]; exists {
+			r.T_product_class_name = className
+		}
+
+		// 设置设备编号
+		if len(device.T_iccid) > 0 {
+			if card, exists := cardMap[device.T_iccid]; exists {
+				r.T_device_number = card.T_device_number
+			}
+		}
+
+		results[i] = r
+	}
+
+	return results
+}
+
 // 添加
 func (dao *DeviceDaoImpl) Add_Device(r Device) (id int64, err error) {
 	now := time.Now().Format("15:04:05")
@@ -181,6 +260,125 @@ func (dao *DeviceDaoImpl) Read_Device_List(T_name, T_product_name, T_product_mod
 	} else {
 		offset = (page - 1) * page_z
 	}
+
+	// 使用参数化查询代替字符串拼接,防止SQL注入并提高性能
+	whereConditions := []string{"d.t__state > 0"}
+	var params []interface{}
+
+	// 构建搜索条件
+	if len(T_name) > 0 {
+		searchTerm := "%" + T_name + "%"
+		whereConditions = append(whereConditions, "(d.t_contract_number LIKE ? OR d.t_out_number LIKE ? OR d.t_sn LIKE ? OR d.t_iccid LIKE ? OR d.t_project LIKE ?)")
+		params = append(params, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm)
+	}
+
+	if T_State > 0 {
+		whereConditions = append(whereConditions, "d.t__state = ?")
+		params = append(params, T_State)
+	}
+
+	if len(T_product_name) > 0 {
+		whereConditions = append(whereConditions, "p.t_name LIKE ?")
+		params = append(params, "%"+T_product_name+"%")
+	}
+
+	if len(T_product_model) > 0 {
+		whereConditions = append(whereConditions, "p.t_model LIKE ?")
+		params = append(params, "%"+T_product_model+"%")
+	}
+
+	// 组合WHERE条件
+	whereClause := " WHERE " + strings.Join(whereConditions, " AND ")
+
+	// 优化子查询:使用窗口函数替代子查询(MySQL 5.7不支持窗口函数,使用JOIN优化)
+	// 使用FORCE INDEX提示优化器使用主键索引
+	countSQL := `
+		SELECT COUNT(*) 
+		FROM (
+			SELECT d.t_sn
+			FROM device d
+			FORCE INDEX (PRIMARY)
+			JOIN product p ON d.t_product_id = p.ID
+			JOIN (
+				SELECT t_sn, MAX(create_time) AS max_create_time 
+				FROM device 
+				GROUP BY t_sn
+			) latest ON d.t_sn = latest.t_sn AND d.create_time = latest.max_create_time
+			` + whereClause + `
+		) AS count_query`
+
+	// 执行计数查询
+	var count int64
+	err := dao.orm.Raw(countSQL, params...).QueryRow(&count)
+	if err != nil {
+		logs.Error(lib.FuncName(), "Count query error:", err)
+		return nil, 0
+	}
+
+	if count == 0 {
+		return nil, 0
+	}
+
+	// 主查询 - 只选择需要的列,避免使用SELECT *
+	dataSQL := `
+		SELECT 
+			d.ID, d.t_contract_number, d.t_product_id, d.t_in_number, d.t_out_number, 
+			d.t_sn, d.t_iccid, d.t_imei, d.t__state AS t_device_state, d.t_remark,
+			d.t_project, d.t_project_log, d.t_device_number, d.t_batch_number,
+			p.t_name AS t_product_name, p.t_model AS t_product_model, 
+			p.t_class AS t_product_class, p.t_spec AS t_product_spec, p.t_img AS t_product_img
+		FROM device d
+		FORCE INDEX (PRIMARY)
+		JOIN product p ON d.t_product_id = p.ID
+		JOIN (
+			SELECT t_sn, MAX(create_time) AS max_create_time 
+			FROM device 
+			GROUP BY t_sn
+		) latest ON d.t_sn = latest.t_sn AND d.create_time = latest.max_create_time
+		` + whereClause + `
+		ORDER BY d.create_time DESC`
+
+	// 添加分页
+	if page_z != 9999 {
+		dataSQL += " LIMIT ?, ?"
+		params = append(params, offset, page_z)
+	}
+
+	// 执行数据查询
+	var maps []Device_R
+	_, err = dao.orm.Raw(dataSQL, params...).QueryRows(&maps)
+	if err != nil {
+		logs.Error(lib.FuncName(), "Data query error:", err)
+		return nil, 0
+	}
+
+	// 批量获取设备历史记录,避免N+1查询
+	if len(maps) > 0 {
+		// 批量获取历史记录
+		historyMap := dao.batchReadDeviceHistory(maps)
+
+		// 填充历史记录
+		for i := range maps {
+			if history, exists := historyMap[maps[i].T_sn]; exists {
+				maps[i].T_remark = history.T_remark
+				maps[i].T_project_log = history.T_project_log
+			}
+		}
+
+		// 批量处理设备转换,避免N+1查询
+		r = DevicesToDevice_Rs(maps)
+	}
+
+	return r, count
+}
+
+func (dao *DeviceDaoImpl) Read_Device_List_upback(T_name, T_product_name, T_product_model string, T_State, page, page_z int) (r []Device_R, cnt int64) {
+	var offset int
+	if page <= 1 {
+		offset = 0
+	} else {
+		offset = (page - 1) * page_z
+	}
 	// 过滤
 
 	sqlWhere := " WHERE d.t__state > 0"
@@ -217,8 +415,9 @@ func (dao *DeviceDaoImpl) Read_Device_List(T_name, T_product_name, T_product_mod
 	sql = sql + sqlWhere
 
 	sql += " ORDER BY d.create_time DESC"
-
-	sql += " LIMIT " + strconv.Itoa(offset) + "," + strconv.Itoa(page_z)
+	if page_z != 9999 {
+		sql += " LIMIT " + strconv.Itoa(offset) + "," + strconv.Itoa(page_z)
+	}
 
 	fmt.Println(sql)
 	var maps []Device_R
@@ -234,6 +433,97 @@ func (dao *DeviceDaoImpl) Read_Device_List(T_name, T_product_name, T_product_mod
 	count, _ := strconv.Atoi(maps_z[0][0].(string))
 	return r, int64(count)
 }
+
+/**
+ * 批量获取设备历史记录,避免N+1查询问题
+ * @param devices 设备列表
+ * @return 设备SN到历史记录的映射
+ */
+func (dao *DeviceDaoImpl) batchReadDeviceHistory(devices []Device_R) map[string]struct {
+	T_remark      string
+	T_project_log string
+} {
+	if len(devices) == 0 {
+		return make(map[string]struct {
+			T_remark      string
+			T_project_log string
+		})
+	}
+
+	// 收集所有设备SN
+	snList := make([]string, 0, len(devices))
+	for _, device := range devices {
+		snList = append(snList, device.T_sn)
+	}
+
+	// 构建IN子句
+	placeholders := make([]string, len(snList))
+	args := make([]interface{}, len(snList))
+	for i, sn := range snList {
+		placeholders[i] = "?"
+		args[i] = sn
+	}
+
+	// 批量查询所有设备的历史记录
+	sql := `SELECT t_sn, t__state, create_time, t_out_number, t_in_number, t_project
+			FROM device 
+			WHERE t_sn IN (` + strings.Join(placeholders, ",") + `)
+			ORDER BY t_sn, create_time`
+
+	var historyRecords []struct {
+		T_sn         string
+		T_State      int
+		CreateTime   time.Time
+		T_out_number string
+		T_in_number  string
+		T_project    string
+	}
+
+	_, err := dao.orm.Raw(sql, args...).QueryRows(&historyRecords)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return make(map[string]struct {
+			T_remark      string
+			T_project_log string
+		})
+	}
+
+	// 按设备SN分组并构建历史记录
+	result := make(map[string]struct {
+		T_remark      string
+		T_project_log string
+	})
+
+	for _, record := range historyRecords {
+		if _, exists := result[record.T_sn]; !exists {
+			result[record.T_sn] = struct {
+				T_remark      string
+				T_project_log string
+			}{}
+		}
+
+		history := result[record.T_sn]
+		if record.T_State == 1 {
+			// 出库记录
+			history.T_remark += fmt.Sprintf("%s:%s(%s)|", record.CreateTime.Format("2006-01-02"), "出库", record.T_out_number)
+			if len(record.T_project) > 0 {
+				history.T_project_log += fmt.Sprintf("%s:%s(%s):%s|", record.CreateTime.Format("2006-01-02"), "出库", record.T_out_number, record.T_project)
+			}
+		} else if record.T_State == 2 {
+			// 入库记录
+			history.T_remark += fmt.Sprintf("%s:%s(%s)|", record.CreateTime.Format("2006-01-02"), "入库", record.T_in_number)
+		}
+		result[record.T_sn] = history
+	}
+
+	return result
+}
+
+/**
+ * 获取单个设备的历史记录(保留原方法以兼容其他调用)
+ * @param T_sn 设备SN
+ * @return 备注和项目日志
+ */
 func (dao *DeviceDaoImpl) Read_Device_History(T_sn string) (T_remark string, T_project string) {
 	qs := dao.orm.QueryTable(new(Device))
 	var maps []Device
@@ -253,7 +543,6 @@ func (dao *DeviceDaoImpl) Read_Device_History(T_sn string) (T_remark string, T_p
 		if v.T_State == 2 {
 			T_remark += fmt.Sprintf("%s:%s(%s)|", v.CreateTime.Format("2006-01-02"), "入库", v.T_in_number)
 		}
-
 	}
 
 	return
@@ -373,3 +662,86 @@ func (dao *DeviceDaoImpl) Update_Device_CreateTimeByT_in_number(T_number, T_date
 
 	return nil
 }
+
+/**
+ * 根据IMEI查询上次入库的批次号
+ * @param imei 设备IMEI
+ * @return 批次号,如果未找到则返回空字符串
+ */
+func (dao *DeviceDaoImpl) Read_Device_BatchNumber_ByImei(imei string) (string, error) {
+	if len(imei) == 0 {
+		return "", nil
+	}
+
+	// 直接查询device表,获取该IMEI对应的最新入库记录的批次号
+	// T_State = 2 是入库状态,入库时已将t_batch_number保存到device表
+	sql := `SELECT t_batch_number 
+			FROM device 
+			WHERE t_imei = ? AND t__state = 2 
+			ORDER BY create_time DESC 
+			LIMIT 1`
+
+	var batchNumber string
+	err := dao.orm.Raw(sql, imei).QueryRow(&batchNumber)
+	if err != nil {
+		if err.Error() == orm.ErrNoRows.Error() {
+			// 未找到记录,返回空字符串
+			return "", nil
+		}
+		logs.Error(lib.FuncName(), err)
+		return "", err
+	}
+
+	return batchNumber, nil
+}
+
+/**
+ * 根据入库单号批量更新所有相关设备的批次号
+ * 先查询出所有t_in_number对应的设备,然后根据T_imei修改,不属于该入库编号的也要同步修改
+ * @param inNumber 入库单号
+ * @param batchNumber 批次号
+ * @return 错误信息
+ */
+func (dao *DeviceDaoImpl) Update_Device_BatchNumber_ByInNumber(inNumber, batchNumber string) error {
+	if len(inNumber) == 0 || len(batchNumber) == 0 {
+		return nil
+	}
+
+	// 1. 先查询出所有该入库单号下的设备IMEI列表
+	sql := `SELECT DISTINCT t_imei FROM device WHERE t_in_number = ? AND t__state = 2 AND t_imei IS NOT NULL AND t_imei != ''`
+
+	var imeiList []string
+	_, err := dao.orm.Raw(sql, inNumber).QueryRows(&imeiList)
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return err
+	}
+
+	if len(imeiList) == 0 {
+		// 没有找到相关设备,直接返回
+		return nil
+	}
+
+	// 2. 根据IMEI列表批量更新所有相关设备的批次号(包括不属于当前入库编号的设备)
+	// 构建IN子句
+	placeholders := make([]string, len(imeiList))
+	args := make([]interface{}, len(imeiList)+1)
+	args[0] = batchNumber
+
+	for i, imei := range imeiList {
+		placeholders[i] = "?"
+		args[i+1] = imei
+	}
+
+	updateSQL := `UPDATE device 
+				  SET t_batch_number = ? 
+				  WHERE t_imei IN (` + strings.Join(placeholders, ",") + `)`
+
+	_, err = dao.orm.Raw(updateSQL, args...).Exec()
+	if err != nil {
+		logs.Error(lib.FuncName(), err)
+		return err
+	}
+
+	return nil
+}

+ 4 - 2
models/Stock/StockOutProduct.go

@@ -3,12 +3,14 @@ package Stock
 import (
 	"ERP_storage/models/Account"
 	"ERP_storage/models/Basic"
-	orm2 "github.com/beego/beego/v2/client/orm"
-	"gogs.baozhida.cn/zoie/ERP_libs/lib"
 	"strconv"
 	"time"
 
+	orm2 "github.com/beego/beego/v2/client/orm"
+	"gogs.baozhida.cn/zoie/ERP_libs/lib"
+
 	"ERP_storage/logs"
+
 	_ "github.com/astaxie/beego/cache/redis"
 	"github.com/beego/beego/v2/adapter/orm"
 	_ "github.com/go-sql-driver/mysql"

+ 5 - 2
routers/Stock.go

@@ -2,14 +2,17 @@ package routers
 
 import (
 	"ERP_storage/controllers"
+
 	beego "github.com/beego/beego/v2/server/web"
 )
 
 func init() {
 
 	device := beego.NewNamespace("/Device",
-		beego.NSRouter("/List", &controllers.StockController{}, "*:Device_List"),   // 设备列表
-		beego.NSRouter("/Check", &controllers.StockController{}, "*:Device_Check"), // 检查设备
+		beego.NSRouter("/List", &controllers.StockController{}, "*:Device_List"),             // 设备列表
+		beego.NSRouter("/Check", &controllers.StockController{}, "*:Device_Check"),           // 检查设备
+		beego.NSRouter("/Excel", &controllers.StockController{}, "*:Device_Excel"),           // 导出设备
+		beego.NSRouter("/Take_Stock", &controllers.StockController{}, "*:Device_Take_Stock"), // 盘点设备
 	)
 	stock := beego.NewNamespace("/Stock",
 		beego.NSRouter("/List", &controllers.StockController{}, "*:Stock_List"),                 // 库存列表

+ 17 - 18
tests/default_test.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"encoding/json"
 	"fmt"
 	"testing"
 	"time"
@@ -23,21 +22,21 @@ func generateMonthList(startMonth time.Time) []string {
 }
 
 func TestJson(t *testing.T) {
-	jsonStr := "[\"2025152150736858\",\"2025151362687552\",\"2025151858776242\"]"
-
-	// 定义一个字符串切片用于存储解析后的数据
-	var strSlice []string
-
-	// 解析JSON字符串到切片
-	err := json.Unmarshal([]byte(jsonStr), &strSlice)
-	if err != nil {
-		fmt.Printf("解析JSON错误: %v\n", err)
-		return
-	}
-
-	// 打印解析后的切片
-	fmt.Println("解析后的切片:")
-	for i, s := range strSlice {
-		fmt.Printf("  元素%d: %s\n", i+1, s)
-	}
+	//jsonStr := "[\"2025152150736858\",\"2025151362687552\",\"2025151858776242\"]"
+	//
+	//// 定义一个字符串切片用于存储解析后的数据
+	//var strSlice []string
+	//
+	//// 解析JSON字符串到切片
+	//err := json.Unmarshal([]byte(jsonStr), &strSlice)
+	//if err != nil {
+	//	fmt.Printf("解析JSON错误: %v\n", err)
+	//	return
+	//}
+	//
+	//// 打印解析后的切片
+	//fmt.Println("解析后的切片:")
+	//for i, s := range strSlice {
+	//	fmt.Printf("  元素%d: %s\n", i+1, s)
+	//}
 }