feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段

- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
This commit is contained in:
SunYF
2026-09-17 12:49:07 +08:00
parent b4b274301d
commit 1cc93795ce
191 changed files with 24504 additions and 1250 deletions
@@ -79,12 +79,11 @@ func logoutHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// configHandler GET /api/config 返回工位配置:// 固定工位号 stationNo(由 yaml 配置在启动时加载,终端不可修改)、本工位需拧紧的螺丝颗数
// configHandler GET /api/config 返回工位配置:固定工位号 stationNo(由 yaml 配置在启动时加载,终端不可修改)。
func configHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ok(w, map[string]any{
"stationNo": ctx.Config.Station.StationNo,
"screwCount": ctx.Config.Station.ScrewCount,
"stationNo": ctx.Config.Station.StationNo,
})
}
}
+17 -5
View File
@@ -94,7 +94,7 @@ func pdfQueryHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// pdfOpenHandler GET /pdfopen/<name>(免鉴权,供 iframe 直链)
// pdfOpenHandler GET /pdfopen/<name>(免鉴权,供 iframe 直链;仅单层文件名
func pdfOpenHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
raw := strings.TrimPrefix(r.URL.EscapedPath(), "/pdfopen/")
@@ -107,14 +107,26 @@ func pdfOpenHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// forwardFile 校验单段文件名后拉取 MES 文件流并原样转发;优先命中本地预缓存
// pdfViewHandler GET /pdfview?name=<encoded>(免鉴权,注册在鉴权中间件之前的路由组)
// 以 query 传文件名,天然支持 MES 落盘的「日期子目录/文件名」(如 2026-09-16/xxx.pdf),
// 供工艺流程 PDF 与工序步骤图纸附件(Item J)统一使用。
func pdfViewHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
forwardFile(w, ctx.Mes, ctx.PdfCache, strings.TrimSpace(r.URL.Query().Get("name")))
}
}
// forwardFile 校验文件名后拉取 MES 文件流并原样转发;优先命中本地预缓存。
// 允许「单层日期子目录/文件名」(MES 上传落盘格式),拒绝路径穿越与多级/绝对路径。
func forwardFile(w http.ResponseWriter, mesc *mes.Client, cache *pdfcache.Cache, name string) {
name = strings.TrimSpace(name)
if name == "" {
fail(w, http.StatusBadRequest, "缺少文件名")
return
}
if strings.ContainsAny(name, `/\`) || name == "." || name == ".." || strings.Contains(name, "..") {
if strings.Contains(name, "..") || strings.ContainsAny(name, `\`) ||
strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
strings.Count(name, "/") > 1 || name == "." {
fail(w, http.StatusBadRequest, "非法文件名")
return
}
@@ -129,8 +141,8 @@ func forwardFile(w http.ResponseWriter, mesc *mes.Client, cache *pdfcache.Cache,
f.Close()
}
// 2) 回源 MES 下载
body, header, err := mesc.Get("/api/internal/files/"+url.PathEscape(name), nil)
// 2) 回源 MES 下载(内部文件接口按 query 参数 name 读取,支持日期子目录)
body, header, err := mesc.Get("/api/internal/files", url.Values{"name": {name}})
if err != nil {
fail(w, http.StatusBadGateway, "工艺文件获取失败:"+errString(err))
return
@@ -59,6 +59,9 @@ func processDoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
BindValue string `json:"bindValue"`
} `json:"binds"`
Operator string `json:"operator"`
// 作业时长采集(P0-3):上料时刻/报工时刻(ISO8601),原样转发 MES 算 durationSec;离线重传时同 payload 保留
StartedAt string `json:"startedAt"`
EndedAt string `json:"endedAt"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
@@ -83,6 +86,12 @@ func processDoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
if len(req.Binds) > 0 {
body["binds"] = req.Binds
}
if req.StartedAt != "" {
body["startedAt"] = req.StartedAt
}
if req.EndedAt != "" {
body["endedAt"] = req.EndedAt
}
payloadBytes, _ := json.Marshal(body)
ctx.Store.AddEventLog(req.OrderNo, req.Operator, "process_done",
@@ -16,6 +16,8 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
{Method: http.MethodGet, Path: "/api/health", Handler: healthHandler(ctx)},
{Method: http.MethodPost, Path: "/api/auth/login", Handler: loginHandler(ctx)},
{Method: http.MethodGet, Path: "/api/config", Handler: configHandler(ctx)},
// 工艺文件免鉴权直链(query 传名,支持日期子目录;供 iframe/window.open
{Method: http.MethodGet, Path: "/pdfview", Handler: pdfViewHandler(ctx)},
// 前端静态托管
{Method: http.MethodGet, Path: "/", Handler: indexHandler(ctx.WebFS)},
{Method: http.MethodGet, Path: "/assets/:file", Handler: assetHandler(ctx.WebFS)},
@@ -37,6 +39,7 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
{Method: http.MethodGet, Path: "/api/pdf", Handler: pdfQueryHandler(ctx)},
{Method: http.MethodGet, Path: "/pdfopen/:name", Handler: pdfOpenHandler(ctx)},
{Method: http.MethodGet, Path: "/api/tightening/list", Handler: tighteningListHandler(ctx)},
{Method: http.MethodPost, Path: "/api/tightening/record", Handler: tighteningRecordHandler(ctx)},
{Method: http.MethodPost, Path: "/api/report/process-done", Handler: processDoneHandler(ctx)},
{Method: http.MethodPost, Path: "/api/report/temp-store", Handler: tempStoreHandler(ctx)},
{Method: http.MethodPost, Path: "/api/station/logout", Handler: logoutHandler(ctx)},
@@ -58,6 +61,14 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
{Method: http.MethodPost, Path: "/api/qty-report", Handler: qtyReportHandler(ctx)},
{Method: http.MethodPost, Path: "/api/return-material", Handler: returnMaterialHandler(ctx)},
{Method: http.MethodPost, Path: "/api/inspection/upload", Handler: inspectionUploadHandler(ctx)},
// 在制品 / 0-13 上下线建档完工 / BOM 领料 / 追溯(代理 MES 内部 API)
{Method: http.MethodGet, Path: "/api/wip", Handler: wipHandler(ctx)},
{Method: http.MethodGet, Path: "/api/bom", Handler: bomHandler(ctx)},
{Method: http.MethodGet, Path: "/api/order/query", Handler: orderQueryHandler(ctx)},
{Method: http.MethodGet, Path: "/api/trace", Handler: traceHandler(ctx)},
{Method: http.MethodPost, Path: "/api/workpiece/online", Handler: onlineHandler(ctx)},
{Method: http.MethodPost, Path: "/api/workpiece/done", Handler: doneHandler(ctx)},
},
)
}
@@ -1,9 +1,14 @@
package handler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"bj_power_workstation/internal/store"
"bj_power_workstation/internal/svc"
)
@@ -34,4 +39,65 @@ func tighteningListHandler(ctx *svc.ServiceContext) http.HandlerFunc {
"list": list,
})
}
}
// tighteningRecordHandler POST /api/tightening/record —— 录入一条拧紧数据。
// 真实场景:工件走到「需要拧紧枪」的工艺步骤时界面聚焦拧紧输入框,人工操作拧紧枪把采集到的
// 扭矩值打入输入框回车 → 本接口按当前工件 SN + 工艺步骤落本地库,再由后台同步器上报 MES。
// 入参 {sn, workOrderNo, stepId, stepName, screwNo, torque, angle, result, operator}
// result 由前端按该步骤考核标准(扭矩 RANGE)判定后传入,非 NG 一律记 OK。
func tighteningRecordHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
fail(w, http.StatusBadRequest, "请求体读取失败")
return
}
var req struct {
Sn string `json:"sn"`
WorkOrderNo string `json:"workOrderNo"`
StepId string `json:"stepId"`
StepName string `json:"stepName"`
ScrewNo string `json:"screwNo"`
Torque float64 `json:"torque"`
Angle float64 `json:"angle"`
Result string `json:"result"`
Operator string `json:"operator"`
}
if err := json.Unmarshal(payload, &req); err != nil {
fail(w, http.StatusBadRequest, "请求体解析失败")
return
}
if strings.TrimSpace(req.Sn) == "" {
fail(w, http.StatusBadRequest, "缺少工件 SN,请先上料")
return
}
if strings.TrimSpace(req.StepId) == "" {
fail(w, http.StatusBadRequest, "缺少拧紧工艺步骤")
return
}
if req.Result != "NG" {
req.Result = "OK"
}
dockCode := fmt.Sprintf("DOCK%02d", ctx.Config.Station.StationNo)
id, err := ctx.Store.InsertTorqueResult(store.TorqueResult{
WorkOrderNo: req.WorkOrderNo,
Sn: req.Sn,
DockCode: dockCode,
ScrewNo: req.ScrewNo,
StepId: req.StepId,
StepName: req.StepName,
Torque: req.Torque,
Angle: req.Angle,
Result: req.Result,
Operator: req.Operator,
})
if err != nil {
fail(w, http.StatusInternalServerError, "拧紧数据入库失败:"+err.Error())
return
}
ctx.Store.AddEventLog(req.WorkOrderNo, req.Operator, "torque_record",
fmt.Sprintf("拧紧录入 sn=%s 步骤=%s 扭矩=%.1f 结果=%s", req.Sn, req.StepName, req.Torque, req.Result))
ok(w, map[string]any{"id": id, "result": req.Result})
}
}
@@ -0,0 +1,180 @@
package handler
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"bj_power_workstation/internal/svc"
"github.com/zeromicro/go-zero/core/logx"
)
// passJSON 原样透传 MES 的 {code,message,data} 响应体(不再二次包装)。
func passJSON(w http.ResponseWriter, body []byte) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
// wipHandler GET /api/wip?stationNo= —— 代理 MES /api/internal/workpieces/in-process。
// 在制品默认展示(5.2):停在本工位/上线位(0)/下线位(13) 的未完工工件列表。
func wipHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := url.Values{}
if sn := strings.TrimSpace(r.URL.Query().Get("stationNo")); sn != "" {
params.Set("stationNo", sn)
}
body, _, err := ctx.Mes.Get("/api/internal/workpieces/in-process", params)
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
passJSON(w, body)
}
}
// bomHandler GET /api/bom?productCode=&bomName= —— 代理 MES /api/internal/bom。
// 上线建档「领料确认」按物料清单展示应领料。
func bomHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := url.Values{}
if v := strings.TrimSpace(r.URL.Query().Get("productCode")); v != "" {
params.Set("productCode", v)
}
if v := strings.TrimSpace(r.URL.Query().Get("bomName")); v != "" {
params.Set("bomName", v)
}
body, _, err := ctx.Mes.Get("/api/internal/bom", params)
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
passJSON(w, body)
}
}
// orderQueryHandler GET /api/order/query?orderNo= —— 代理 MES /api/internal/order/query。
// 上线建档选工单后带出产品编码/名称(用于查 BOM 应领料)。
func orderQueryHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := url.Values{}
if v := strings.TrimSpace(r.URL.Query().Get("orderNo")); v != "" {
params.Set("orderNo", v)
}
body, _, err := ctx.Mes.Get("/api/internal/order/query", params)
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
passJSON(w, body)
}
}
// traceHandler GET /api/trace?sn= —— 代理 MES /api/internal/workpiece/trace。
// 下线完工面板扫 SN 后展示工件状态/工序时间线/拧紧等追溯信息。
func traceHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sn := strings.TrimSpace(r.URL.Query().Get("sn"))
if sn == "" {
fail(w, http.StatusBadRequest, "缺少 sn 参数")
return
}
body, _, err := ctx.Mes.Get("/api/internal/workpiece/trace", url.Values{"sn": {sn}})
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
passJSON(w, body)
}
}
// onlineHandler POST /api/workpiece/online —— 上线建档(StationNo=0)。
// {sn, orderNo, workOrderId, processSeq, operator}MES 不可达时写离线队列(kind=online_register)稍后重传。
func onlineHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
fail(w, http.StatusBadRequest, "请求体读取失败")
return
}
var req struct {
Sn string `json:"sn"`
OrderNo string `json:"orderNo"`
Operator string `json:"operator"`
}
_ = json.Unmarshal(payload, &req)
if strings.TrimSpace(req.Sn) == "" {
fail(w, http.StatusBadRequest, "缺少 sn")
return
}
ctx.Syncer.RunOnce(r.Context())
ctx.Store.AddEventLog(req.OrderNo, req.Operator, "online_register", "上线建档 sn="+req.Sn)
q := url.Values{}
if req.Operator != "" {
q.Set("operator", req.Operator)
}
respBody, _, err := ctx.Mes.Post("/api/internal/workpiece/online?"+q.Encode(), json.RawMessage(payload))
if err != nil {
if qerr := ctx.Store.InsertQueueItem("online_register", string(payload)); qerr != nil {
fail(w, http.StatusInternalServerError, "上报失败且离线队列写入失败:"+qerr.Error())
return
}
logx.Errorf("上线建档失败已离线排队: %v", err)
ok(w, map[string]any{"queued": true})
return
}
if msg := mesRespCode(respBody); msg != "" {
fail(w, http.StatusConflict, msg)
return
}
ok(w, map[string]any{"queued": false})
}
}
// doneHandler POST /api/workpiece/done —— 下线完工入库(StationNo=13)。
// {sn, batchItems, serialItems, operator};完工内部自动回流 WMS 成品库存;
// MES 不可达时写离线队列(kind=offline_finish)稍后重传。
func doneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
payload, err := io.ReadAll(r.Body)
if err != nil {
fail(w, http.StatusBadRequest, "请求体读取失败")
return
}
var req struct {
Sn string `json:"sn"`
Operator string `json:"operator"`
}
_ = json.Unmarshal(payload, &req)
if strings.TrimSpace(req.Sn) == "" {
fail(w, http.StatusBadRequest, "缺少 sn")
return
}
ctx.Syncer.RunOnce(r.Context())
ctx.Store.AddEventLog("", req.Operator, "offline_finish", "下线完工入库 sn="+req.Sn)
q := url.Values{}
if req.Operator != "" {
q.Set("operator", req.Operator)
}
respBody, _, err := ctx.Mes.Post("/api/internal/workpiece/done?"+q.Encode(), json.RawMessage(payload))
if err != nil {
if qerr := ctx.Store.InsertQueueItem("offline_finish", string(payload)); qerr != nil {
fail(w, http.StatusInternalServerError, "上报失败且离线队列写入失败:"+qerr.Error())
return
}
logx.Errorf("下线完工上报失败已离线排队: %v", err)
ok(w, map[string]any{"queued": true})
return
}
if msg := mesRespCode(respBody); msg != "" {
fail(w, http.StatusConflict, msg)
return
}
ok(w, map[string]any{"queued": false})
}
}