Files
bj_power/bj_power_wms/internal/handler/internalapi.go
T
SunYF 0ab21b1234 feat&refactor: 完成多模块功能迭代与配置优化
本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
2026-09-10 16:59:15 +08:00

209 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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=2semi_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 "系统生成"
}