2026-09-17 12:49:07 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"bj_power_wms/internal/svc"
|
|
|
|
|
)
|
|
|
|
|
|
2026-09-19 07:50:21 +08:00
|
|
|
// 可生产数量(问题记录 L470-474 + L256):
|
|
|
|
|
// - 可生产套数 = min(各物料可用量 ÷ 单台用量)。BOM 单台用量在 MES、库存可用量在 WMS,
|
|
|
|
|
// 由 MES 实时计算,WMS 打开本页时按需拉一次(两系统之间不做定时/批量同步)。
|
2026-09-17 12:49:07 +08:00
|
|
|
// - 未来5天需求量 = 近5天日排产 × 物料清单单台用量,是备料四态「预警」判定的数据源。
|
|
|
|
|
//
|
2026-09-19 07:50:21 +08:00
|
|
|
// 拉取语义:每次查询先实时取 MES 结果,再全量覆盖本地快照(单事务),保证展示与 MES 当前口径一致。
|
|
|
|
|
|
|
|
|
|
// queryProducibleHandler GET /api/stock/producible 可生产数量(实时拉取 MES 后落快照再返回)
|
|
|
|
|
func queryProducibleHandler(ctx *svc.ServiceContext) http.HandlerFunc {
|
2026-09-17 12:49:07 +08:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
2026-09-19 07:50:21 +08:00
|
|
|
if ctx.Mes == nil {
|
|
|
|
|
fail(w, http.StatusBadRequest, "未配置 MES 地址,无法获取可生产数量")
|
|
|
|
|
return
|
2026-09-17 12:49:07 +08:00
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
res, err := ctx.Mes.FetchProducible(ctx0())
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusBadGateway, "拉取 MES 可生产数量失败: "+err.Error())
|
2026-09-17 12:49:07 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
now := time.Now().Unix()
|
2026-09-19 07:50:21 +08:00
|
|
|
|
2026-09-17 12:49:07 +08:00
|
|
|
tx, err := ctx.EntClient.Tx(ctx0())
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "开启事务失败: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
committed := false
|
|
|
|
|
defer func() {
|
|
|
|
|
if !committed {
|
|
|
|
|
_ = tx.Rollback()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
if _, err = tx.ProducibleSnapshot.Delete().Exec(ctx0()); err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "清理旧快照失败: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
for _, it := range res.Items {
|
2026-09-17 12:49:07 +08:00
|
|
|
if it.ProductCode == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, err = tx.ProducibleSnapshot.Create().
|
|
|
|
|
SetProductCode(it.ProductCode).
|
|
|
|
|
SetNillableProductName(strPtr(it.ProductName)).
|
|
|
|
|
SetProducibleQty(it.ProducibleQty).
|
|
|
|
|
SetNillableShortText(strPtr(it.ShortText)).
|
|
|
|
|
SetComputedAt(it.ComputedAt).
|
|
|
|
|
SetSyncedAt(now).
|
|
|
|
|
Save(ctx0()); err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "写入快照失败: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 07:50:21 +08:00
|
|
|
if _, err = tx.MaterialDemand.Delete().Exec(ctx0()); err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "清理旧需求失败: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, d := range res.Demands {
|
|
|
|
|
if d.MaterialCode == "" {
|
|
|
|
|
continue
|
2026-09-17 12:49:07 +08:00
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
if _, err = tx.MaterialDemand.Create().
|
|
|
|
|
SetMaterialCode(d.MaterialCode).
|
|
|
|
|
SetFuture5Qty(d.Future5Qty).
|
|
|
|
|
SetUpdatedAt(now).
|
|
|
|
|
Save(ctx0()); err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "写入需求失败: "+err.Error())
|
|
|
|
|
return
|
2026-09-17 12:49:07 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, "提交事务失败: "+err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
committed = true
|
|
|
|
|
|
|
|
|
|
list, err := ctx.EntClient.ProducibleSnapshot.Query().All(ctx0())
|
|
|
|
|
if err != nil {
|
|
|
|
|
fail(w, http.StatusInternalServerError, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-09-19 07:50:21 +08:00
|
|
|
ok(w, map[string]any{"list": list, "syncedAt": now})
|
2026-09-17 12:49:07 +08:00
|
|
|
}
|
|
|
|
|
}
|