feat: 重构BOM架构,新增基础设置与虚拟工位管理功能
本次重构删除原有BOM物料清单表,改用工单工艺组合×工艺物料清单作为唯一用料来源;新增系统基础设置表支持WMS地址、日志保留天数等配置,新增虚拟工位作业管理后台接口与前端页签控制功能,同时优化工位号校验逻辑、事件日志自动清理与工单用料查询能力。
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bj_power_mes/ent/sysconfig"
|
||||
"bj_power_mes/internal/wmsclient"
|
||||
)
|
||||
|
||||
// 系统基础设置配置键(sys_config 表,系统管理员「基础设置」页维护,保存即生效)
|
||||
const (
|
||||
// CfgWmsBaseURL WMS 地址(IP:端口),保存即热切换 wmsclient;空=用 YAML 配置/默认地址
|
||||
CfgWmsBaseURL = "wms.base_url"
|
||||
// CfgDemandDays 需求窗口天数:缺料补货/未来需求按日排产取未来 N 天(默认 5)
|
||||
CfgDemandDays = "demand.days"
|
||||
// CfgExtrapWindow 排产支撑外推均值窗口(近 N 天平均日需求,默认 3)
|
||||
CfgExtrapWindow = "producible.extrap.window"
|
||||
// CfgExtrapCap 排产支撑外推封顶天数(防死循环,默认 365)
|
||||
CfgExtrapCap = "producible.extrap.cap"
|
||||
// CfgLogRetention 事件日志保留天数,超过自动清理(0=不清理)
|
||||
CfgLogRetention = "eventlog.retention.days"
|
||||
)
|
||||
|
||||
// startedAt 服务启动时刻(本机信息展示用)
|
||||
var startedAt = time.Now()
|
||||
|
||||
// SettingsVO 基础设置视图:可配置项 + 本机基础信息(只读展示)
|
||||
type SettingsVO struct {
|
||||
// 可配置项
|
||||
WmsBaseURL string `json:"wmsBaseURL"` // WMS 地址(空=未自定义,走 YAML/默认)
|
||||
DemandDays int `json:"demandDays"` // 需求窗口天数
|
||||
ExtrapWindow int `json:"extrapWindow"` // 外推均值窗口天数
|
||||
ExtrapCap int `json:"extrapCap"` // 外推封顶天数
|
||||
LogRetentionDays int `json:"logRetentionDays"` // 事件日志保留天数(0=不清理)
|
||||
|
||||
// 本机基础信息(只读)
|
||||
LocalIPs []string `json:"localIPs"` // 本机内网 IPv4 列表
|
||||
ListenAddr string `json:"listenAddr"` // 服务监听地址
|
||||
DBInfo string `json:"dbInfo"` // 数据库 host:port/db(不含密码)
|
||||
EffectiveWms string `json:"effectiveWms"` // 当前生效的 WMS 地址
|
||||
GoVersion string `json:"goVersion"` // Go 运行时版本
|
||||
StartedAt string `json:"startedAt"` // 服务启动时间
|
||||
}
|
||||
|
||||
// SettingsReq 基础设置保存请求(nil 字段不改,便于部分保存)
|
||||
type SettingsReq struct {
|
||||
WmsBaseURL *string `json:"wmsBaseURL"`
|
||||
DemandDays *int `json:"demandDays"`
|
||||
ExtrapWindow *int `json:"extrapWindow"`
|
||||
ExtrapCap *int `json:"extrapCap"`
|
||||
LogRetentionDays *int `json:"logRetentionDays"`
|
||||
}
|
||||
|
||||
// defaults 建议默认值(未配置时生效)
|
||||
var settingsDefaults = map[string]int{
|
||||
CfgDemandDays: 5,
|
||||
CfgExtrapWindow: 3,
|
||||
CfgExtrapCap: 365,
|
||||
CfgLogRetention: 0,
|
||||
}
|
||||
|
||||
// GetSettings 读取基础设置 + 本机信息。DB 读失败时按默认值返回(设置页必须能打开)。
|
||||
func (s *Service) GetSettings(ctx context.Context) (*SettingsVO, error) {
|
||||
kv := map[string]string{}
|
||||
if rows, err := s.ctx.EntClient.SysConfig.Query().All(ctx); err == nil {
|
||||
for _, r := range rows {
|
||||
kv[r.Key] = r.Value
|
||||
}
|
||||
}
|
||||
vo := &SettingsVO{
|
||||
WmsBaseURL: kv[CfgWmsBaseURL],
|
||||
DemandDays: cfgIntFrom(kv, CfgDemandDays),
|
||||
ExtrapWindow: cfgIntFrom(kv, CfgExtrapWindow),
|
||||
ExtrapCap: cfgIntFrom(kv, CfgExtrapCap),
|
||||
LogRetentionDays: cfgIntFrom(kv, CfgLogRetention),
|
||||
LocalIPs: localIPv4s(),
|
||||
ListenAddr: fmt.Sprintf("%s:%d", s.ctx.Config.Host, s.ctx.Config.Port),
|
||||
DBInfo: fmt.Sprintf("%s:%d/%s", s.ctx.Config.Database.Host, s.ctx.Config.Database.Port, s.ctx.Config.Database.Dbname),
|
||||
EffectiveWms: strings.TrimSuffix(s.ctx.Wms.BaseURL(), "/"),
|
||||
GoVersion: runtime.Version(),
|
||||
StartedAt: startedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
return vo, nil
|
||||
}
|
||||
|
||||
// SaveSettings 保存基础设置(幂等 upsert,nil 字段不改);WMS 地址变化即热切换。
|
||||
func (s *Service) SaveSettings(ctx context.Context, req SettingsReq, operator string) error {
|
||||
// 校验
|
||||
if req.WmsBaseURL != nil {
|
||||
u := strings.TrimSpace(*req.WmsBaseURL)
|
||||
if u != "" && !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||
return errors.New("WMS 地址需以 http:// 或 https:// 开头,如 http://192.168.1.100:9091")
|
||||
}
|
||||
}
|
||||
type item struct {
|
||||
key string
|
||||
val *int
|
||||
lo int
|
||||
hi int
|
||||
remark string
|
||||
}
|
||||
items := []item{
|
||||
{CfgDemandDays, req.DemandDays, 1, 60, "需求窗口天数(缺料补货/未来需求)"},
|
||||
{CfgExtrapWindow, req.ExtrapWindow, 1, 30, "排产支撑外推均值窗口天数"},
|
||||
{CfgExtrapCap, req.ExtrapCap, 30, 3650, "排产支撑外推封顶天数"},
|
||||
{CfgLogRetention, req.LogRetentionDays, 0, 3650, "事件日志保留天数(0=不清理)"},
|
||||
}
|
||||
for _, it := range items {
|
||||
if it.val == nil {
|
||||
continue
|
||||
}
|
||||
if *it.val < it.lo || *it.val > it.hi {
|
||||
return fmt.Errorf("%s 取值范围 %d~%d", it.remark, it.lo, it.hi)
|
||||
}
|
||||
if err := s.upsertConfig(ctx, it.key, strconv.Itoa(*it.val), it.remark); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.WmsBaseURL != nil {
|
||||
u := strings.TrimSpace(*req.WmsBaseURL)
|
||||
if err := s.upsertConfig(ctx, CfgWmsBaseURL, u, "WMS 服务地址(保存即生效)"); err != nil {
|
||||
return err
|
||||
}
|
||||
// 热切换:保存即生效,无需重启
|
||||
s.ctx.Wms.SetBaseURL(u)
|
||||
}
|
||||
payload := map[string]any{"req": req}
|
||||
s.ctx.EventLog.Write(ctx, "sys.settings.save", "", operator, "sys_config", "", "保存基础设置", payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestWms 测试 WMS 连通性:调免登录看板总览接口,返回耗时/错误。
|
||||
// candidate 非空时测试该地址(保存前试测用),为空测当前生效地址。
|
||||
func (s *Service) TestWms(ctx context.Context, candidate string) (string, error) {
|
||||
client := s.ctx.Wms
|
||||
if u := strings.TrimSpace(candidate); u != "" {
|
||||
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||
return "", errors.New("WMS 地址需以 http:// 或 https:// 开头")
|
||||
}
|
||||
client = wmsclient.New(u, s.ctx.Config.Wms.Token)
|
||||
}
|
||||
c2, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
if _, err := client.DisplayOverview(c2); err != nil {
|
||||
return "", fmt.Errorf("连接失败: %v", err)
|
||||
}
|
||||
return fmt.Sprintf("连接成功(%d ms)", time.Since(start).Milliseconds()), nil
|
||||
}
|
||||
|
||||
// CfgInt 读取整型配置(未配置/非法值回退 def)。供业务模块读取可配置参数。
|
||||
func (s *Service) CfgInt(ctx context.Context, key string, def int) int {
|
||||
v, err := s.ctx.EntClient.SysConfig.Query().Where(sysconfig.Key(key)).Only(ctx)
|
||||
if err != nil || v == nil {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(v.Value))
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// upsertConfig 幂等写入配置
|
||||
func (s *Service) upsertConfig(ctx context.Context, key, value, remark string) error {
|
||||
exist, err := s.ctx.EntClient.SysConfig.Query().Where(sysconfig.Key(key)).Only(ctx)
|
||||
if err == nil && exist != nil {
|
||||
return exist.Update().SetValue(value).SetRemark(remark).Exec(ctx)
|
||||
}
|
||||
return s.ctx.EntClient.SysConfig.Create().SetKey(key).SetValue(value).SetRemark(remark).Exec(ctx)
|
||||
}
|
||||
|
||||
// cfgIntFrom 从已加载的 kv map 解析整型(非法/未配置回退默认值)
|
||||
func cfgIntFrom(kv map[string]string, key string) int {
|
||||
v, ok := kv[key]
|
||||
if !ok || strings.TrimSpace(v) == "" {
|
||||
return settingsDefaults[key]
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(v))
|
||||
if err != nil {
|
||||
return settingsDefaults[key]
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// localIPv4s 枚举本机非环回 IPv4(内网 IP 展示)
|
||||
func localIPv4s() []string {
|
||||
hostname, _ := os.Hostname()
|
||||
out := []string{}
|
||||
if hostname != "" {
|
||||
out = append(out, hostname)
|
||||
}
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
for _, a := range addrs {
|
||||
ipnet, ok := a.(*net.IPNet)
|
||||
if !ok || ipnet.IP.IsLoopback() || ipnet.IP.To4() == nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, ipnet.IP.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user