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:
SunYF
2026-08-27 19:50:57 +08:00
parent c85c3bd6f1
commit a2afd2f6dc
335 changed files with 87495 additions and 7357 deletions
@@ -0,0 +1,432 @@
package production
import (
"encoding/json"
"net/http"
"strconv"
errx "bj_power_mes/common/errorx"
"bj_power_mes/internal/logic/dashboard"
"bj_power_mes/internal/logic/production"
"bj_power_mes/internal/svc"
"bj_power_mes/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func invalidParams() error { return errx.New(errx.RequestParamInvalid) }
// ============ 内部解析工具(不依赖 goctl 生成代码)============
func decodeJSON(r *http.Request, v any) bool {
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
return false
}
return true
}
func ok(w http.ResponseWriter, r *http.Request, data any) {
httpx.OkJsonCtx(r.Context(), w, data)
}
func fail(w http.ResponseWriter, r *http.Request, err error) {
httpx.ErrorCtx(r.Context(), w, err)
}
func qInt(r *http.Request, name string, def int) int {
v := r.URL.Query().Get(name)
if v == "" {
return def
}
n, err := strconv.Atoi(v)
if err != nil {
return def
}
return n
}
// ============ BOM 管理 ============
func BomListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := production.NewBomLogic(r.Context(), svcCtx)
resp, err := l.BomList(&types.BomListReq{ProductCode: r.URL.Query().Get("productCode")})
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func BomSaveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BomSaveReq
if !decodeJSON(r, &req) {
fail(w, r, invalidParams())
return
}
l := production.NewBomLogic(r.Context(), svcCtx)
if err := l.BomSave(&req); err != nil {
fail(w, r, err)
return
}
ok(w, r, map[string]any{"saved": len(req.Items)})
}
}
// ============ 工序字典 ============
func ProcessDictListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := production.NewProcessDictLogic(r.Context(), svcCtx)
resp, err := l.ProcessDictList()
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func ProcessDictSaveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ProcessDictSaveReq
if !decodeJSON(r, &req) {
fail(w, r, invalidParams())
return
}
l := production.NewProcessDictLogic(r.Context(), svcCtx)
if err := l.ProcessDictSave(&req); err != nil {
fail(w, r, err)
return
}
ok(w, r, map[string]any{"saved": len(req.Items)})
}
}
// ============ 备料单 ============
func MaterialRequestCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.MaterialRequestCreateReq
if !decodeJSON(r, &req) || req.OrderNo == "" {
fail(w, r, invalidParams())
return
}
req.Operator = production.CurrentUserName(r.Context(), svcCtx)
l := production.NewMaterialRequestLogic(r.Context(), svcCtx)
resp, err := l.MaterialRequestCreate(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func MaterialRequestListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := types.MaterialRequestQueryReq{
OrderNo: r.URL.Query().Get("orderNo"),
Page: qInt(r, "page", 1),
Size: qInt(r, "size", 20),
}
l := production.NewMaterialRequestLogic(r.Context(), svcCtx)
resp, err := l.MaterialRequestList(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ PLC 工序下发 ============
func PlcSendProcessHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.PlcSendProcessReq
if !decodeJSON(r, &req) || req.CodeStr == "" {
fail(w, r, invalidParams())
return
}
l := production.NewPlcLogic(r.Context(), svcCtx)
resp, err := l.PlcSendProcess(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func PlcSendLogsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := types.PlcSendLogQueryReq{
OrderNo: r.URL.Query().Get("orderNo"),
Limit: qInt(r, "limit", 50),
}
l := production.NewPlcLogic(r.Context(), svcCtx)
resp, err := l.PlcSendLogs(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ 拧紧记录(工位终端上报)============
func TorqueReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.TorqueReportReq
if !decodeJSON(r, &req) {
fail(w, r, invalidParams())
return
}
l := production.NewTorqueLogic(r.Context(), svcCtx)
resp, err := l.TorqueReport(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func TorqueQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := types.TorqueQueryReq{
Sn: r.URL.Query().Get("sn"),
OrderNo: r.URL.Query().Get("orderNo"),
Page: qInt(r, "page", 1),
Size: qInt(r, "size", 20),
}
l := production.NewTorqueLogic(r.Context(), svcCtx)
resp, err := l.TorqueQuery(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ 扫码报工 ============
func ScanReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ScanReportReq
if !decodeJSON(r, &req) || req.Sn == "" {
fail(w, r, invalidParams())
return
}
if req.Operator == "" {
req.Operator = production.CurrentUserName(r.Context(), svcCtx)
}
l := production.NewScanLogic(r.Context(), svcCtx)
resp, err := l.ScanReport(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func ScanQueryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := types.ScanQueryReq{
OrderNo: r.URL.Query().Get("orderNo"),
Sn: r.URL.Query().Get("sn"),
Page: qInt(r, "page", 1),
Size: qInt(r, "size", 20),
}
l := production.NewScanLogic(r.Context(), svcCtx)
resp, err := l.ScanQuery(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ 半成品流转 ============
func SemiInboundHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SemiFlowReq
if !decodeJSON(r, &req) || req.Sn == "" {
fail(w, r, invalidParams())
return
}
if req.Operator == "" {
req.Operator = production.CurrentUserName(r.Context(), svcCtx)
}
l := production.NewSemiFlowLogic(r.Context(), svcCtx)
resp, err := l.SemiInbound(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func SemiOutboundHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SemiFlowReq
if !decodeJSON(r, &req) || req.Sn == "" {
fail(w, r, invalidParams())
return
}
if req.Operator == "" {
req.Operator = production.CurrentUserName(r.Context(), svcCtx)
}
l := production.NewSemiFlowLogic(r.Context(), svcCtx)
resp, err := l.SemiOutbound(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func SemiFlowsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := types.SemiFlowQueryReq{
Sn: r.URL.Query().Get("sn"),
OrderNo: r.URL.Query().Get("orderNo"),
Page: qInt(r, "page", 1),
Size: qInt(r, "size", 20),
}
l := production.NewSemiFlowLogic(r.Context(), svcCtx)
resp, err := l.SemiFlows(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ AGV 任务 ============
func AgvTaskCreateHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AgvTaskCreateReq
if !decodeJSON(r, &req) {
fail(w, r, invalidParams())
return
}
l := production.NewAgvTaskLogic(r.Context(), svcCtx)
resp, err := l.AgvTaskCreate(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func AgvTaskListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := types.AgvTaskQueryReq{
Status: r.URL.Query().Get("status"),
OrderNo: r.URL.Query().Get("orderNo"),
Page: qInt(r, "page", 1),
Size: qInt(r, "size", 20),
}
l := production.NewAgvTaskLogic(r.Context(), svcCtx)
resp, err := l.AgvTaskList(&req)
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ 追溯 ============
func TraceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
l := production.NewTraceLogic(r.Context(), svcCtx)
resp, err := l.Trace(q.Get("sn"), q.Get("orderNo"))
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ 看板(内部 APIDashboard A 免登录拉取)============
func DashboardOverviewInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := dashboard.NewDashboardOverviewLogic(r.Context(), svcCtx)
resp, err := l.DashboardOverview()
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
// ============ 看板明细内部 API(设备/进度/报警/趋势)============
func DashboardEquipmentInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := dashboard.NewSnapshotLogic(r.Context(), svcCtx)
resp, err := l.EquipmentList()
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func DashboardProgressInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := dashboard.NewSnapshotLogic(r.Context(), svcCtx)
resp, err := l.ProgressList()
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func DashboardAlarmsInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := dashboard.NewSnapshotLogic(r.Context(), svcCtx)
resp, err := l.AlarmList()
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
func DashboardTrendsInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := dashboard.NewSnapshotLogic(r.Context(), svcCtx)
resp, err := l.TrendList()
if err != nil {
fail(w, r, err)
return
}
ok(w, r, resp)
}
}
@@ -0,0 +1,88 @@
package handler
import (
"net/http"
productionHandler "bj_power_mes/internal/handler/production"
"bj_power_mes/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
/*
RegisterProductionRoutes 注册生产业务扩展路由(BOM/工序字典/备料/PLC/拧紧/报工/半成品/AGV/追溯/看板内部API)。
不复用 goctl 生成的 routes.go,独立注册以便手写路由长期维护。
- /api/v1/* JWT 保护,前端 MES 页面使用
- /api/internal/* X-API-TOKEN 写死 token,供 WMS/工位终端/Dashboard 服务间调用
*/
func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext) {
// ---------- 内部 API(写死 token ----------
internal := func(next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expect := serverCtx.Config.Internal.Token
if expect == "" || r.Header.Get("X-API-TOKEN") != expect {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"code":401,"message":"内部接口 token 错误"}`))
return
}
next(w, r)
})
}
server.AddRoute(rest.Route{
Method: http.MethodPost,
Path: "/api/internal/tightening/report",
Handler: internal(productionHandler.TorqueReportHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/api/internal/dashboard/overview",
Handler: internal(productionHandler.DashboardOverviewInternalHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/api/internal/dashboard/equipment",
Handler: internal(productionHandler.DashboardEquipmentInternalHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/api/internal/dashboard/progress",
Handler: internal(productionHandler.DashboardProgressInternalHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/api/internal/dashboard/alarms",
Handler: internal(productionHandler.DashboardAlarmsInternalHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/api/internal/dashboard/trends",
Handler: internal(productionHandler.DashboardTrendsInternalHandler(serverCtx)),
})
// ---------- JWT 业务 API ----------
jwtRoutes := []rest.Route{
{Method: http.MethodGet, Path: "/bom", Handler: productionHandler.BomListHandler(serverCtx)},
{Method: http.MethodPut, Path: "/bom", Handler: productionHandler.BomSaveHandler(serverCtx)},
{Method: http.MethodGet, Path: "/process-dict", Handler: productionHandler.ProcessDictListHandler(serverCtx)},
{Method: http.MethodPut, Path: "/process-dict", Handler: productionHandler.ProcessDictSaveHandler(serverCtx)},
{Method: http.MethodPost, Path: "/material-requests", Handler: productionHandler.MaterialRequestCreateHandler(serverCtx)},
{Method: http.MethodGet, Path: "/material-requests", Handler: productionHandler.MaterialRequestListHandler(serverCtx)},
{Method: http.MethodPost, Path: "/plc/send-process", Handler: productionHandler.PlcSendProcessHandler(serverCtx)},
{Method: http.MethodGet, Path: "/plc/send-logs", Handler: productionHandler.PlcSendLogsHandler(serverCtx)},
{Method: http.MethodPost, Path: "/scan/report", Handler: productionHandler.ScanReportHandler(serverCtx)},
{Method: http.MethodGet, Path: "/scan/records", Handler: productionHandler.ScanQueryHandler(serverCtx)},
{Method: http.MethodPost, Path: "/semi/inbound", Handler: productionHandler.SemiInboundHandler(serverCtx)},
{Method: http.MethodPost, Path: "/semi/outbound", Handler: productionHandler.SemiOutboundHandler(serverCtx)},
{Method: http.MethodGet, Path: "/semi/flows", Handler: productionHandler.SemiFlowsHandler(serverCtx)},
{Method: http.MethodPost, Path: "/agv/tasks", Handler: productionHandler.AgvTaskCreateHandler(serverCtx)},
{Method: http.MethodGet, Path: "/agv/tasks", Handler: productionHandler.AgvTaskListHandler(serverCtx)},
{Method: http.MethodGet, Path: "/trace", Handler: productionHandler.TraceHandler(serverCtx)},
}
server.AddRoutes(jwtRoutes,
rest.WithPrefix("/api/v1"),
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
)
}