bluetooth.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. package item
  2. import (
  3. "errors"
  4. "fmt"
  5. "go.uber.org/zap"
  6. "gorm.io/gorm"
  7. "math"
  8. "server/dao"
  9. "server/global"
  10. "server/model"
  11. "server/model/item/request"
  12. "time"
  13. )
  14. type BluetoothService struct {
  15. }
  16. func HandleTempScanDeviceData(bv model.BeaconValue) {
  17. // 查询网关
  18. var gw dao.Gateway
  19. global.GVA_DB.Where("device_name = ?", bv.DeviceName).First(&gw)
  20. // 组装临时记录
  21. temp := dao.TempScanDevice{
  22. GatewayID: gw.ID,
  23. GatewaySN: bv.DeviceName,
  24. AreaNo: bv.Area,
  25. ClusterNo: bv.Cluster,
  26. Number: bv.Number,
  27. UUID: bv.UUID,
  28. FirmwareVer: bv.Version,
  29. DeviceType: "lamp",
  30. IsSelected: 0,
  31. ScanTime: time.Now(),
  32. }
  33. // 同网关+UUID自动覆盖旧扫描记录
  34. global.GVA_DB.Where("gateway_id = ? AND uuid = ?", gw.ID, temp.UUID).Assign(temp).FirstOrCreate(&temp)
  35. }
  36. func HandleHeartBeat(deviceName, uuid string) {
  37. now := time.Now()
  38. updateGatewayData := map[string]interface{}{
  39. "online": 1,
  40. "online_time": now,
  41. }
  42. // 网关deviceName匹配,更新网关在线时间
  43. err := global.GVA_DB.Model(&dao.Gateway{}).Where("device_name = ?", deviceName).
  44. Updates(updateGatewayData).Error
  45. if err != nil {
  46. // 网关不存在/更新失败仅打日志,不阻断灯具更新逻辑
  47. global.GVA_LOG.Error("心跳:", zap.Any("err", err))
  48. return
  49. }
  50. // 以UUID为唯一主键匹配灯具
  51. updateData := map[string]interface{}{
  52. "online": 1,
  53. "online_time": now,
  54. }
  55. // 仅更新,不存在则不操作
  56. global.GVA_DB.Model(&dao.Bluetooth{}).
  57. Where("uuid = ?", uuid).
  58. Updates(updateData)
  59. }
  60. func (bs *BluetoothService) QueryAllTempScanDevices() ([]dao.TempScanDevice, error) {
  61. return dao.QueryAllTempScanDevices()
  62. }
  63. func (bs *BluetoothService) DeviceSendCmd(info request.DeviceSendCmdData) error {
  64. gateway, err := dao.QueryGatewayByDeviceName(info.DeviceName)
  65. if err != nil {
  66. return err
  67. }
  68. return DeviceSendCmd(gateway.ProductKey, gateway.DeviceName, gateway.NetPwd, "00 00", "00 00", info.Action, info.Params)
  69. }
  70. func (bs *BluetoothService) GetDeviceData(info request.GetDeviceOrderData) error {
  71. gateway, err := dao.QueryGatewayByDeviceName(info.DeviceName)
  72. if err != nil {
  73. return err
  74. }
  75. return GetSetting(gateway.ProductKey, gateway.DeviceName, gateway.NetPwd, "00 00", "00 00")
  76. }
  77. func GetDeviceConsumption() error {
  78. gateways, err := dao.QueryAllGateways()
  79. if err != nil {
  80. return err
  81. }
  82. for _, gateway := range gateways {
  83. go DeviceSendCmd(gateway.ProductKey, gateway.DeviceName, gateway.NetPwd, "00 00", "00 00", "getConsumption", "")
  84. }
  85. return nil
  86. }
  87. func (bs *BluetoothService) ScanAllDevice(info request.GetDeviceOrderData) error {
  88. gateway, err := dao.QueryGatewayByDeviceName(info.DeviceName)
  89. if err != nil {
  90. return err
  91. }
  92. return ScanAll(gateway.ProductKey, gateway.DeviceName, gateway.NetPwd, "00 00")
  93. }
  94. func (bs *BluetoothService) CreateBluetooth(info request.CreateBluetoothData) error {
  95. // 1、插入正式灯具表
  96. blu := dao.Bluetooth{
  97. GatewayID: uint(info.GatewayId),
  98. UUID: info.Device.UUID,
  99. AreaNo: info.Device.AreaNo,
  100. ClusterNo: info.Device.ClusterNo,
  101. Number: info.Device.Number,
  102. DeviceType: info.Device.DeviceType,
  103. Online: 0,
  104. }
  105. err := blu.CreateBluetooth()
  106. if err != nil {
  107. return err
  108. }
  109. // 2、更新对应临时设备 IsSelected=1(已入库)
  110. tempModel := dao.TempScanDevice{}
  111. err = global.GVA_DB.Model(&tempModel).
  112. Where("gateway_id = ? AND uuid = ?", info.Device.GatewayID, info.Device.UUID).
  113. Update("is_selected", 1).Error
  114. return err
  115. }
  116. func HandleSceneData(sv model.SceneValue) {
  117. now := time.Now()
  118. // 更新字段映射
  119. updateMap := map[string]interface{}{
  120. "scene_no": sv.SceneNo,
  121. "high_bright": sv.HighBright,
  122. "standby_bright": sv.StandbyBright,
  123. "cct_bright": sv.CctBright,
  124. "delay_time": sv.DelayTime,
  125. "delay_time2": sv.DelayTime2,
  126. "light_mode": sv.LightMode,
  127. "delay_mode": sv.DelayMode,
  128. "als_control_mode": sv.AlsControlMode,
  129. "scene_validity": sv.SceneValidity,
  130. "online": 1,
  131. "online_time": now,
  132. }
  133. // 根据唯一UUID更新灯具记录
  134. err := global.GVA_DB.Model(&dao.Bluetooth{}).
  135. Where("uuid = ?", sv.UUID).
  136. Updates(updateMap).Error
  137. if err != nil {
  138. global.GVA_LOG.Error("处理情景参数上报失败",
  139. zap.String("uuid", sv.UUID),
  140. zap.Error(err))
  141. return
  142. }
  143. // 顺带刷新网关在线时间(DeviceName为网关SN)
  144. err = global.GVA_DB.Model(&dao.Gateway{}).
  145. Where("device_name = ?", sv.DeviceName).
  146. Updates(map[string]interface{}{
  147. "online_time": now,
  148. "online": 1,
  149. }).Error
  150. if err != nil {
  151. global.GVA_LOG.Error("情景上报刷新网关在线失败",
  152. zap.String("gatewaySn", sv.DeviceName),
  153. zap.Error(err))
  154. }
  155. }
  156. func (bs *BluetoothService) QueryBluetoothsByGatewayID(gatewayId int) ([]dao.Bluetooth, error) {
  157. return dao.QueryBluetoothsByGatewayID(gatewayId)
  158. }
  159. func CreateDeviceConsumption(val model.ConsumptionValue) error {
  160. // 1. 获取网关信息
  161. gateway, err := dao.QueryGatewayByDeviceName(val.DeviceName)
  162. if err != nil {
  163. return err
  164. }
  165. // 2. 查询该灯具(UUID)上一次上报的记录
  166. var lastRecord dao.DeviceConsumption
  167. // 根据 UUID 倒序查询最新的一条
  168. err = global.GVA_DB.Where("uuid = ?", val.UUID).
  169. Order("acquisition_ts DESC").
  170. First(&lastRecord).Error
  171. // 3. 初始化本次存储的“增量”数据(默认等于硬件上报值,以防第一次上报)
  172. deltaTimeDur := val.TimeDur
  173. deltaEnergyDur := val.EnergyDur
  174. // 4. 如果历史记录存在,计算出差值
  175. if err == nil { // 说明查到了上次记录
  176. // 防御性编程:防止硬件重启导致 TimeDur 数值变小变成负数
  177. if val.TimeDur > lastRecord.TimeDur {
  178. deltaTimeDur = val.TimeDur - lastRecord.TimeDur
  179. }
  180. if val.EnergyDur > lastRecord.EnergyDur {
  181. deltaEnergyDur = val.EnergyDur - lastRecord.EnergyDur
  182. }
  183. }
  184. // 5. 根据差值,重新计算本次的实际和理论能耗 (度)
  185. deltaActualKwh := float64(deltaEnergyDur) * val.Power / 3600 / 1000
  186. deltaTheoryKwh := float64(deltaTimeDur) * val.Power / 3600 / 1000
  187. // 6. 构建要落库的结构体,此时存入的都是 【差值】
  188. dc := dao.DeviceConsumption{
  189. GatewayID: gateway.ID,
  190. UUID: val.UUID,
  191. AreaNo: val.Area,
  192. Number: val.Number,
  193. DeviceName: val.DeviceName,
  194. AcquisitionTs: val.AcquisitionTime,
  195. TimeDur: deltaTimeDur, // 【关键修改】存差值,而不是源值
  196. SensorDur: val.SensorDur, // 如果 sensor_dur 也是总累积,也需要按此逻辑做减法
  197. EnergyDur: deltaEnergyDur, // 【关键修改】存差值
  198. Power: val.Power,
  199. ActualKwh: deltaActualKwh, // 【关键修改】存差值对应的实际耗电
  200. TheoryKwh: deltaTheoryKwh, // 【关键修改】存差值对应的理论耗电
  201. }
  202. // 7. 调用你原有的 dao 方法,直接落库
  203. return dc.CreateDeviceConsumption()
  204. }
  205. // SummaryWeekConsumption 按网关维度聚合每周总能耗
  206. func SummaryWeekConsumption() error {
  207. db := global.GVA_DB.Begin()
  208. defer func() {
  209. if err := recover(); err != nil {
  210. global.GVA_LOG.Error("能耗汇总panic", zap.Any("err", err))
  211. db.Rollback()
  212. }
  213. }()
  214. now := time.Now()
  215. curYear, curWeek := now.ISOWeek()
  216. // ========== 第一步:先统计【每一盏灯具】每周能耗 ==========
  217. // 先删除本周已存在的灯具维度汇总数据,避免重复累加
  218. err := db.Where("year = ? AND week = ? AND uuid <> ''", curYear, curWeek).
  219. Delete(&dao.WeekConsumptionSummary{}).Error
  220. if err != nil {
  221. db.Rollback()
  222. global.GVA_LOG.Error("删除本周灯具历史汇总失败", zap.Error(err))
  223. return err
  224. }
  225. type LampSummary struct {
  226. GatewayID uint
  227. UUID string
  228. Year int
  229. Week int
  230. ActualTotal float64
  231. TheoryTotal float64
  232. }
  233. var lampSummaryList []LampSummary
  234. // 按网关、灯具UUID、年、周分组求和
  235. err = db.Model(&dao.DeviceConsumption{}).
  236. Select(`gateway_id, uuid,
  237. YEAR(FROM_UNIXTIME(acquisition_ts)) AS year,
  238. WEEK(FROM_UNIXTIME(acquisition_ts), 1) AS week,
  239. SUM(actual_kwh) AS actual_total,
  240. SUM(theory_kwh) AS theory_total`).
  241. Group("gateway_id, uuid, year, week").
  242. Scan(&lampSummaryList).Error
  243. if err != nil {
  244. db.Rollback()
  245. global.GVA_LOG.Error("灯具周能耗聚合失败", zap.Error(err))
  246. return err
  247. }
  248. // 批量插入灯具维度周汇总
  249. var lampInsertList []dao.WeekConsumptionSummary
  250. for _, item := range lampSummaryList {
  251. lampInsertList = append(lampInsertList, dao.WeekConsumptionSummary{
  252. GatewayID: item.GatewayID,
  253. UUID: item.UUID,
  254. Year: item.Year,
  255. Week: item.Week,
  256. ActualTotal: item.ActualTotal,
  257. TheoryTotal: item.TheoryTotal,
  258. })
  259. }
  260. if len(lampInsertList) > 0 {
  261. if err = db.CreateInBatches(lampInsertList, 500).Error; err != nil {
  262. db.Rollback()
  263. return err
  264. }
  265. }
  266. // ========== 第二步:统计【单个网关下所有灯具】每周总能耗(前端图表用) ==========
  267. // UUID为空代表网关全局汇总
  268. err = db.Where("year = ? AND week = ? AND uuid = ''", curYear, curWeek).
  269. Delete(&dao.WeekConsumptionSummary{}).Error
  270. if err != nil {
  271. db.Rollback()
  272. return err
  273. }
  274. type GatewaySummary struct {
  275. GatewayID uint
  276. Year int
  277. Week int
  278. ActualTotal float64
  279. TheoryTotal float64
  280. }
  281. var gatewaySummaryList []GatewaySummary
  282. // 只按网关、年、周分组,自动累加该网关下所有灯具能耗
  283. err = db.Model(&dao.DeviceConsumption{}).
  284. Select(`gateway_id,
  285. YEAR(FROM_UNIXTIME(acquisition_ts)) AS year,
  286. WEEK(FROM_UNIXTIME(acquisition_ts), 1) AS week,
  287. SUM(actual_kwh) AS actual_total,
  288. SUM(theory_kwh) AS theory_total`).
  289. Group("gateway_id, year, week").
  290. Scan(&gatewaySummaryList).Error
  291. if err != nil {
  292. db.Rollback()
  293. return err
  294. }
  295. // 网关总汇总:UUID赋值为空字符串
  296. var gatewayInsertList []dao.WeekConsumptionSummary
  297. for _, item := range gatewaySummaryList {
  298. gatewayInsertList = append(gatewayInsertList, dao.WeekConsumptionSummary{
  299. GatewayID: item.GatewayID,
  300. UUID: "", // 空标识网关全局汇总
  301. Year: item.Year,
  302. Week: item.Week,
  303. ActualTotal: item.ActualTotal,
  304. TheoryTotal: item.TheoryTotal,
  305. })
  306. }
  307. if len(gatewayInsertList) > 0 {
  308. if err = db.CreateInBatches(gatewayInsertList, 500).Error; err != nil {
  309. db.Rollback()
  310. return err
  311. }
  312. }
  313. db.Commit()
  314. global.GVA_LOG.Info("本周网关&灯具能耗汇总统计完成")
  315. return nil
  316. }
  317. // QueryDataDashboardParameter 获取数据大屏综合参数
  318. func (bs *BluetoothService) QueryDataDashboardParameter(req model.DashboardRequest) model.DashboardResponse {
  319. db := global.GVA_DB
  320. // 1. 获取所有网关列表 (下拉框数据)
  321. var gateways []dao.Gateway
  322. if err := db.Find(&gateways).Error; err != nil {
  323. global.GVA_LOG.Error("获取网关列表失败", zap.Error(err))
  324. }
  325. // 2. 获取可供选择的历史周列表 (下拉框数据)
  326. // 注意:如果选了网关,只展示该网关下的历史周
  327. var rawWeeks []struct {
  328. Year int
  329. Week int
  330. }
  331. weekQuery := db.Model(&dao.WeekConsumptionSummary{}).Select("DISTINCT year, week").Where("uuid = ''")
  332. if req.GatewayID != 0 {
  333. weekQuery = weekQuery.Where("gateway_id = ?", req.GatewayID)
  334. }
  335. weekQuery.Order("year DESC, week DESC").Scan(&rawWeeks)
  336. var availableWeeks []model.AvailableWeek
  337. for _, r := range rawWeeks {
  338. availableWeeks = append(availableWeeks, model.AvailableWeek{
  339. Year: r.Year,
  340. Week: r.Week,
  341. Label: fmt.Sprintf("%d 第 %d 周", r.Year, r.Week),
  342. })
  343. }
  344. // 3. 默认时间处理:若前端没传年份/周数,则使用表中最新的周
  345. if req.Year == 0 || req.Week == 0 {
  346. if len(rawWeeks) > 0 {
  347. req.Year, req.Week = rawWeeks[0].Year, rawWeeks[0].Week
  348. } else {
  349. // 如果没有任何记录,则使用当前系统周(兜底)
  350. now := time.Now()
  351. req.Year, req.Week = now.ISOWeek()
  352. }
  353. }
  354. // ================= 核心数据聚合 =================
  355. // 4. 获取指定网关、指定周的汇总能耗数据 (实际耗电、理论耗电)
  356. var actualEnergy, theoryEnergy float64
  357. var weekSummary dao.WeekConsumptionSummary
  358. querySummary := db.Where("uuid = ''")
  359. if req.GatewayID != 0 {
  360. querySummary = querySummary.Where("gateway_id = ?", req.GatewayID)
  361. }
  362. err := querySummary.Where("year = ? AND week = ?", req.Year, req.Week).
  363. First(&weekSummary).Error
  364. if err == nil {
  365. actualEnergy = weekSummary.ActualTotal
  366. theoryEnergy = weekSummary.TheoryTotal
  367. } else if !errors.Is(err, gorm.ErrRecordNotFound) {
  368. global.GVA_LOG.Error("查询周汇总能耗失败", zap.Error(err))
  369. }
  370. // 5. 计算碳排放和占比
  371. carbonReduction := math.Max(0, theoryEnergy-actualEnergy)
  372. carbonReductionRatio := 0.0
  373. if theoryEnergy > 0 {
  374. carbonReductionRatio = (carbonReduction / theoryEnergy) * 100
  375. }
  376. // ================= 统计灯具与预警数 =================
  377. var totalLuminaires int64
  378. lampsQuery := db.Model(&dao.Bluetooth{})
  379. if req.GatewayID != 0 {
  380. lampsQuery = lampsQuery.Where("gateway_id = ?", req.GatewayID)
  381. }
  382. lampsQuery.Count(&totalLuminaires)
  383. var warningCount int64
  384. alarmQuery := db.Model(&dao.Bluetooth{}).Where("online = 0")
  385. if req.GatewayID != 0 {
  386. alarmQuery = alarmQuery.Where("gateway_id = ?", req.GatewayID)
  387. }
  388. alarmQuery.Count(&warningCount)
  389. // ================= 获取回路设备(智能控制)列表 =================
  390. var devices []dao.Device
  391. if err := db.Group("device.id").Find(&devices).Error; err != nil {
  392. global.GVA_LOG.Error("查询回路设备失败", zap.Error(err))
  393. }
  394. loopDeviceCount := len(devices)
  395. // ================= 趋势图数据计算 =================
  396. var trends []model.TrendData
  397. var yearOnYearTrends []model.TrendData
  398. // 只有当传了确切的周数才处理趋势图,否则无法计算时间范围
  399. if req.Year != 0 && req.Week != 0 {
  400. // 6. 获取本周每日能耗趋势
  401. startTs, endTs := getWeekRange(req.Year, req.Week) // 修正后的ISO周计算
  402. var dailyData []dailyTrendDB
  403. trendQuery := db.Model(&dao.DeviceConsumption{})
  404. if req.GatewayID != 0 {
  405. trendQuery = trendQuery.Where("gateway_id = ?", req.GatewayID)
  406. }
  407. err = trendQuery.
  408. Select("DATE_FORMAT(CONVERT_TZ(FROM_UNIXTIME(acquisition_ts), '+00:00', '+08:00'), '%Y-%m-%d') as date_str, "+
  409. "SUM(actual_kwh) as actual_total, SUM(theory_kwh) as theory_total").
  410. Where("acquisition_ts >= ? AND acquisition_ts <= ?", startTs, endTs).
  411. Group("date_str").
  412. Order("date_str ASC").
  413. Scan(&dailyData).Error
  414. if err != nil {
  415. global.GVA_LOG.Error("获取本周每日趋势失败", zap.Error(err))
  416. }
  417. // 序列化为 7天的标准 TrendData (补齐缺失的日期为0)
  418. trendMap := make(map[string]model.TrendData)
  419. for _, d := range dailyData {
  420. trendMap[d.DateStr] = model.TrendData{Date: d.DateStr, Actual: d.Actual, Theory: d.Theory}
  421. }
  422. for i := 0; i < 7; i++ {
  423. dayUnix := startTs + int64(i*24*3600)
  424. dayStr := time.Unix(dayUnix, 0).Format("2006-01-02")
  425. if val, ok := trendMap[dayStr]; ok {
  426. trends = append(trends, val)
  427. } else {
  428. trends = append(trends, model.TrendData{Date: dayStr, Actual: 0, Theory: 0})
  429. }
  430. }
  431. // 7. 获取去年同周同比数据 (按星期几分组)
  432. lastYear, lastWeek := getPrevYearWeek(req.Year, req.Week)
  433. lastStartTs, lastEndTs := getWeekRange(lastYear, lastWeek)
  434. var lastYearData []weeklyTrendDB
  435. yearTrendQuery := db.Model(&dao.DeviceConsumption{})
  436. if req.GatewayID != 0 {
  437. yearTrendQuery = yearTrendQuery.Where("gateway_id = ?", req.GatewayID)
  438. }
  439. err = yearTrendQuery.
  440. Select("WEEKDAY(CONVERT_TZ(FROM_UNIXTIME(acquisition_ts), '+00:00', '+08:00')) as weekday, "+
  441. "SUM(actual_kwh) as actual_total, SUM(theory_kwh) as theory_total").
  442. Where("acquisition_ts >= ? AND acquisition_ts <= ?", lastStartTs, lastEndTs).
  443. Group("weekday").
  444. Order("weekday ASC").
  445. Scan(&lastYearData).Error
  446. if err != nil {
  447. global.GVA_LOG.Error("获取去年同比趋势失败", zap.Error(err))
  448. }
  449. // 星期名称映射
  450. weekNameMap := map[int]string{0: "星期一", 1: "星期二", 2: "星期三", 3: "星期四", 4: "星期五", 5: "星期六", 6: "星期日"}
  451. yearOnYearTrends = make([]model.TrendData, 7)
  452. for i := 0; i < 7; i++ {
  453. yearOnYearTrends[i] = model.TrendData{Date: weekNameMap[i], Actual: 0, Theory: 0}
  454. }
  455. for _, d := range lastYearData {
  456. if d.Weekday >= 0 && d.Weekday <= 6 {
  457. yearOnYearTrends[d.Weekday] = model.TrendData{
  458. Date: weekNameMap[d.Weekday],
  459. Actual: d.Actual,
  460. Theory: d.Theory,
  461. }
  462. }
  463. }
  464. }
  465. // ================= 组装返回结构 =================
  466. return model.DashboardResponse{
  467. Gateways: gateways,
  468. AvailableWeeks: availableWeeks,
  469. CurrentGatewayID: req.GatewayID,
  470. TotalLuminaires: int(totalLuminaires),
  471. ElectricityConsumption: actualEnergy,
  472. CarbonReduction: carbonReduction,
  473. CarbonReductionRatio: carbonReductionRatio,
  474. WarningCount: int(warningCount),
  475. LoopDeviceCount: loopDeviceCount,
  476. Devices: devices,
  477. GatewayCount: len(gateways),
  478. Trends: trends,
  479. YearOnYearTrends: yearOnYearTrends,
  480. }
  481. }
  482. // ================= 内部辅助函数 (ISO周严格计算,修正了偏移问题) =================
  483. // getWeekRange 根据年、周数计算该周周一 00:00:00 和周日 23:59:59
  484. func getWeekRange(year, week int) (startTs, endTs int64) {
  485. // ISO 8601标准:第一周包含第一个星期四
  486. jan1 := time.Date(year, time.January, 1, 0, 0, 0, 0, time.Local)
  487. offsetToThursday := int(time.Thursday - jan1.Weekday())
  488. if offsetToThursday < 0 {
  489. offsetToThursday += 7
  490. }
  491. firstThursday := jan1.AddDate(0, 0, offsetToThursday)
  492. // 第一个星期一 = 第一个星期四 - 3天
  493. firstMonday := firstThursday.AddDate(0, 0, -3)
  494. // 目标周的周一
  495. targetMonday := firstMonday.AddDate(0, 0, (week-1)*7)
  496. start := time.Date(targetMonday.Year(), targetMonday.Month(), targetMonday.Day(), 0, 0, 0, 0, time.Local)
  497. end := start.AddDate(0, 0, 7).Add(-time.Second)
  498. return start.Unix(), end.Unix()
  499. }
  500. // getPrevYearWeek 获取去年的同一周
  501. func getPrevYearWeek(curYear, curWeek int) (prevYear, prevWeek int) {
  502. // 基于相同算法稳健减去365天
  503. jan1 := time.Date(curYear, time.January, 1, 0, 0, 0, 0, time.Local)
  504. offsetToThursday := int(time.Thursday - jan1.Weekday())
  505. if offsetToThursday < 0 {
  506. offsetToThursday += 7
  507. }
  508. firstThursday := jan1.AddDate(0, 0, offsetToThursday)
  509. firstMonday := firstThursday.AddDate(0, 0, -3)
  510. targetMonday := firstMonday.AddDate(0, 0, (curWeek-1)*7)
  511. lastYearMonday := targetMonday.AddDate(0, 0, -364)
  512. return lastYearMonday.ISOWeek()
  513. }
  514. // 结构体本地引用 (若全局有定义可移除)
  515. type dailyTrendDB struct {
  516. DateStr string `gorm:"column:date_str"`
  517. Actual float64 `gorm:"column:actual_total"`
  518. Theory float64 `gorm:"column:theory_total"`
  519. }
  520. type weeklyTrendDB struct {
  521. Weekday int `gorm:"column:weekday"`
  522. Actual float64 `gorm:"column:actual_total"`
  523. Theory float64 `gorm:"column:theory_total"`
  524. }
  525. // 定时删除 2 年前的记录
  526. func CleanupOldDeviceConsumption() {
  527. // 计算 2 年的时间戳
  528. twoYearsAgo := time.Now().AddDate(-2, 0, 0).Unix()
  529. // 循环分批删除,直到没有数据可删
  530. for {
  531. result := global.GVA_DB.Where("acquisition_ts < ?", twoYearsAgo).
  532. Limit(5000).
  533. Delete(&dao.DeviceConsumption{})
  534. if result.Error != nil {
  535. global.GVA_LOG.Error("清理历史明细失败", zap.Error(result.Error))
  536. break // 遇到错误终止循环
  537. }
  538. // 如果这一批删了 0 条,说明历史数据已经全部清完,退出循环
  539. if result.RowsAffected == 0 {
  540. global.GVA_LOG.Info("历史数据清理完毕")
  541. break
  542. }
  543. // 稍微停顿 200 毫秒,给主库一点喘息的时间,避免 DELETE 压力过大导致主从同步延迟
  544. time.Sleep(200 * time.Millisecond)
  545. }
  546. }
  547. func (bs *BluetoothService) QueryBluetoothList(info request.SearchBluetoothList) ([]dao.Bluetooth, int64, error) {
  548. limit := info.PageSize
  549. offset := info.PageSize * (info.Page - 1)
  550. return dao.QueryBluetoothList(info.GatewayId, limit, offset)
  551. }