Files
bj_power/bj_power_mes/internal/logic/settings.go
T
SunYF f274da044c feat: 重构BOM架构,新增基础设置与虚拟工位管理功能
本次重构删除原有BOM物料清单表,改用工单工艺组合×工艺物料清单作为唯一用料来源;新增系统基础设置表支持WMS地址、日志保留天数等配置,新增虚拟工位作业管理后台接口与前端页签控制功能,同时优化工位号校验逻辑、事件日志自动清理与工单用料查询能力。
2026-09-23 11:41:28 +08:00

214 lines
7.6 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 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
}