Files
bj_power/bj_power_mes/internal/logic/settings.go
T

214 lines
7.6 KiB
Go
Raw Normal View History

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
}