242 lines
6.9 KiB
Go
242 lines
6.9 KiB
Go
package processor
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"database/sql"
|
|||
|
|
"fmt"
|
|||
|
|
"log/slog"
|
|||
|
|
"regexp"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
momlib "bjhardman.cn/bjhardman/mom"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// EquipUploadService 每日定时把 OKUMA 设备的运行时长汇总写到 MOM 的 AUTO_EQUIP_UPLOAD 表。
|
|||
|
|
//
|
|||
|
|
// 数据源:dc_device_status_record(PostgreSQL,与 back_cover 共库)
|
|||
|
|
// 触发:每分钟一次 ticker,命中 EquipUploadHour 整点且当日未上传过则触发
|
|||
|
|
// 时长口径(与 OEE 计算保持一致):
|
|||
|
|
// - WorkTime = RUN(及其变体) 状态时长(小时)
|
|||
|
|
// - OpenTime = 非 OFFLINE 状态时长(小时)
|
|||
|
|
// - WaitTime = OpenTime - WorkTime
|
|||
|
|
// - SumCost = WorkTime
|
|||
|
|
type EquipUploadService struct {
|
|||
|
|
db *sql.DB
|
|||
|
|
mom *momlib.Mom
|
|||
|
|
hour int // 每日触发整点(0-23),上传前一日数据
|
|||
|
|
deviceTypes []string // device_type 过滤;空表示所有 OKUMA 类
|
|||
|
|
stopCh chan struct{}
|
|||
|
|
lastRunDate string // 已上传的日期(YYYY-MM-DD),用于幂等
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewEquipUploadService 构造服务。db / mom 任一为空时 Start 仍可调用但是空跑。
|
|||
|
|
func NewEquipUploadService(db *sql.DB, mom *momlib.Mom, hour int, deviceTypes []string) *EquipUploadService {
|
|||
|
|
if hour < 0 || hour > 23 {
|
|||
|
|
hour = 10
|
|||
|
|
}
|
|||
|
|
return &EquipUploadService{
|
|||
|
|
db: db,
|
|||
|
|
mom: mom,
|
|||
|
|
hour: hour,
|
|||
|
|
deviceTypes: deviceTypes,
|
|||
|
|
stopCh: make(chan struct{}),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Start 启动后台循环。每分钟检查一次,命中 hour 整点且当日未跑则上传。
|
|||
|
|
func (s *EquipUploadService) Start(ctx context.Context) {
|
|||
|
|
if s.db == nil || s.mom == nil {
|
|||
|
|
slog.Info("equip_upload: skip (db or mom not configured)")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
go func() {
|
|||
|
|
t := time.NewTicker(1 * time.Minute)
|
|||
|
|
defer t.Stop()
|
|||
|
|
slog.Info("equip_upload: started", "hour", s.hour, "deviceTypes", s.deviceTypes)
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-s.stopCh:
|
|||
|
|
return
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
return
|
|||
|
|
case now := <-t.C:
|
|||
|
|
s.tick(ctx, now)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *EquipUploadService) Stop() {
|
|||
|
|
close(s.stopCh)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *EquipUploadService) tick(ctx context.Context, now time.Time) {
|
|||
|
|
if now.Hour() != s.hour {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
today := now.Format("2006-01-02")
|
|||
|
|
if s.lastRunDate == today {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
|
|||
|
|
if err := s.uploadDay(ctx, yesterday); err != nil {
|
|||
|
|
slog.Error("equip_upload: failed", "date", yesterday, "error", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
s.lastRunDate = today
|
|||
|
|
slog.Info("equip_upload: success", "date", yesterday)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UploadDay 对外暴露的手动触发入口(可用于补传)。
|
|||
|
|
func (s *EquipUploadService) UploadDay(ctx context.Context, date string) error {
|
|||
|
|
if s.db == nil || s.mom == nil {
|
|||
|
|
return fmt.Errorf("db or mom not configured")
|
|||
|
|
}
|
|||
|
|
return s.uploadDay(ctx, date)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// uploadDay 汇总 date(YYYY-MM-DD) 的设备状态时长并调 MOM
|
|||
|
|
func (s *EquipUploadService) uploadDay(ctx context.Context, date string) error {
|
|||
|
|
rows, err := s.queryDurations(ctx, date)
|
|||
|
|
if err != nil {
|
|||
|
|
return fmt.Errorf("query durations: %w", err)
|
|||
|
|
}
|
|||
|
|
if len(rows) == 0 {
|
|||
|
|
slog.Info("equip_upload: no data", "date", date)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// device_name → (status → seconds)
|
|||
|
|
agg := make(map[string]map[string]float64)
|
|||
|
|
for _, r := range rows {
|
|||
|
|
if agg[r.DeviceName] == nil {
|
|||
|
|
agg[r.DeviceName] = make(map[string]float64)
|
|||
|
|
}
|
|||
|
|
agg[r.DeviceName][r.Status] += r.DurationSec
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for deviceName, statusMap := range agg {
|
|||
|
|
work, open := computeWorkOpen(statusMap)
|
|||
|
|
wait := open - work
|
|||
|
|
if wait < 0 {
|
|||
|
|
wait = 0
|
|||
|
|
}
|
|||
|
|
equipID := extractEquipID(deviceName)
|
|||
|
|
// 每条记录一个独立 ID,便于排查
|
|||
|
|
uid := fmt.Sprintf("%s-%s-%d", date, deviceName, time.Now().UnixNano())
|
|||
|
|
err := s.mom.InsertEquipUpload(ctx, momlib.EquipUpload{
|
|||
|
|
ID: uid,
|
|||
|
|
// ResourceCode 由 SDK 从 MomConf 自动回填
|
|||
|
|
EquipId: equipID,
|
|||
|
|
WorkDate: date,
|
|||
|
|
OpenTime: hours(open),
|
|||
|
|
WorkTime: hours(work),
|
|||
|
|
WaitTime: hours(wait),
|
|||
|
|
SumCost: hours(work),
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
slog.Error("equip_upload: insert failed",
|
|||
|
|
"device", deviceName, "date", date, "error", err)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
slog.Info("equip_upload: uploaded",
|
|||
|
|
"device", deviceName, "equipId", equipID, "date", date,
|
|||
|
|
"openH", hours(open), "workH", hours(work), "waitH", hours(wait))
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type statusRow struct {
|
|||
|
|
DeviceName string
|
|||
|
|
Status string
|
|||
|
|
DurationSec float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// queryDurations 查询指定日期 [date 00:00, date+1 00:00) 区间内每个设备每个状态的总秒数
|
|||
|
|
func (s *EquipUploadService) queryDurations(ctx context.Context, date string) ([]statusRow, error) {
|
|||
|
|
// 如果 deviceTypes 为空,只查 OKUMA 类(与 dashboard-server 保持一致)
|
|||
|
|
deviceTypes := s.deviceTypes
|
|||
|
|
if len(deviceTypes) == 0 {
|
|||
|
|
deviceTypes = []string{"osp-mc", "osp-lathe", "okuma_mc", "okuma_lathe"}
|
|||
|
|
}
|
|||
|
|
placeholders := make([]string, len(deviceTypes))
|
|||
|
|
args := []any{date, date}
|
|||
|
|
for i, dt := range deviceTypes {
|
|||
|
|
placeholders[i] = fmt.Sprintf("$%d", i+3)
|
|||
|
|
args = append(args, dt)
|
|||
|
|
}
|
|||
|
|
q := fmt.Sprintf(`
|
|||
|
|
SELECT r.device_name, r.status,
|
|||
|
|
EXTRACT(EPOCH FROM (
|
|||
|
|
LEAST(COALESCE(r.end_time, NOW()), ($1::date + INTERVAL '1 day')) -
|
|||
|
|
GREATEST(r.start_time, $2::date)
|
|||
|
|
)) AS duration_sec
|
|||
|
|
FROM dc_device_status_record r
|
|||
|
|
WHERE r.start_time < ($1::date + INTERVAL '1 day')
|
|||
|
|
AND COALESCE(r.end_time, NOW()) > $2::date
|
|||
|
|
AND EXISTS (
|
|||
|
|
SELECT 1 FROM dc_device_status d
|
|||
|
|
WHERE d.device_name = r.device_name
|
|||
|
|
AND d.device_type IN (%s)
|
|||
|
|
)
|
|||
|
|
`, strings.Join(placeholders, ","))
|
|||
|
|
|
|||
|
|
rs, err := s.db.QueryContext(ctx, q, args...)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer rs.Close()
|
|||
|
|
var out []statusRow
|
|||
|
|
for rs.Next() {
|
|||
|
|
var row statusRow
|
|||
|
|
if err := rs.Scan(&row.DeviceName, &row.Status, &row.DurationSec); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if row.DurationSec > 0 {
|
|||
|
|
out = append(out, row)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return out, rs.Err()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// computeWorkOpen 跟 dashboard-server/handlers/oee.go 中的 OEE 计算逻辑保持一致:
|
|||
|
|
// - work:RUN 状态(含 ONLINE 兜底——非 OSP 设备的连接级状态)
|
|||
|
|
// - open:非 OFFLINE 的所有时段(即设备开机时段)
|
|||
|
|
func computeWorkOpen(statusMap map[string]float64) (workSec, openSec float64) {
|
|||
|
|
for st, sec := range statusMap {
|
|||
|
|
if st == "OFFLINE" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
openSec += sec
|
|||
|
|
if st == "RUN" || strings.Contains(st, "RUN") || st == "ONLINE" {
|
|||
|
|
workSec += sec
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func hours(sec float64) float64 {
|
|||
|
|
h := sec / 3600.0
|
|||
|
|
// 保留 2 位小数避免 SDK / DM 端浮点抖动
|
|||
|
|
return float64(int(h*100+0.5)) / 100.0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var equipIDDigits = regexp.MustCompile(`(\d+)`)
|
|||
|
|
|
|||
|
|
// extractEquipID 从 device_name 提取尾部数字作为 MOM EquipId(float64)。
|
|||
|
|
// 例如 "CNC1"→1,"OKUMA-MC-2"→2;提取不到则返回 0。
|
|||
|
|
func extractEquipID(deviceName string) float64 {
|
|||
|
|
matches := equipIDDigits.FindAllString(deviceName, -1)
|
|||
|
|
if len(matches) == 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
// 取最后一段数字,更贴近"序号"语义
|
|||
|
|
v, err := strconv.Atoi(matches[len(matches)-1])
|
|||
|
|
if err != nil {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return float64(v)
|
|||
|
|
}
|