本次提交覆盖多个业务模块的功能完善与体验优化:
1. **鉴权与配置调整**:
- 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
- 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
- 工位配置放开1~12限制,改为仅校验大于0
2. **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3. **功能新增**:
- 新增工位类型、工艺路线与产线点位台账模块
- 添加工艺PDF预览面板、工位终端代理转发接口
- 新增操作日志按操作人列表筛选、工位登出日志记录
- 新增PLC移料指令与产线点位状态管理
4. **业务流程优化**:
- 调整BOM物料删除校验逻辑,优化工单备料计算
- 补充物料图号、检测单号等追溯字段
- 完善工艺流程图与工位绑定关系说明
- 优化前端页面文案与交互细节
5. **代码规范与维护**:
- 新增通用工具函数与前端静态资源
- 整理路由权限与中间件逻辑
- 修复部分接口与配置的不兼容问题
209 lines
7.5 KiB
Go
209 lines
7.5 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"time"
|
||
|
||
"bj_power_wms/ent/inboundorder"
|
||
"bj_power_wms/ent/inventory"
|
||
"bj_power_wms/ent/material"
|
||
"bj_power_wms/ent/ordermaterialledger"
|
||
"bj_power_wms/ent/outboundorder"
|
||
"bj_power_wms/internal/svc"
|
||
)
|
||
|
||
// ledgerSyncRequest MES 下发工单 BOM 后同步台账
|
||
type ledgerSyncRequest struct {
|
||
OrderNo string `json:"orderNo"`
|
||
ProductCode string `json:"productCode"`
|
||
Operator string `json:"operator"`
|
||
Items []struct {
|
||
MaterialCode string `json:"materialCode"`
|
||
MaterialName string `json:"materialName"`
|
||
TotalQty int `json:"totalQty"`
|
||
} `json:"items"`
|
||
}
|
||
|
||
// ledgerSyncHandler 同步/更新工单物料台账(幂等:按 orderNo+materialCode upsert)
|
||
func ledgerSyncHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req ledgerSyncRequest
|
||
if err := parseJSON(r, &req); err != nil {
|
||
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
|
||
return
|
||
}
|
||
if req.OrderNo == "" || len(req.Items) == 0 {
|
||
fail(w, http.StatusBadRequest, "orderNo 和 items 必填")
|
||
return
|
||
}
|
||
saved := 0
|
||
for _, it := range req.Items {
|
||
existing, err := ctx.EntClient.OrderMaterialLedger.Query().
|
||
Where(
|
||
ordermaterialledger.OrderNoEQ(req.OrderNo),
|
||
ordermaterialledger.MaterialCodeEQ(it.MaterialCode),
|
||
).Only(ctx0())
|
||
if err == nil {
|
||
ctx.EntClient.OrderMaterialLedger.UpdateOneID(existing.ID).
|
||
SetTotalQty(it.TotalQty).
|
||
SetStatus("进行中").
|
||
SetUpdatedAt(time.Now().Unix()).
|
||
ExecX(ctx0())
|
||
} else {
|
||
ctx.EntClient.OrderMaterialLedger.Create().
|
||
SetOrderNo(req.OrderNo).
|
||
SetMaterialCode(it.MaterialCode).
|
||
SetNillableMaterialName(strPtr(it.MaterialName)).
|
||
SetTotalQty(it.TotalQty).
|
||
SetOutQty(0).
|
||
SetStatus("进行中").
|
||
SaveX(ctx0())
|
||
}
|
||
saved++
|
||
}
|
||
ok(w, map[string]any{"orderNo": req.OrderNo, "synced": saved})
|
||
}
|
||
}
|
||
|
||
// internalDisplayHandler 库存总览(55寸大屏免登录用)
|
||
func internalDisplayHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
// 批次库存(结构件)总量(按质量状态)
|
||
batches, _ := ctx.EntClient.Inventory.Query().Where(inventory.ManageModeEQ(1)).All(ctx0())
|
||
totalQty := 0
|
||
lockedQty := 0
|
||
type zoneRow struct {
|
||
Zone, Quality string
|
||
Qty int
|
||
}
|
||
zones := map[string]int{}
|
||
batchCount := len(batches)
|
||
for _, b := range batches {
|
||
totalQty += b.Quantity
|
||
lockedQty += b.LockedQty
|
||
zc := b.ZoneCode
|
||
if zc == "" {
|
||
zc = "未分区"
|
||
}
|
||
zones[zc+"/"+b.QualityStatus] += b.Quantity
|
||
}
|
||
snInStock, _ := ctx.EntClient.Inventory.Query().Where(inventory.ManageModeEQ(2), inventory.StatusIn("在库", "锁定")).Count(ctx0())
|
||
snOut, _ := ctx.EntClient.Inventory.Query().Where(inventory.ManageModeEQ(2), inventory.StatusEQ("出库")).Count(ctx0())
|
||
// 半成品统计:统一库存主表 category=2(semi_finisheds 已收编废弃)
|
||
semiInStock, _ := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatSemi), inventory.StatusEQ("在库")).Count(ctx0())
|
||
// 成品在库统计:category=3
|
||
finishedInStock, _ := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatFinished), inventory.StatusIn("在库", "锁定")).Count(ctx0())
|
||
pkgCount, _ := ctx.EntClient.OutboundOrder.Query().Where(outboundorder.BoxNoNEQ("")).Count(ctx0())
|
||
|
||
dayStart := time.Now().Truncate(24 * time.Hour).Unix()
|
||
inToday, _ := ctx.EntClient.InboundOrder.Query().
|
||
Where(inboundorder.CreatedAtGTE(dayStart)).Count(ctx0())
|
||
outToday, _ := ctx.EntClient.OutboundOrder.Query().
|
||
Where(outboundorder.CreatedAtGTE(dayStart)).Count(ctx0())
|
||
|
||
ok(w, map[string]any{
|
||
"batchCount": batchCount,
|
||
"totalQty": totalQty,
|
||
"lockedQty": lockedQty,
|
||
"snInStock": snInStock,
|
||
"snOut": snOut,
|
||
"semiInStock": semiInStock,
|
||
"finishedInStock": finishedInStock,
|
||
"packageCount": pkgCount,
|
||
"inToday": inToday,
|
||
"outToday": outToday,
|
||
"zoneDetail": zones,
|
||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
}
|
||
|
||
// finishedInboundRequest 成品完工回流入库请求(MES 完工后调用,X-API-TOKEN 内部接口)
|
||
// 定稿(2026-09-07):成品 SN 由 MES 系统生成(系统必发号,实物可不贴码,允许手填覆盖),
|
||
// SN 与实物码解耦;完工品经本接口自动回流 WMS 统一库存主表(manage_mode=2 SN单件, category=3 成品)。
|
||
type finishedInboundRequest struct {
|
||
ProductCode string `json:"productCode"` // 产品编码(成品物料编码)
|
||
ProductName string `json:"productName"` // 产品名称(档案缺失时兜底展示)
|
||
Sn string `json:"sn"` // 成品 SN(系统生成或手填,全局唯一)
|
||
OrderNo string `json:"orderNo"` // 关联工单号
|
||
ZoneCode string `json:"zoneCode"` // 入库区域(可空=未分区)
|
||
QualityStatus string `json:"qualityStatus"` // 默认 合格(完工已过全工序校验)
|
||
SnSource string `json:"snSource"` // AUTO=系统生成 / MANUAL=手填,仅记录
|
||
Operator string `json:"operator"`
|
||
}
|
||
|
||
// finishedInboundHandler 成品完工回流入库
|
||
// 物料档案缺失时不阻塞回流(库存行 material_code 仍落产品编码,名称用请求传入兜底),
|
||
// 记 eventlog 提示补建档——编码主数据仍以 WMS 为单一来源,产线不被档案缺失卡死。
|
||
func finishedInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var req finishedInboundRequest
|
||
if err := parseJSON(r, &req); err != nil {
|
||
fail(w, http.StatusBadRequest, "参数错误: "+err.Error())
|
||
return
|
||
}
|
||
if req.Sn == "" || req.ProductCode == "" {
|
||
fail(w, http.StatusBadRequest, "sn 与 productCode 必填")
|
||
return
|
||
}
|
||
quality := req.QualityStatus
|
||
if quality == "" {
|
||
quality = "合格"
|
||
}
|
||
|
||
// SN 全局防重(同品类成品 + 半成品 + 电气件 SN 都不允许撞号)
|
||
dup, err := ctx.EntClient.Inventory.Query().
|
||
Where(inventory.ManageModeEQ(2), inventory.SnCodeEQ(req.Sn)).Exist(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
if dup {
|
||
fail(w, http.StatusConflict, "成品 SN 已存在库存: "+req.Sn)
|
||
return
|
||
}
|
||
|
||
// 物料档案补名称;缺失时记日志(不阻塞回流)
|
||
materialName := req.ProductName
|
||
archiveMiss := false
|
||
if m, err := ctx.EntClient.Material.Query().Where(material.CodeEQ(req.ProductCode)).First(ctx0()); err == nil && m != nil {
|
||
materialName = m.Name
|
||
} else {
|
||
archiveMiss = true
|
||
}
|
||
|
||
rec, err := ctx.EntClient.Inventory.Create().
|
||
SetManageMode(2).
|
||
SetCategory(invCatFinished).
|
||
SetMaterialCode(req.ProductCode).
|
||
SetMaterialName(materialName).
|
||
SetSnCode(req.Sn).
|
||
SetQuantity(1).
|
||
SetQualityStatus(quality).
|
||
SetNillableZoneCode(strPtr(req.ZoneCode)).
|
||
SetStatus("在库").
|
||
SetInboundNo("MES:" + req.OrderNo).
|
||
SetOrderNo(req.OrderNo).
|
||
SetRemark("成品完工回流(SN来源:" + snSourceLabel(req.SnSource) + ")").
|
||
Save(ctx0())
|
||
if err != nil {
|
||
fail(w, http.StatusInternalServerError, err.Error())
|
||
return
|
||
}
|
||
|
||
ctx.EventLog.Write(ctx0(), "finished.inbound", req.Operator, "finished", req.Sn,
|
||
"成品完工回流入库 "+req.Sn, map[string]any{
|
||
"productCode": req.ProductCode, "orderNo": req.OrderNo,
|
||
"snSource": req.SnSource, "archiveMiss": archiveMiss,
|
||
})
|
||
ok(w, rec)
|
||
}
|
||
}
|
||
|
||
func snSourceLabel(s string) string {
|
||
if s == "MANUAL" {
|
||
return "手填"
|
||
}
|
||
return "系统生成"
|
||
}
|