fix: rebuild MES as compilable go-zero+ent backend (renamed bj_power_mes), restore 3 workstation projects from pristine original, align naming; all Go projects go build clean

This commit is contained in:
SunYF
2026-08-27 10:56:41 +08:00
parent 380715f8a9
commit 371b494c54
523 changed files with 67923 additions and 24758 deletions
@@ -0,0 +1,241 @@
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_recordPostgreSQL,与 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 计算逻辑保持一致:
// - workRUN 状态(含 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 EquipIdfloat64)。
// 例如 "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)
}