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:
@@ -0,0 +1,336 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"bj_power_wms/ent/inventorybatch"
|
||||
"bj_power_wms/ent/inventorylock"
|
||||
"bj_power_wms/ent/serialnumber"
|
||||
"bj_power_wms/internal/svc"
|
||||
|
||||
"bj_power_wms/ent"
|
||||
)
|
||||
|
||||
// queryStockHandler 库存查询(MES 查料)
|
||||
// 支持按物料编码、区域、关键字、类型(batch/serial)过滤
|
||||
func queryStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
materialCode := r.URL.Query().Get("materialCode")
|
||||
zoneCode := r.URL.Query().Get("zoneCode")
|
||||
stockType := r.URL.Query().Get("type") // batch / serial
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
page := atoi(r.URL.Query().Get("page"), 1)
|
||||
pageSize := atoi(r.URL.Query().Get("pageSize"), 50)
|
||||
|
||||
result := map[string]any{
|
||||
"batch": []any{},
|
||||
"serial": []any{},
|
||||
"total": 0,
|
||||
}
|
||||
|
||||
// 批次库存
|
||||
batchQ := ctx.EntClient.InventoryBatch.Query()
|
||||
if materialCode != "" {
|
||||
batchQ = batchQ.Where(inventorybatch.MaterialCodeEQ(materialCode))
|
||||
}
|
||||
if zoneCode != "" {
|
||||
batchQ = batchQ.Where(inventorybatch.ZoneCodeEQ(zoneCode))
|
||||
}
|
||||
if keyword != "" {
|
||||
batchQ = batchQ.Where(
|
||||
inventorybatch.Or(
|
||||
inventorybatch.BatchNoContains(keyword),
|
||||
inventorybatch.MaterialNameContains(keyword),
|
||||
),
|
||||
)
|
||||
}
|
||||
batchTotal, err := batchQ.Count(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
batches, err := batchQ.Order(ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// SN 库存
|
||||
serialQ := ctx.EntClient.SerialNumber.Query()
|
||||
if materialCode != "" {
|
||||
serialQ = serialQ.Where(serialnumber.MaterialCodeEQ(materialCode))
|
||||
}
|
||||
if zoneCode != "" {
|
||||
serialQ = serialQ.Where(serialnumber.CurrentZoneEQ(zoneCode))
|
||||
}
|
||||
if keyword != "" {
|
||||
serialQ = serialQ.Where(
|
||||
serialnumber.Or(
|
||||
serialnumber.SnCodeContains(keyword),
|
||||
serialnumber.MaterialCodeContains(keyword),
|
||||
),
|
||||
)
|
||||
}
|
||||
serialTotal, err := serialQ.Count(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
serials, err := serialQ.Order(ent.Desc("id")).
|
||||
Offset((page - 1) * pageSize).Limit(pageSize).All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if stockType == "batch" {
|
||||
result["batch"] = batches
|
||||
result["total"] = batchTotal
|
||||
} else if stockType == "serial" {
|
||||
result["serial"] = serials
|
||||
result["total"] = serialTotal
|
||||
} else {
|
||||
result["batch"] = batches
|
||||
result["serial"] = serials
|
||||
result["total"] = batchTotal + serialTotal
|
||||
}
|
||||
result["page"] = page
|
||||
result["pageSize"] = pageSize
|
||||
|
||||
ok(w, result)
|
||||
}
|
||||
}
|
||||
|
||||
// lockStockHandler 工单锁定库存
|
||||
// body: { orderNo, items: [{ materialCode, batchNo?, sn?, qty }] }
|
||||
// 结构件锁批次数量;精密件锁 SN
|
||||
func lockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
Items []struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
BatchNo string `json:"batchNo"`
|
||||
Sn string `json:"sn"`
|
||||
Qty int `json:"qty"`
|
||||
} `json:"items"`
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
locked := 0
|
||||
for _, item := range req.Items {
|
||||
if item.MaterialCode == "" {
|
||||
continue
|
||||
}
|
||||
// 精密件:锁 SN
|
||||
if item.Sn != "" {
|
||||
sn, err := ctx.EntClient.SerialNumber.Query().
|
||||
Where(
|
||||
serialnumber.SnCodeEQ(item.Sn),
|
||||
serialnumber.MaterialCodeEQ(item.MaterialCode),
|
||||
serialnumber.StatusEQ("在库"),
|
||||
).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "SN 不在库或不存在: "+item.Sn)
|
||||
return
|
||||
}
|
||||
_, err = ctx.EntClient.InventoryLock.Create().
|
||||
SetTargetType("SN").
|
||||
SetTargetID(sn.SnCode).
|
||||
SetMaterialCode(item.MaterialCode).
|
||||
SetLockedQty(1).
|
||||
SetOrderNo(req.OrderNo).
|
||||
SetStatus("ACTIVE").
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "锁定 SN 失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// SN 状态 → 锁定
|
||||
ctx.EntClient.SerialNumber.UpdateOneID(sn.ID).
|
||||
SetStatus("锁定").ExecX(ctx0())
|
||||
locked++
|
||||
continue
|
||||
}
|
||||
|
||||
// 结构件:锁批次数量
|
||||
qty := item.Qty
|
||||
if qty <= 0 {
|
||||
qty = 1
|
||||
}
|
||||
batch, err := ctx.EntClient.InventoryBatch.Query().
|
||||
Where(inventorybatch.BatchNoEQ(item.BatchNo)).
|
||||
Only(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusBadRequest, "批次不存在: "+item.BatchNo)
|
||||
return
|
||||
}
|
||||
avail := batch.Quantity - batch.LockedQty
|
||||
if avail < qty {
|
||||
fail(w, http.StatusConflict, "批次库存不足: "+item.BatchNo+" 可用 "+itoa(avail))
|
||||
return
|
||||
}
|
||||
_, err = ctx.EntClient.InventoryLock.Create().
|
||||
SetTargetType("BATCH").
|
||||
SetTargetID(batch.BatchNo).
|
||||
SetMaterialCode(item.MaterialCode).
|
||||
SetLockedQty(qty).
|
||||
SetOrderNo(req.OrderNo).
|
||||
SetStatus("ACTIVE").
|
||||
Save(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, "锁定批次失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
// 累加批次锁定数
|
||||
ctx.EntClient.InventoryBatch.UpdateOneID(batch.ID).
|
||||
SetLockedQty(batch.LockedQty + qty).ExecX(ctx0())
|
||||
locked += qty
|
||||
}
|
||||
|
||||
ok(w, map[string]any{"locked": locked})
|
||||
}
|
||||
}
|
||||
|
||||
// unlockStockHandler 释放库存锁定(取消锁定/返工)
|
||||
func unlockStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
OrderNo string `json:"orderNo"`
|
||||
LockIDs []int `json:"lockIds"`
|
||||
}
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
q := ctx.EntClient.InventoryLock.Query().
|
||||
Where(inventorylock.StatusEQ("ACTIVE"))
|
||||
if req.OrderNo != "" {
|
||||
q = q.Where(inventorylock.OrderNoEQ(req.OrderNo))
|
||||
}
|
||||
if len(req.LockIDs) > 0 {
|
||||
q = q.Where(inventorylock.IDIn(req.LockIDs...))
|
||||
}
|
||||
locks, err := q.All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
released := 0
|
||||
for _, l := range locks {
|
||||
// 还原批次锁定数 / SN 状态
|
||||
if l.TargetType == "BATCH" {
|
||||
b, err := ctx.EntClient.InventoryBatch.Query().
|
||||
Where(inventorybatch.BatchNoEQ(l.TargetID)).Only(ctx0())
|
||||
if err == nil {
|
||||
newLocked := b.LockedQty - l.LockedQty
|
||||
if newLocked < 0 {
|
||||
newLocked = 0
|
||||
}
|
||||
ctx.EntClient.InventoryBatch.UpdateOneID(b.ID).
|
||||
SetLockedQty(newLocked).ExecX(ctx0())
|
||||
}
|
||||
} else if l.TargetType == "SN" {
|
||||
ctx.EntClient.SerialNumber.Update().
|
||||
Where(serialnumber.SnCodeEQ(l.TargetID)).
|
||||
SetStatus("在库").ExecX(ctx0())
|
||||
}
|
||||
ctx.EntClient.InventoryLock.UpdateOneID(l.ID).
|
||||
SetStatus("RELEASED").ExecX(ctx0())
|
||||
released++
|
||||
}
|
||||
|
||||
ok(w, map[string]any{"released": released})
|
||||
}
|
||||
}
|
||||
|
||||
// checkStockHandler 校验库存是否满足需求(锁库前检查)
|
||||
func checkStockHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Items []struct {
|
||||
MaterialCode string `json:"materialCode"`
|
||||
Qty int `json:"qty"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := parseJSON(r, &req); err != nil {
|
||||
fail(w, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
results := make([]map[string]any, 0, len(req.Items))
|
||||
allOk := true
|
||||
for _, item := range req.Items {
|
||||
// 汇总批次可用量
|
||||
batches, err := ctx.EntClient.InventoryBatch.Query().
|
||||
Where(
|
||||
inventorybatch.MaterialCodeEQ(item.MaterialCode),
|
||||
inventorybatch.QualityStatusEQ("合格"),
|
||||
).
|
||||
All(ctx0())
|
||||
if err != nil {
|
||||
fail(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
availBatch := 0
|
||||
for _, b := range batches {
|
||||
availBatch += b.Quantity - b.LockedQty
|
||||
}
|
||||
// 汇总 SN 在库数
|
||||
availSerial, _ := ctx.EntClient.SerialNumber.Query().
|
||||
Where(
|
||||
serialnumber.MaterialCodeEQ(item.MaterialCode),
|
||||
serialnumber.StatusEQ("在库"),
|
||||
).
|
||||
Count(ctx0())
|
||||
|
||||
enough := availBatch+availSerial >= item.Qty
|
||||
if !enough {
|
||||
allOk = false
|
||||
}
|
||||
results = append(results, map[string]any{
|
||||
"materialCode": item.MaterialCode,
|
||||
"requireQty": item.Qty,
|
||||
"availQty": availBatch + availSerial,
|
||||
"batchAvail": availBatch,
|
||||
"serialAvail": availSerial,
|
||||
"enough": enough,
|
||||
})
|
||||
}
|
||||
|
||||
ok(w, map[string]any{"allOk": allOk, "items": results})
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var b [20]byte
|
||||
i := len(b)
|
||||
for n > 0 {
|
||||
i--
|
||||
b[i] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
i--
|
||||
b[i] = '-'
|
||||
}
|
||||
return string(b[i:])
|
||||
}
|
||||
Reference in New Issue
Block a user