feat: 完成产线与仓储系统多模块迭代升级
本次迭代覆盖MES与WMS核心业务: 1. 新增接驳台托盘传感器读取与AGV对接能力 2. 完善工单排产、备料流程与权限体系拆分 3. 优化看板接口与前端路由、样式 4. 新增操作日志、库存盘点与角色保护逻辑 5. 修复代理地址、BOM保存等已知问题
This commit is contained in:
@@ -28,15 +28,17 @@ var pathPermMap = map[string]string{
|
||||
"POST:/api/v1/process-flows/upload": "produce.processflow:upload",
|
||||
"POST:/api/v1/process-steps": "produce.processflow:edit",
|
||||
"POST:/api/v1/stations": "produce.station:edit",
|
||||
"POST:/api/v1/users": "sys.rbac:user",
|
||||
"PUT:/api/v1/users": "sys.rbac:user",
|
||||
"DELETE:/api/v1/users/*": "sys.rbac:user",
|
||||
"POST:/api/v1/roles": "sys.rbac:role",
|
||||
"PUT:/api/v1/roles": "sys.rbac:role",
|
||||
"DELETE:/api/v1/roles/*": "sys.rbac:role",
|
||||
"POST:/api/v1/permissions": "sys.rbac:perm",
|
||||
"PUT:/api/v1/permissions": "sys.rbac:perm",
|
||||
"DELETE:/api/v1/permissions/*": "sys.rbac:perm",
|
||||
// 账号管理(拆分自原 sys.rbac:user;sys.account 菜单下增/改/删)
|
||||
"POST:/api/v1/users": "sys.account:add",
|
||||
"PUT:/api/v1/users": "sys.account:edit",
|
||||
"DELETE:/api/v1/users/*": "sys.account:delete",
|
||||
// 角色管理(拆分自原 sys.rbac:role/perm;sys.role 菜单下增/改/删)
|
||||
"POST:/api/v1/roles": "sys.role:add",
|
||||
"PUT:/api/v1/roles": "sys.role:edit",
|
||||
"DELETE:/api/v1/roles/*": "sys.role:delete",
|
||||
"POST:/api/v1/permissions": "sys.role:perm",
|
||||
"PUT:/api/v1/permissions": "sys.role:perm",
|
||||
"DELETE:/api/v1/permissions/*": "sys.role:perm",
|
||||
}
|
||||
|
||||
// userHasPerm 校验当前登录用户是否拥有某权限码(SUPER_ADMIN 或 * 放行)。
|
||||
|
||||
@@ -2,19 +2,29 @@ package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/common/httpx"
|
||||
"bj_power_mes/internal/logic"
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/associationtrace"
|
||||
"bj_power_mes/ent/processstep"
|
||||
"bj_power_mes/ent/station"
|
||||
"bj_power_mes/ent/stepdata"
|
||||
"bj_power_mes/ent/torquerecord"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/ent/workpiece"
|
||||
"bj_power_mes/ent/workpieceprocess"
|
||||
"bj_power_mes/internal/svc"
|
||||
)
|
||||
|
||||
// ProcessCardHandler GET /api/v1/process-card?sn=xxx
|
||||
// 按 SN 生成可打印流程卡(无公司名、含签名栏、可调样式),返回 text/html。
|
||||
// 按 SN 生成可打印流程卡。数据经工单联查(产品名称/数量),工序时间线补全操作人与完成时间,
|
||||
// 拧紧数据(如有拧紧步骤)渲染到卡尾。
|
||||
// 带 ?check=1 时返回 JSON {missing:[缺失项…]} 供前端"打印前预检"弹窗,不渲染 HTML。
|
||||
func ProcessCardHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
sn := strings.TrimSpace(r.URL.Query().Get("sn"))
|
||||
@@ -22,12 +32,16 @@ func ProcessCardHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
httpx.BadRequest(w, "缺少 sn 参数")
|
||||
return
|
||||
}
|
||||
data, err := logic.New(svcCtx).Trace(r.Context(), sn)
|
||||
card, missing, err := buildCard(r.Context(), svcCtx, sn)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2401, err.Error())
|
||||
return
|
||||
}
|
||||
html, err := renderProcessCard(sn, data)
|
||||
if r.URL.Query().Get("check") == "1" {
|
||||
httpx.Ok(w, map[string]any{"sn": sn, "missing": missing})
|
||||
return
|
||||
}
|
||||
html, err := renderProcessCard(card)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2402, "流程卡生成失败:"+err.Error())
|
||||
return
|
||||
@@ -37,27 +51,16 @@ func ProcessCardHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
type cardTmpl struct {
|
||||
Sn string
|
||||
OrderNo string
|
||||
Product string
|
||||
CreatedAt string
|
||||
Processes []cardProcess
|
||||
Materials []string
|
||||
Operator string
|
||||
Inspector string
|
||||
DoneTime string
|
||||
Result string
|
||||
}
|
||||
|
||||
type cardProcess struct {
|
||||
Code string
|
||||
Name string
|
||||
Station string
|
||||
Operator string
|
||||
Status string
|
||||
Result string
|
||||
Steps []cardStep
|
||||
ID int
|
||||
Code string
|
||||
Name string
|
||||
Station string
|
||||
Operator string
|
||||
Status string
|
||||
Result string
|
||||
DoneTime string
|
||||
Steps []cardStep
|
||||
}
|
||||
|
||||
type cardStep struct {
|
||||
@@ -66,70 +69,184 @@ type cardStep struct {
|
||||
OK bool
|
||||
}
|
||||
|
||||
func renderProcessCard(sn string, data map[string]any) (string, error) {
|
||||
t := cardTmpl{Sn: sn}
|
||||
wp, _ := data["workpiece"].(map[string]any)
|
||||
if wp != nil {
|
||||
t.OrderNo = strAny(wp["orderNo"])
|
||||
t.Product = strAny(wp["productType"])
|
||||
if c, ok := wp["createdAt"]; ok {
|
||||
if ts, ok := c.(time.Time); ok {
|
||||
t.CreatedAt = ts.Format("2006-01-02 15:04")
|
||||
} else {
|
||||
t.CreatedAt = fmt.Sprintf("%v", c)
|
||||
}
|
||||
}
|
||||
type cardTorque struct {
|
||||
ScrewNo string
|
||||
Torque string
|
||||
Angle string
|
||||
Result string
|
||||
Operator string
|
||||
Time string
|
||||
}
|
||||
|
||||
type cardTmpl struct {
|
||||
Sn string
|
||||
OrderNo string
|
||||
Product string
|
||||
Quantity int
|
||||
CreatedAt string
|
||||
Processes []cardProcess
|
||||
Materials []string
|
||||
Operator string
|
||||
Inspector string
|
||||
DoneTime string
|
||||
Result string
|
||||
NeedTorque bool
|
||||
TorqueRows []cardTorque
|
||||
}
|
||||
|
||||
// buildCard 汇总流程卡所需全部数据(ent 实体直查),并计算缺失项。
|
||||
// missing 非空时表示存在数据不完整,前端应提示"是否继续打印"。
|
||||
func buildCard(ctx context.Context, svcCtx *svc.ServiceContext, sn string) (*cardTmpl, []string, error) {
|
||||
client := svcCtx.EntClient
|
||||
wp, err := client.Workpiece.Query().Where(workpiece.Sn(sn)).First(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("该SN无追溯数据")
|
||||
}
|
||||
procList, _ := data["processTimeline"].([]any)
|
||||
for _, p := range procList {
|
||||
pm, _ := p.(map[string]any)
|
||||
t := &cardTmpl{Sn: sn, Result: "待完工", NeedTorque: false}
|
||||
missing := []string{}
|
||||
|
||||
if wp.OrderNo == "" {
|
||||
missing = append(missing, "工单号")
|
||||
} else {
|
||||
t.OrderNo = wp.OrderNo
|
||||
}
|
||||
t.CreatedAt = wp.CreatedAt.Format("2006-01-02 15:04")
|
||||
|
||||
// 产品信息:工件 → 工单 联查(修复 wp.productType 恒空 bug)
|
||||
var wo *ent.WorkOrder
|
||||
if t.OrderNo != "" {
|
||||
wo, _ = client.WorkOrder.Query().Where(workorder.WorkOrderNo(t.OrderNo)).First(ctx)
|
||||
}
|
||||
if wo != nil {
|
||||
t.Product = wo.ProductName
|
||||
if wo.ProductName == "" {
|
||||
t.Product = wo.ProductCode
|
||||
}
|
||||
t.Quantity = wo.Quantity
|
||||
}
|
||||
if t.Product == "" {
|
||||
missing = append(missing, "产品名称")
|
||||
}
|
||||
if wo == nil || t.Quantity <= 0 {
|
||||
missing = append(missing, "产品数量")
|
||||
}
|
||||
|
||||
// 工序时间线(操作人 + 完成时间)
|
||||
procs, _ := client.WorkpieceProcess.Query().
|
||||
Where(workpieceprocess.Sn(sn)).Order(workpieceprocess.ByProcessCode()).All(ctx)
|
||||
for _, p := range procs {
|
||||
doneAt := ""
|
||||
if p.EndedAt != nil {
|
||||
doneAt = p.EndedAt.Format("2006-01-02 15:04")
|
||||
}
|
||||
if p.Operator == "" {
|
||||
missing = append(missing, fmt.Sprintf("工序%s报工记录(缺操作人)", itoaCard(p.ProcessCode)))
|
||||
}
|
||||
if p.EndedAt == nil {
|
||||
missing = append(missing, fmt.Sprintf("工序%s报工记录(缺完成时间)", itoaCard(p.ProcessCode)))
|
||||
}
|
||||
cp := cardProcess{
|
||||
Code: strAny(pm["processCode"]),
|
||||
Name: strAny(pm["processName"]),
|
||||
Station: strAny(pm["stationNo"]),
|
||||
Operator: strAny(pm["operator"]),
|
||||
Status: strAny(pm["status"]),
|
||||
Result: strAny(pm["result"]),
|
||||
ID: p.ID,
|
||||
Code: fmt.Sprintf("%d", p.ProcessCode),
|
||||
Name: p.ProcessName,
|
||||
Station: p.StationNo,
|
||||
Operator: p.Operator,
|
||||
Status: p.Status,
|
||||
Result: p.Result,
|
||||
DoneTime: doneAt,
|
||||
}
|
||||
if p.Result == "NG" {
|
||||
t.Result = "NG"
|
||||
}
|
||||
t.Processes = append(t.Processes, cp)
|
||||
}
|
||||
steps, _ := data["stepData"].([]any)
|
||||
stepByProc := map[string][]cardStep{}
|
||||
if len(procs) == 0 {
|
||||
missing = append(missing, "工序报工记录")
|
||||
}
|
||||
|
||||
// 步骤考核数据按工序实绩 ID 归组,回填到对应工序行的"采集参数"
|
||||
steps, _ := client.StepData.Query().Where(stepdata.Sn(sn)).All(ctx)
|
||||
stepByProc := map[int][]cardStep{}
|
||||
for _, s := range steps {
|
||||
sm, _ := s.(map[string]any)
|
||||
key := fmt.Sprintf("%v", sm["processCode"])
|
||||
ok, _ := sm["isOK"].(bool)
|
||||
stepByProc[key] = append(stepByProc[key], cardStep{
|
||||
Name: strAny(sm["stepName"]),
|
||||
Value: strAny(sm["valueText"]),
|
||||
OK: ok,
|
||||
})
|
||||
stepByProc[s.ProcessId] = append(stepByProc[s.ProcessId], cardStep{Name: s.StepName, Value: s.ValueText, OK: s.IsOK})
|
||||
}
|
||||
for i := range t.Processes {
|
||||
t.Processes[i].Steps = stepByProc[t.Processes[i].Code]
|
||||
t.Processes[i].Steps = stepByProc[t.Processes[i].ID]
|
||||
}
|
||||
assoc, _ := data["associationTrace"].(map[string]any)
|
||||
if assoc != nil {
|
||||
if b, ok := assoc["batchItems"].([]any); ok {
|
||||
for _, x := range b {
|
||||
t.Materials = append(t.Materials, fmt.Sprintf("%v", x))
|
||||
}
|
||||
}
|
||||
if s, ok := assoc["serialItems"].([]any); ok {
|
||||
for _, x := range s {
|
||||
t.Materials = append(t.Materials, fmt.Sprintf("%v", x))
|
||||
}
|
||||
}
|
||||
t.Operator = strAny(assoc["operator"])
|
||||
}
|
||||
done := "待完工"
|
||||
for _, p := range t.Processes {
|
||||
if p.Result == "NG" {
|
||||
done = "NG"
|
||||
}
|
||||
}
|
||||
t.Result = done
|
||||
|
||||
// 拧紧数据:工件所属流程含 isTorque 步骤时判定"需要拧紧数据"
|
||||
needTorque := hasTorqueStep(ctx, client, sn, wp)
|
||||
t.NeedTorque = needTorque
|
||||
torque, _ := client.TorqueRecord.Query().
|
||||
Where(torquerecord.Sn(sn)).Order(ent.Desc(torquerecord.FieldTime)).Limit(200).All(ctx)
|
||||
for _, q := range torque {
|
||||
t.TorqueRows = append(t.TorqueRows, cardTorque{
|
||||
ScrewNo: q.ScrewNo, Torque: fmt.Sprintf("%.2f", q.Torque),
|
||||
Angle: fmt.Sprintf("%.2f", q.Angle), Result: q.Result,
|
||||
Operator: q.Operator, Time: q.Time.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
if needTorque && len(t.TorqueRows) == 0 {
|
||||
missing = append(missing, "拧紧数据")
|
||||
}
|
||||
|
||||
// 关联物料(批次/精密件SN)
|
||||
assoc, _ := client.AssociationTrace.Query().
|
||||
Where(associationtrace.FinishedSn(sn)).First(ctx)
|
||||
if assoc != nil {
|
||||
t.Operator = assoc.Operator
|
||||
for _, b := range assoc.BatchItems {
|
||||
t.Materials = append(t.Materials, b)
|
||||
}
|
||||
for _, s := range assoc.SerialItems {
|
||||
t.Materials = append(t.Materials, s)
|
||||
}
|
||||
}
|
||||
return t, missing, nil
|
||||
}
|
||||
|
||||
// hasTorqueStep 判断该工件加工路径上是否存在"拧紧采集"步骤(经 工位→流程→步骤 链)
|
||||
func hasTorqueStep(ctx context.Context, client *ent.Client, sn string, wp *ent.Workpiece) bool {
|
||||
procCodes := parseCardSeq(wp.ProcessSeq)
|
||||
if len(procCodes) == 0 {
|
||||
return false
|
||||
}
|
||||
stations, err := client.Station.Query().
|
||||
Where(station.StationNoIn(procCodes...), station.FlowIdGT(0)).All(ctx)
|
||||
if err != nil || len(stations) == 0 {
|
||||
return false
|
||||
}
|
||||
flowIds := make([]int, 0, len(stations))
|
||||
for _, st := range stations {
|
||||
flowIds = append(flowIds, st.FlowId)
|
||||
}
|
||||
n, err := client.ProcessStep.Query().
|
||||
Where(processstep.FlowIdIn(flowIds...), processstep.IsTorque(true)).Count(ctx)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return n > 0
|
||||
}
|
||||
|
||||
// parseCardSeq 解析工单/工件工序组合 "1,3,5" → 工位号列表
|
||||
func parseCardSeq(s string) []int {
|
||||
out := []int{}
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(p, "%d", &n); err == nil && n >= 1 && n <= 12 {
|
||||
out = append(out, n)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func itoaCard(n int) string { return fmt.Sprintf("%d", n) }
|
||||
|
||||
func renderProcessCard(t *cardTmpl) (string, error) {
|
||||
const tpl = `<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
@@ -150,6 +267,7 @@ func renderProcessCard(sn string, data map[string]any) (string, error) {
|
||||
.sub td { text-align:left; color:#374151; background:#fafafa; }
|
||||
.sign { display:flex; justify-content:space-between; margin-top:22px; font-size:14px; }
|
||||
.sign div { width:45%; border-top:1px solid #333; padding-top:6px; text-align:center; }
|
||||
h2 { font-size:15px; margin:14px 0 6px; border-left:4px solid #1f2937; padding-left:8px; }
|
||||
@media print { body { padding:0; } .card { border-color:#000; box-shadow:none; } .no-print { display:none; } }
|
||||
</style>
|
||||
</head>
|
||||
@@ -160,21 +278,44 @@ func renderProcessCard(sn string, data map[string]any) (string, error) {
|
||||
<span>SN:<b>{{.Sn}}</b></span>
|
||||
<span>工单号:<b>{{.OrderNo}}</b></span>
|
||||
<span>产品型号:<b>{{.Product}}</b></span>
|
||||
<span>数量:<b>{{.Quantity}}</b></span>
|
||||
<span>登记时间:<b>{{.CreatedAt}}</b></span>
|
||||
<span>判定:<b>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}{{.Result}}{{end}}</b></span>
|
||||
</div>
|
||||
{{if .Processes}}
|
||||
<h2>工序流转记录</h2>
|
||||
<table>
|
||||
<thead><tr><th>工序</th><th>工序名称</th><th>工位</th><th>操作人</th><th>采集参数</th><th>结果</th></tr></thead>
|
||||
<thead><tr><th>工序</th><th>工序名称</th><th>工位</th><th>操作人</th><th>完成时间</th><th>采集参数</th><th>结果</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Processes}}
|
||||
<tr>
|
||||
<td>工序{{.Code}}</td><td>{{.Name}}</td><td>{{.Station}}</td><td>{{.Operator}}</td>
|
||||
<td>工序{{.Code}}</td><td>{{.Name}}</td><td>{{.Station}}</td><td>{{.Operator}}</td><td>{{.DoneTime}}</td>
|
||||
<td>{{if .Steps}}{{range .Steps}}{{.Name}}={{.Value}}({{if .OK}}<span class="ok">OK</span>{{else}}<span class="ng">NG</span>{{end}}) {{end}}{{else}}-{{end}}</td>
|
||||
<td>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}OK{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<p style="color:#b45309;">暂无工序报工记录。</p>
|
||||
{{end}}
|
||||
{{if .NeedTorque}}
|
||||
<h2>拧紧数据{{if not .TorqueRows}}(无数据){{end}}</h2>
|
||||
{{if .TorqueRows}}
|
||||
<table>
|
||||
<thead><tr><th>螺丝编号</th><th>扭矩(N·m)</th><th>角度(°)</th><th>结果</th><th>操作人</th><th>时间</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .TorqueRows}}
|
||||
<tr>
|
||||
<td>{{.ScrewNo}}</td><td>{{.Torque}}</td><td>{{.Angle}}</td>
|
||||
<td>{{if eq .Result "NG"}}<span class="ng">NG</span>{{else}}<span class="ok">OK</span>{{end}}</td>
|
||||
<td>{{.Operator}}</td><td>{{.Time}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .Materials}}
|
||||
<p style="margin-top:10px;font-size:13px;">关联物料/批次:{{range .Materials}}{{.}} {{end}}</p>
|
||||
{{end}}
|
||||
@@ -199,10 +340,3 @@ func renderProcessCard(sn string, data map[string]any) (string, error) {
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func strAny(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func ListBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
// ---------- 备料单 ----------
|
||||
|
||||
// GenerateMaterialHandler 按日排产生成备料单(生成前校验 BOM 物料在 WMS 档案存在)
|
||||
func GenerateMaterialHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
@@ -54,12 +55,17 @@ func GenerateMaterialHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
httpx.BadRequest(w, "请求体解析失败")
|
||||
return
|
||||
}
|
||||
n, err := logic.New(svcCtx).GenerateMaterialRequest(r.Context(), req.PlanDate, operator(r, ""))
|
||||
count, missing, err := logic.New(svcCtx).GenerateMaterialRequest(r.Context(), req.PlanDate, operator(r, ""))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3103, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "生成完成", map[string]int{"count": n})
|
||||
if len(missing) > 0 {
|
||||
// 有物料在 WMS 档案不存在:不生成,把缺失列表返回给前端弹窗
|
||||
httpx.Ok(w, map[string]any{"count": 0, "missing": missing, "blocked": true})
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "生成完成", map[string]any{"count": count, "blocked": false})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,4 +79,42 @@ func ListMaterialRequestsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// ListMaterialRequestsInternalHandler 内部接口:供 WMS 拉取备料单(含 target_dock),X-API-TOKEN 保护。
|
||||
// WMS AGV 配送页据此展示待发料行,仓管确认 target_dock 后调用 WMS AGV 下发。
|
||||
func ListMaterialRequestsInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data, err := logic.New(svcCtx).ListMaterialRequests(r.Context(), q.Get("orderNo"), q.Get("planDate"), q.Get("status"))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3105, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// MarkMaterialRequestStatusInternalHandler 内部接口:WMS 下发 AGV 后回写备料单状态(DELIVERING/DONE)。
|
||||
// 供 WMS AGV 任务推进状态时联动 MES 备料单,保持一致。
|
||||
func MarkMaterialRequestStatusInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
RequestNo string `json:"requestNo"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
if err := httpx.ParseJSON(r, &req); err != nil || req.RequestNo == "" {
|
||||
httpx.BadRequest(w, "请求体缺失")
|
||||
return
|
||||
}
|
||||
if req.Status != "DELIVERING" && req.Status != "DONE" {
|
||||
httpx.Fail(w, 3106, "非法状态")
|
||||
return
|
||||
}
|
||||
if err := logic.New(svcCtx).SetMaterialRequestStatus(r.Context(), req.RequestNo, req.Status); err != nil {
|
||||
httpx.Fail(w, 3106, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package production
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bj_power_mes/common/httpx"
|
||||
"bj_power_mes/internal/svc"
|
||||
)
|
||||
|
||||
// DockPalletInternalHandler 内部接口:读某接驳台是否有托盘(PLC 托盘传感器)。
|
||||
// WMS AGV 到达目的地前调用,确认接驳台空闲。
|
||||
func DockPalletInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
dockCode := r.URL.Query().Get("dockCode")
|
||||
if dockCode == "" {
|
||||
httpx.BadRequest(w, "缺少 dockCode")
|
||||
return
|
||||
}
|
||||
has, err := svcCtx.PLC.Get().QueryDockPallet(r.Context(), dockCode)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3201, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, map[string]any{"dockCode": dockCode, "hasPallet": has})
|
||||
}
|
||||
}
|
||||
|
||||
// SetDockPalletInternalHandler 内部接口:mock 联调用,手动置位某接驳台是否有托盘。
|
||||
// 仅在 mock 模式下生效(真实 PLC 模式忽略,读的是真实传感器)。
|
||||
func SetDockPalletInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
DockCode string `json:"dockCode"`
|
||||
HasPallet bool `json:"hasPallet"`
|
||||
}
|
||||
if err := httpx.ParseJSON(r, &req); err != nil || req.DockCode == "" {
|
||||
httpx.BadRequest(w, "请求体缺失")
|
||||
return
|
||||
}
|
||||
svcCtx.PLC.SetMockDockPallet(req.DockCode, req.HasPallet)
|
||||
if !svcCtx.PLC.IsMock() {
|
||||
httpx.OkMessage(w, "真实 PLC 模式忽略手动置位", nil)
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, nil)
|
||||
}
|
||||
}
|
||||
@@ -105,9 +105,12 @@ func OrderQueryInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
// ---------- 看板内部接口 ----------
|
||||
|
||||
func DashboardOverviewInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
// DashboardSnapshotInternalHandler 看板全量快照。
|
||||
// 结构与本项目看板前端 src/types.ts 严格对齐(见 internal/logic/dashboard_front.go),
|
||||
// 由 DashboardSnapshotInternalHandler 一次性返回,前端不再分接口拼装。
|
||||
func DashboardSnapshotInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).DashboardOverview(r.Context())
|
||||
data, err := logic.New(svcCtx).DashboardFrontSnapshot(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2005, err.Error())
|
||||
return
|
||||
@@ -115,47 +118,3 @@ func DashboardOverviewInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFu
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
func DashboardEquipmentInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).DashboardEquipment(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2006, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
func DashboardProgressInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).DashboardProgress(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2007, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
func DashboardAlarmsInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).DashboardAlarms(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2008, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
func DashboardTrendsInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).DashboardTrends(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2009, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,18 +127,19 @@ func GetWorkOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// SetWorkOrderStatusHandler POST /work-orders/status(工单状态流转)
|
||||
// SetWorkOrderStatusHandler POST /work-orders/status(工单状态流转,可选原因)
|
||||
func SetWorkOrderStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Id int `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := httpx.ParseJSON(r, &req); err != nil {
|
||||
httpx.BadRequest(w, "请求体解析失败")
|
||||
return
|
||||
}
|
||||
if err := logic.New(svcCtx).SetWorkOrderStatus(r.Context(), req.Id, req.Status, operator(r, "")); err != nil {
|
||||
if err := logic.New(svcCtx).SetWorkOrderStatus(r.Context(), req.Id, req.Status, req.Reason, operator(r, "")); err != nil {
|
||||
httpx.Fail(w, 3008, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -201,3 +202,16 @@ func DeleteDailyPlanHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
httpx.Ok(w, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// SuggestedDockCodesHandler 查某工单「上次使用的接驳台」,供排产默认推荐
|
||||
func SuggestedDockCodesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orderNo := r.URL.Query().Get("orderNo")
|
||||
if orderNo == "" {
|
||||
httpx.BadRequest(w, "缺少工单号")
|
||||
return
|
||||
}
|
||||
docks := logic.New(svcCtx).SuggestedDockCodes(r.Context(), orderNo)
|
||||
httpx.Ok(w, map[string]any{"dockCodes": docks})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package production
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"bj_power_mes/common/httpx"
|
||||
"bj_power_mes/internal/logic"
|
||||
@@ -114,8 +115,10 @@ func ListEventLogsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
q := r.URL.Query()
|
||||
page := atoiDefault(q.Get("page"), 1)
|
||||
pageSize := atoiDefault(q.Get("pageSize"), 20)
|
||||
fromMs, _ := strconv.ParseInt(q.Get("from"), 10, 64)
|
||||
toMs, _ := strconv.ParseInt(q.Get("to"), 10, 64)
|
||||
list, total, err := logic.New(svcCtx).ListEventLogs(r.Context(),
|
||||
q.Get("orderNo"), q.Get("operator"), q.Get("eventType"), page, pageSize)
|
||||
q.Get("orderNo"), q.Get("operator"), q.Get("eventType"), fromMs, toMs, page, pageSize)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3308, err.Error())
|
||||
return
|
||||
|
||||
@@ -31,19 +31,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
Method: http.MethodGet, Path: "/api/internal/order/query", Handler: internal(production.OrderQueryInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/overview", Handler: internal(production.DashboardOverviewInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/equipment", Handler: internal(production.DashboardEquipmentInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/progress", Handler: internal(production.DashboardProgressInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/alarms", Handler: internal(production.DashboardAlarmsInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/trends", Handler: internal(production.DashboardTrendsInternalHandler(serverCtx)),
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/snapshot", Handler: internal(production.DashboardSnapshotInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dashboard/stream", Handler: serverCtx.SSE.Handler,
|
||||
@@ -71,6 +59,18 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/files", Handler: internal(FileDownloadInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/material-requests", Handler: internal(production.ListMaterialRequestsInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodPost, Path: "/api/internal/material-request/status", Handler: internal(production.MarkMaterialRequestStatusInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodGet, Path: "/api/internal/dock/pallet", Handler: internal(production.DockPalletInternalHandler(serverCtx)),
|
||||
})
|
||||
server.AddRoute(rest.Route{
|
||||
Method: http.MethodPost, Path: "/api/internal/dock/pallet/set", Handler: internal(production.SetDockPalletInternalHandler(serverCtx)),
|
||||
})
|
||||
|
||||
// ---------- JWT 业务 API ----------
|
||||
jwtRoutes := []rest.Route{
|
||||
@@ -89,6 +89,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
{Method: http.MethodPut, Path: "/daily-plans", Handler: production.SaveDailyPlanHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/daily-plans", Handler: production.ListDailyPlansHandler(serverCtx)},
|
||||
{Method: http.MethodDelete, Path: "/daily-plans/:id", Handler: production.DeleteDailyPlanHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/daily-plans/suggest-docks", Handler: production.SuggestedDockCodesHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodPut, Path: "/bom", Handler: production.SaveBomHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/bom", Handler: production.ListBomHandler(serverCtx)},
|
||||
|
||||
Reference in New Issue
Block a user