feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE - WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置 - WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页 - 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面 - Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理 - 清理各项目球形磨遗留代码,新增部署手册.md
This commit is contained in:
@@ -2,13 +2,27 @@ package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/constants"
|
||||
"bj_power_mes/ent/dockslot"
|
||||
"bj_power_mes/ent/job"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/internal/svc"
|
||||
"bj_power_mes/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
/*
|
||||
看板总览(对齐 frontend-api.md 7.1 节):
|
||||
- 数据来源全部为 MES 库真实统计
|
||||
- Redis 缓存 60 秒(跨项目 1 分钟过期约定)
|
||||
- 工单/拧紧/扫码等关键事件通过 SSE event=dashboard_update 通知订阅方刷新
|
||||
*/
|
||||
const overviewCacheKey = "mes:dashboard:overview"
|
||||
|
||||
type DashboardOverviewLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
@@ -24,9 +38,116 @@ func NewDashboardOverviewLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
||||
}
|
||||
|
||||
func (l *DashboardOverviewLogic) DashboardOverview() (*types.DashboardOverviewReply, error) {
|
||||
// 先读缓存
|
||||
if l.svcCtx.RedisClient != nil {
|
||||
if val, err := l.svcCtx.RedisClient.Get(overviewCacheKey); err == nil && val != "" {
|
||||
var cached types.DashboardOverviewReply
|
||||
if json.Unmarshal([]byte(val), &cached) == nil {
|
||||
return &cached, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reply, err := l.compute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 写缓存(60s 过期)
|
||||
if l.svcCtx.RedisClient != nil {
|
||||
if b, err := json.Marshal(reply); err == nil {
|
||||
_ = l.svcCtx.RedisClient.Setex(overviewCacheKey, string(b), 60)
|
||||
}
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// compute 实时统计
|
||||
func (l *DashboardOverviewLogic) compute() (*types.DashboardOverviewReply, error) {
|
||||
ent := l.svcCtx.EntClient
|
||||
|
||||
activeOrderCount, err := ent.WorkOrder.Query().
|
||||
Where(workorder.StatusIn(
|
||||
constants.WorkOrderStatus_InProgress,
|
||||
constants.WorkOrderStatus_Pausing,
|
||||
)).
|
||||
Count(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
activeJobCount, err := ent.Job.Query().
|
||||
Where(job.StatusIn(
|
||||
constants.JobStatus_Created,
|
||||
constants.JobStatus_Processing,
|
||||
constants.JobStatus_WaitingUnload,
|
||||
constants.JobStatus_OnBuffer,
|
||||
constants.JobStatus_WaitingDecision,
|
||||
)).
|
||||
Count(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
completedToday, err := ent.Job.Query().
|
||||
Where(
|
||||
job.StatusEQ(constants.JobStatus_Completed),
|
||||
job.CompletedAtGTE(todayStart()),
|
||||
).
|
||||
Count(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scrappedToday, err := ent.Job.Query().
|
||||
Where(
|
||||
job.StatusEQ(constants.JobStatus_Scrapped),
|
||||
job.UpdatedAtGTE(todayStart()),
|
||||
).
|
||||
Count(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 暂存台/接驳台占用情况
|
||||
total, occupied, needRefill := l.bufferStats()
|
||||
|
||||
return &types.DashboardOverviewReply{
|
||||
ActiveOrderCount: activeOrderCount,
|
||||
ActiveJobCount: activeJobCount,
|
||||
CompletedToday: completedToday,
|
||||
ScrappedToday: scrappedToday,
|
||||
StationFaultCount: 0,
|
||||
Buffer: types.BufferSummary{
|
||||
Total: 8,
|
||||
Total: total,
|
||||
Occupied: occupied,
|
||||
Free: total - occupied,
|
||||
NeedRefill: needRefill,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *DashboardOverviewLogic) bufferStats() (total, occupied int, needRefill bool) {
|
||||
n, err := l.svcCtx.EntClient.DockSlot.Query().Count(l.ctx)
|
||||
if err != nil || n == 0 {
|
||||
return 8, 0, false
|
||||
}
|
||||
occ, err := l.svcCtx.EntClient.DockSlot.Query().
|
||||
Where(dockslot.Not(dockslot.StatusEQ(0))).
|
||||
Count(l.ctx)
|
||||
if err != nil {
|
||||
return n, 0, false
|
||||
}
|
||||
refill := n > 0 && occ*5 >= n*4 // 占用 ≥80% 提示补料
|
||||
return n, occ, refill
|
||||
}
|
||||
|
||||
func todayStart() time.Time {
|
||||
now := time.Now()
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
}
|
||||
|
||||
// EmitDashboardChange 业务模块在任何看板相关变更后调用,通知 SSE 订阅方刷新
|
||||
func EmitDashboardChange(svcCtx *svc.ServiceContext) {
|
||||
svcCtx.SSEHandler.Emit("dashboard_update", "{}")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/constants"
|
||||
"bj_power_mes/ent/alarm"
|
||||
"bj_power_mes/ent/equipment"
|
||||
"bj_power_mes/ent/equipmentslot"
|
||||
"bj_power_mes/ent/job"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
/*
|
||||
看板明细数据(设备/进度/报警/趋势),供 Dashboard 内部 API 使用:
|
||||
- 数据来源全部为 MES 库真实统计
|
||||
- Redis 缓存 60 秒(跨项目 1 分钟过期约定,同 overview)
|
||||
- 关键事件通过 SSE event=dashboard_update 通知订阅方刷新
|
||||
*/
|
||||
|
||||
type SnapshotLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewSnapshotLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SnapshotLogic {
|
||||
return &SnapshotLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// cached 统一的 60s Redis 缓存包装
|
||||
func (l *SnapshotLogic) cached(key string, fn func() (any, error)) (any, error) {
|
||||
if l.svcCtx.RedisClient != nil {
|
||||
if val, err := l.svcCtx.RedisClient.Get(key); err == nil && val != "" {
|
||||
var v any
|
||||
if json.Unmarshal([]byte(val), &v) == nil {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data, err := fn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if l.svcCtx.RedisClient != nil {
|
||||
if b, err := json.Marshal(data); err == nil {
|
||||
_ = l.svcCtx.RedisClient.Setex(key, string(b), 60)
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// EquipmentList 设备状态列表(含槽位占用统计)
|
||||
func (l *SnapshotLogic) EquipmentList() (any, error) {
|
||||
return l.cached("mes:dashboard:equipment", l.computeEquipment)
|
||||
}
|
||||
|
||||
func (l *SnapshotLogic) computeEquipment() (any, error) {
|
||||
ent := l.svcCtx.EntClient
|
||||
|
||||
equipments, err := ent.Equipment.Query().
|
||||
WithEquipmentType().
|
||||
Order(equipment.ByID()).
|
||||
All(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 槽位占用统计(非 EMPTY 视为占用)
|
||||
type slotStat struct{ occupied int }
|
||||
slotMap := map[int]*slotStat{}
|
||||
allSlots, err := ent.EquipmentSlot.Query().
|
||||
Where(equipmentslot.Not(
|
||||
equipmentslot.StatusEQ(constants.SlotStatus_Empty)),
|
||||
).
|
||||
All(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range allSlots {
|
||||
if s.EquipmentId == nil {
|
||||
continue
|
||||
}
|
||||
st, ok := slotMap[*s.EquipmentId]
|
||||
if !ok {
|
||||
st = &slotStat{}
|
||||
slotMap[*s.EquipmentId] = st
|
||||
}
|
||||
st.occupied++
|
||||
}
|
||||
|
||||
list := make([]map[string]any, 0, len(equipments))
|
||||
for _, e := range equipments {
|
||||
occupied := 0
|
||||
if st := slotMap[e.ID]; st != nil {
|
||||
occupied = st.occupied
|
||||
}
|
||||
typ := e.Edges.EquipmentType
|
||||
typeCode, typeName := "", ""
|
||||
if typ != nil {
|
||||
typeCode = string(typ.Code)
|
||||
typeName = typ.Name
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"id": e.ID,
|
||||
"name": e.Name,
|
||||
"type_code": typeCode,
|
||||
"type_name": typeName,
|
||||
"status": string(e.Status),
|
||||
"slot_count": e.SlotCount,
|
||||
"occupied_slots": occupied,
|
||||
"location": e.Location,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// ProgressList 生产进度(最近50条工单,含完成百分比)
|
||||
func (l *SnapshotLogic) ProgressList() (any, error) {
|
||||
return l.cached("mes:dashboard:progress", l.computeProgress)
|
||||
}
|
||||
|
||||
func (l *SnapshotLogic) computeProgress() (any, error) {
|
||||
orders, err := l.svcCtx.EntClient.WorkOrder.Query().
|
||||
WithProductType().
|
||||
Order(workorder.ByCreatedAt()).
|
||||
Limit(50).
|
||||
All(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := make([]map[string]any, 0, len(orders))
|
||||
for _, o := range orders {
|
||||
progress := 0
|
||||
if o.Quantity > 0 {
|
||||
progress = o.FinishedNum * 100 / o.Quantity
|
||||
}
|
||||
productName, category := "", ""
|
||||
if pt := o.Edges.ProductType; pt != nil {
|
||||
productName = pt.Name
|
||||
category = string(pt.Category)
|
||||
}
|
||||
list = append(list, map[string]any{
|
||||
"id": o.ID,
|
||||
"work_order_no": o.WorkOrderNo,
|
||||
"product_name": productName,
|
||||
"category": category,
|
||||
"status": string(o.Status),
|
||||
"quantity": o.Quantity,
|
||||
"finished_num": o.FinishedNum,
|
||||
"fail_num": o.FailNum,
|
||||
"progress": progress,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// AlarmList 报警列表(未处理优先 + 最近50条)
|
||||
func (l *SnapshotLogic) AlarmList() (any, error) {
|
||||
return l.cached("mes:dashboard:alarms", l.computeAlarms)
|
||||
}
|
||||
|
||||
func (l *SnapshotLogic) computeAlarms() (any, error) {
|
||||
ent := l.svcCtx.EntClient
|
||||
|
||||
alarms, err := ent.Alarm.Query().
|
||||
Order(alarm.ByCreatedAt()).
|
||||
Limit(200).
|
||||
All(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eqNames := map[int]string{}
|
||||
equipments, _ := ent.Equipment.Query().All(l.ctx)
|
||||
for _, e := range equipments {
|
||||
eqNames[e.ID] = e.Name
|
||||
}
|
||||
|
||||
type row struct {
|
||||
resolved bool
|
||||
at time.Time
|
||||
item map[string]any
|
||||
}
|
||||
rows := make([]row, 0, len(alarms))
|
||||
for _, a := range alarms {
|
||||
item := map[string]any{
|
||||
"id": a.ID,
|
||||
"alarm_code": a.AlarmCode,
|
||||
"alarm_message": a.AlarmMessage,
|
||||
"level": string(a.Level),
|
||||
"source": a.Source,
|
||||
"resolved": a.Resolved,
|
||||
"created_at": a.CreatedAt.Format(time.RFC3339),
|
||||
"equipment_id": nil,
|
||||
"equipment_name": "",
|
||||
}
|
||||
if a.EquipmentId != nil {
|
||||
item["equipment_id"] = *a.EquipmentId
|
||||
item["equipment_name"] = eqNames[*a.EquipmentId]
|
||||
}
|
||||
rows = append(rows, row{resolved: a.Resolved, at: a.CreatedAt, item: item})
|
||||
}
|
||||
sort.SliceStable(rows, func(i, j int) bool {
|
||||
if rows[i].resolved != rows[j].resolved {
|
||||
return !rows[i].resolved // 未处理在前
|
||||
}
|
||||
return rows[i].at.After(rows[j].at) // 新的在前
|
||||
})
|
||||
|
||||
list := make([]map[string]any, 0, 50)
|
||||
for i, r := range rows {
|
||||
if i >= 50 {
|
||||
break
|
||||
}
|
||||
list = append(list, r.item)
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// TrendList 近10日产量趋势(按产品分类分组:完成数/报废数,10日×分类全覆盖)
|
||||
func (l *SnapshotLogic) TrendList() (any, error) {
|
||||
return l.cached("mes:dashboard:trends", l.computeTrends)
|
||||
}
|
||||
|
||||
func (l *SnapshotLogic) computeTrends() (any, error) {
|
||||
ent := l.svcCtx.EntClient
|
||||
|
||||
start := todayStart().AddDate(0, 0, -9)
|
||||
|
||||
jobs, err := ent.Job.Query().
|
||||
Where(job.StatusIn(constants.JobStatus_Completed, constants.JobStatus_Scrapped)).
|
||||
All(l.ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 工单ID → 产品分类
|
||||
categoryOf := map[int]string{}
|
||||
workOrders, _ := ent.WorkOrder.Query().WithProductType().All(l.ctx)
|
||||
for _, o := range workOrders {
|
||||
cat := ""
|
||||
if pt := o.Edges.ProductType; pt != nil {
|
||||
cat = string(pt.Category)
|
||||
}
|
||||
categoryOf[o.ID] = cat
|
||||
}
|
||||
|
||||
// date → category → [completed, scrapped]
|
||||
buckets := map[string][2]int{}
|
||||
cats := map[string]bool{}
|
||||
dateSet := map[string]bool{}
|
||||
dates := make([]string, 0, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
d := start.AddDate(0, 0, i).Format("01-02")
|
||||
dateSet[d] = true
|
||||
dates = append(dates, d)
|
||||
}
|
||||
|
||||
for _, j := range jobs {
|
||||
ts := j.CompletedAt
|
||||
if ts.IsZero() || j.Status == constants.JobStatus_Scrapped {
|
||||
// 无完成时间的报废按更新时间近似
|
||||
if up := j.UpdatedAt; up != nil {
|
||||
ts = *up
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
d := ts.Format("01-02")
|
||||
if !dateSet[d] {
|
||||
continue
|
||||
}
|
||||
cat := categoryOf[j.WorkOrderId]
|
||||
if cat == "" {
|
||||
cat = "OTHER"
|
||||
}
|
||||
cats[cat] = true
|
||||
key := d + "|" + cat
|
||||
b := buckets[key]
|
||||
if j.Status == constants.JobStatus_Completed {
|
||||
b[0]++
|
||||
} else {
|
||||
b[1]++
|
||||
}
|
||||
buckets[key] = b
|
||||
}
|
||||
|
||||
if len(cats) == 0 {
|
||||
for _, c := range []string{"A6VM107", "A6VM160", "A6VM200"} {
|
||||
cats[c] = true
|
||||
}
|
||||
}
|
||||
catKeys := make([]string, 0, len(cats))
|
||||
for c := range cats {
|
||||
catKeys = append(catKeys, c)
|
||||
}
|
||||
sort.Strings(catKeys)
|
||||
|
||||
list := make([]map[string]any, 0, 30)
|
||||
for _, d := range dates {
|
||||
for _, c := range catKeys {
|
||||
b := buckets[d+"|"+c]
|
||||
list = append(list, map[string]any{
|
||||
"date": d,
|
||||
"category": c,
|
||||
"completed": b[0],
|
||||
"scrapped": b[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
Reference in New Issue
Block a user