Files
bj_power/bj_power_wms/internal/handler/misc.go
T
SunYF 4a5e2d7bac feat: 完成多模块迭代更新
- 新增过程巡检按步骤上报、数量不符上报、工位退料功能
- 增加工单工程编号三级归属字段
- 新增物料安全库存与缺货预警
- 新增附件管理、退库单管理模块
- 优化生产看板展示逻辑与前端页面文案
- 清理冗余的工艺路线相关代码与备份文件
2026-09-15 16:37:39 +08:00

358 lines
11 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"
"strconv"
"strings"
"time"
"bj_power_wms/ent"
"bj_power_wms/ent/inventory"
"bj_power_wms/ent/material"
"bj_power_wms/ent/ordermaterialledger"
"bj_power_wms/internal/svc"
)
// 库存品类(与 schema/inventory.go 保持一致):1=原材料 2=半成品 3=成品
const (
invCatRaw = 1
invCatSemi = 2
invCatFinished = 3
)
// 半成品 / 台账 相关(盘点见 stocktake.go
//
// 【统一库存主表收编 2026-09-07】半成品不再写独立 semi_finisheds 表,
// 全部落 inventoriesmanage_mode=2 SN单件, category=2 半成品)。
// 三个 semi 接口签名与响应结构保持不变,MES / 前端零改造平滑切换。
// semiInboundHandler 半成品入库(携带已完成工序)
// 与 MES 闭环:MES 工位报工暂存退库时调用本接口,把半成品 SN 入 WMS 库(inventories, category=2)。
// 为保持两系统解耦,materialCode 不强制要求——优先用请求传入,都没有则留空(SN 本身即追溯锚点)。
func semiInboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Sn string `json:"sn"`
MaterialCode string `json:"materialCode"`
DoneProcessCodes []int `json:"doneProcessCodes"`
CompletedProcess string `json:"completedProcess"`
ZoneCode string `json:"zoneCode"`
OrderNo string `json:"orderNo"`
Operator string `json:"operator"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.Sn == "" {
fail(w, http.StatusBadRequest, "sn 必填")
return
}
// SN 防重:同一 SN 只允许一条在库/锁定/出库的库存行(含已出库历史,避免重复回流)
exists, err := ctx.EntClient.Inventory.Query().
Where(inventory.CategoryEQ(invCatSemi), inventory.SnCodeEQ(req.Sn)).
Exist(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
if exists {
fail(w, http.StatusConflict, "半成品已存在: "+req.Sn)
return
}
// 已完成工序:MES 传 doneProcessCodes([]int) 优先,否则用 completedProcess 字符串
completed := req.CompletedProcess
if len(req.DoneProcessCodes) > 0 {
parts := make([]string, 0, len(req.DoneProcessCodes))
for _, c := range req.DoneProcessCodes {
parts = append(parts, strconv.Itoa(c))
}
completed = strings.Join(parts, ",")
}
// 物料编码:以请求传入为准;名称从物料档案补齐(档案缺失则用编码兜底)
materialCode := req.MaterialCode
materialName := materialCode
if materialCode != "" {
if m, err := ctx.EntClient.Material.Query().Where(material.CodeEQ(materialCode)).First(ctx0()); err == nil && m != nil {
materialName = m.Name
}
}
rec, err := ctx.EntClient.Inventory.Create().
SetManageMode(2).
SetCategory(invCatSemi).
SetSnCode(req.Sn).
SetMaterialCode(materialCode).
SetMaterialName(materialName).
SetQuantity(1).
SetQualityStatus("未检").
SetNillableZoneCode(strPtr(req.ZoneCode)).
SetStatus("在库").
SetNillableInboundNo(strPtr(req.OrderNo)).
SetOrderNo(req.OrderNo).
SetCompletedProcess(completed).
SetRemark("半成品完工暂存回流").
Save(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
ctx.EventLog.Write(ctx0(), "semi.inbound", r.Header.Get("X-Username"), "semi", req.Sn,
"半成品入库 "+req.Sn, map[string]any{"materialCode": materialCode})
ok(w, semiView(rec))
}
}
// semiOutboundHandler 半成品出库(重上线):更新 inventories 状态
func semiOutboundHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Sn string `json:"sn"`
ZoneCode string `json:"zoneCode"`
Operator string `json:"operator"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.Sn == "" {
fail(w, http.StatusBadRequest, "sn 必填")
return
}
rec, err := ctx.EntClient.Inventory.Query().
Where(inventory.CategoryEQ(invCatSemi), inventory.SnCodeEQ(req.Sn)).
Only(ctx0())
if err != nil {
fail(w, http.StatusNotFound, "半成品不存在: "+req.Sn)
return
}
if rec.Status != "在库" {
fail(w, http.StatusConflict, "半成品不在库: "+rec.Status)
return
}
ctx.EntClient.Inventory.UpdateOneID(rec.ID).
SetStatus("出库").
SetUpdatedAt(time.Now().Unix()).
ExecX(ctx0())
ctx.EventLog.Write(ctx0(), "semi.outbound", r.Header.Get("X-Username"), "semi", req.Sn,
"半成品出库(重上线) "+req.Sn, nil)
ok(w, map[string]any{"sn": req.Sn, "status": "出库"})
}
}
// semiView 把统一库存行映射为半成品视图(保持原 /api/semi/query 响应结构,前端零改造)
type semiViewRow struct {
ID int `json:"id"`
Sn string `json:"sn"`
MaterialCode string `json:"materialCode"`
CompletedProcess string `json:"completedProcess"`
Quantity int `json:"quantity"`
ZoneCode string `json:"zoneCode"`
Status string `json:"status"`
OrderNo string `json:"orderNo"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}
func semiView(inv *ent.Inventory) *semiViewRow {
return &semiViewRow{
ID: inv.ID,
Sn: inv.SnCode,
MaterialCode: inv.MaterialCode,
CompletedProcess: inv.CompletedProcess,
Quantity: inv.Quantity,
ZoneCode: inv.ZoneCode,
Status: inv.Status,
OrderNo: inv.OrderNo,
CreatedAt: inv.CreatedAt,
UpdatedAt: inv.UpdatedAt,
}
}
// querySemiHandler 半成品查询(读统一库存主表 category=2)
func querySemiHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
page := atoi(r.URL.Query().Get("page"), 1)
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
materialCode := r.URL.Query().Get("materialCode")
status := r.URL.Query().Get("status")
q := ctx.EntClient.Inventory.Query().Where(inventory.CategoryEQ(invCatSemi))
if materialCode != "" {
q = q.Where(inventory.MaterialCodeEqualFold(materialCode))
}
if status != "" {
q = q.Where(inventory.StatusEQ(status))
}
total, err := q.Count(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
list, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
rows := make([]*semiViewRow, 0, len(list))
for _, inv := range list {
rows = append(rows, semiView(inv))
}
ok(w, map[string]any{
"total": total,
"list": rows,
"page": page,
"pageSize": pageSize,
})
}
}
// ledgerRow 台账行(服务端派生 可用库存 / 缺口 / 四态)
type ledgerRow struct {
*ent.OrderMaterialLedger
AvailableQty int `json:"availableQty"` // 实时可用库存
ShortQty int `json:"shortQty"` // 缺口 = 需求 - 可用(下限 0)
State string `json:"state"` // 备料四态:齐套/未齐套/预警/未到货
}
// ledgerState 备料四态判定(客户诉求 问题记录 L242-258),优先级:未到货 > 预警 > 未齐套 > 齐套。
//
// 说明:客户原始口径「预警 = 可用 < 未来5天需求量」,该需求量需 MES 日排产数据;
// 当前 WMS 侧无该数据源,暂以「已开始领料(已出库>0)但可用仍不足」作为预警判定——
// 语义等价(已在领料却仍不够,随时可能停线)。待 MES 提供日排产接口后替换即可。
func ledgerState(avail, need, out int) string {
if avail >= need {
return "齐套"
}
if avail <= 0 && out <= 0 {
return "未到货"
}
if out > 0 {
return "预警"
}
return "未齐套"
}
// materialAvailableQty 物料可用库存汇总(在库/锁定 且 质量状态非 不合格/待退)。
func materialAvailableQty(ctx *svc.ServiceContext, code string) int {
rows, err := ctx.EntClient.Inventory.Query().
Where(
inventory.MaterialCodeEQ(code),
inventory.StatusIn("在库", "锁定"),
inventory.QualityStatusNotIn("不合格", "待退"),
).All(ctx0())
if err != nil {
return 0
}
avail := 0
for _, inv := range rows {
if inv.ManageMode == 1 {
avail += inv.Quantity - inv.LockedQty
} else if inv.Status == "在库" {
avail++
}
}
return avail
}
func max0(n int) int {
if n < 0 {
return 0
}
return n
}
// queryLedgerHandler 工单物料台账查询(含备料四态)
func queryLedgerHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orderNo := r.URL.Query().Get("orderNo")
materialCode := r.URL.Query().Get("materialCode")
materialName := r.URL.Query().Get("materialName")
status := r.URL.Query().Get("status")
state := r.URL.Query().Get("state") // 四态筛选:齐套/未齐套/预警/未到货
page := atoi(r.URL.Query().Get("page"), 1)
pageSize := atoi(r.URL.Query().Get("pageSize"), 20)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
q := ctx.EntClient.OrderMaterialLedger.Query()
if orderNo != "" {
q = q.Where(ordermaterialledger.OrderNoEQ(orderNo))
}
if materialCode != "" {
q = q.Where(ordermaterialledger.MaterialCodeEqualFold(materialCode))
}
if materialName != "" {
q = q.Where(ordermaterialledger.MaterialNameContains(materialName))
}
if status != "" {
// 兼容旧口径(进行中/领料完结)
switch status {
case "进行中":
q = q.Where(ordermaterialledger.StatusEQ("进行中"))
case "领料完结":
q = q.Where(ordermaterialledger.StatusEQ("领料完结"))
}
}
all, err := q.Order(ent.Desc("created_at"), ent.Desc("id")).All(ctx0())
if err != nil {
fail(w, http.StatusInternalServerError, err.Error())
return
}
// 计算可用库存 + 四态(四态需实时库存,故在 Go 侧派生)
availCache := map[string]int{}
rows := make([]*ledgerRow, 0, len(all))
for _, lg := range all {
avail, ok := availCache[lg.MaterialCode]
if !ok {
avail = materialAvailableQty(ctx, lg.MaterialCode)
availCache[lg.MaterialCode] = avail
}
st := ledgerState(avail, lg.TotalQty, lg.OutQty)
if state != "" && st != state {
continue
}
rows = append(rows, &ledgerRow{
OrderMaterialLedger: lg,
AvailableQty: avail,
ShortQty: max0(lg.TotalQty - avail),
State: st,
})
}
total := len(rows)
start := (page - 1) * pageSize
if start > total {
start = total
}
end := start + pageSize
if end > total {
end = total
}
pageRows := rows[start:end]
ok(w, map[string]any{
"total": total,
"list": pageRows,
"page": page,
"pageSize": pageSize,
})
}
}