chore: 完成多系统功能迭代与优化

1. 通用优化:
   - 统一系统标题为"库房客户端"/"MES产线控制",移除北自所前缀
   - 调整内置账号密码为统一123456,优化密码校验逻辑
   - 新增数据库自动创建逻辑,简化部署流程
   - 修复日志时间格式配置,优化日志输出
   - 新增纯数字密码安全提示

2. MES系统优化:
   - 新增日排产管理功能,支持增删改查与自动算料生成备料单
   - 重构BOM模块,改为按产品编码维护而非工单维度
   - 新增工艺参数模板配置与手动报工页面
   - 新增产品类型自动编码功能
   - 优化菜单结构,调整工单管理、扫码报工等页面名称与路由
   - 新增删除日排产接口与权限保护
   - 修复物料清单查询逻辑,适配新BOM结构
   - 新增refreshToken支持,优化登录会话管理

3. WMS系统优化:
   - 新增基础数据维护页面,支持区域与物料档案管理
   - 新增工单列表下拉接口,对接MES获取未完成工单优先展示
   - 优化入库管理提示文案,替换英文提示为中文
   - 修复精密件入库校验逻辑,优化错误提示
   - 优化Excel导入功能,新增备注字段支持
   - 优化登录页面与菜单文案,统一备料台账名称
   - 新增自动打开浏览器功能,优化客户端启动体验
   - 修复备料出库页面查询提示文案
   - 新增WMS与MES对接配置,完善内部API调用逻辑

4. 其他优化:
   - 删除冗余的旧版静态资源文件,更新资源引用路径
   - 新增帮助文档,完善基础数据模块说明
   - 修复多处文案不统一、英文残留问题
This commit is contained in:
SunYF
2026-08-29 15:30:25 +08:00
parent e82b744bbd
commit ebf901ad16
124 changed files with 1815 additions and 480 deletions
+41
View File
@@ -164,6 +164,9 @@ func (s *Service) ChangePassword(ctx context.Context, userId int, req ChangePass
if len(req.NewPassword) < 6 {
return errors.New("新密码长度至少 6 位")
}
if err := checkPasswordStrong(req.NewPassword); err != nil {
return err
}
usr, err := s.ctx.EntClient.User.Get(ctx, userId)
if err != nil {
return errors.New("用户不存在")
@@ -189,7 +192,28 @@ type UserReq struct {
Status string `json:"status"`
}
// checkPasswordStrong 校验密码:长度>=6 且不能是纯数字
func checkPasswordStrong(pwd string) error {
if len(pwd) < 6 {
return errors.New("密码长度至少 6 位")
}
allDigit := true
for _, r := range pwd {
if r < '0' || r > '9' {
allDigit = false
break
}
}
if allDigit {
return errors.New("密码不能是纯数字")
}
return nil
}
func (s *Service) CreateUser(ctx context.Context, req UserReq) error {
if err := checkPasswordStrong(req.Password); err != nil {
return err
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return err
@@ -210,6 +234,9 @@ func (s *Service) CreateUser(ctx context.Context, req UserReq) error {
func (s *Service) UpdateUser(ctx context.Context, req UserReq) error {
u := s.ctx.EntClient.User.UpdateOneID(req.Id).SetRoleId(req.RoleId).SetName(req.Name)
if req.Password != "" {
if err := checkPasswordStrong(req.Password); err != nil {
return err
}
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
u.SetPassword(string(hash))
}
@@ -251,6 +278,20 @@ func (s *Service) UpdateRole(ctx context.Context, req RoleReq) error {
}
func (s *Service) DeleteRole(ctx context.Context, id int) error {
// 保护:不可删除超级管理员角色
r, err := s.ctx.EntClient.Role.Get(ctx, id)
if err != nil {
return errors.New("角色不存在")
}
if r.Code == "SUPER_ADMIN" {
return errors.New("超级管理员角色不允许删除")
}
// 保护:仍有关联用户的角色不可删除(避免产生无主用户/失去全部管理员)
cnt, err := s.ctx.EntClient.User.Query().
Where(user.RoleId(id)).Count(ctx)
if err == nil && cnt > 0 {
return errors.New("该角色下仍有用户,请先移除相关用户")
}
return s.ctx.EntClient.Role.DeleteOneID(id).Exec(ctx)
}
+14 -14
View File
@@ -8,26 +8,26 @@ import (
)
type BomItemReq struct {
WorkOrderNo string `json:"workOrderNo"`
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
ManageMode string `json:"manageMode"`
ProductCode string `json:"productCode"`
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
Spec string `json:"spec"`
Unit string `json:"unit"`
ManageMode string `json:"manageMode"`
UnitQty float64 `json:"unitQty"`
LossRate float64 `json:"lossRate"`
RequiredQty float64 `json:"requiredQty"`
SerialSns []string `json:"serialSns"`
}
// SaveBom 保存工单 BOM(覆盖式:按 工单号+物料编码 做 upsert
func (s *Service) SaveBom(ctx context.Context, workOrderNo string, items []BomItemReq, operator string) error {
// SaveBom 保存产品 BOM(覆盖式:按 产品编码+物料编码 做 upsert
func (s *Service) SaveBom(ctx context.Context, productCode string, items []BomItemReq, operator string) error {
for _, it := range items {
if it.ManageMode == "" {
it.ManageMode = "2"
}
exist, err := s.ctx.EntClient.BomItem.Query().
Where(bomitem.WorkOrderNo(workOrderNo), bomitem.MaterialCode(it.MaterialCode)).First(ctx)
Where(bomitem.ProductCode(productCode), bomitem.MaterialCode(it.MaterialCode)).First(ctx)
if err == nil && exist != nil {
u := s.ctx.EntClient.BomItem.UpdateOneID(exist.ID).
SetMaterialName(it.MaterialName).SetSpec(it.Spec).SetUnit(it.Unit).
@@ -39,7 +39,7 @@ func (s *Service) SaveBom(ctx context.Context, workOrderNo string, items []BomIt
_ = u.Exec(ctx)
} else {
b := s.ctx.EntClient.BomItem.Create().
SetWorkOrderNo(workOrderNo).SetMaterialCode(it.MaterialCode).
SetProductCode(productCode).SetMaterialCode(it.MaterialCode).
SetMaterialName(it.MaterialName).SetSpec(it.Spec).SetUnit(it.Unit).
SetManageMode(it.ManageMode).SetUnitQty(it.UnitQty).SetLossRate(it.LossRate).
SetRequiredQty(it.RequiredQty)
@@ -49,14 +49,14 @@ func (s *Service) SaveBom(ctx context.Context, workOrderNo string, items []BomIt
_ = b.Exec(ctx)
}
}
s.ctx.EventLog.Write(ctx, "bom.save", workOrderNo, operator, "work_order_bom", workOrderNo, "保存BOM", map[string]any{"count": len(items)})
s.ctx.EventLog.Write(ctx, "bom.save", "", operator, "product_bom", productCode, "保存产品BOM", map[string]any{"count": len(items)})
return nil
}
func (s *Service) ListBom(ctx context.Context, workOrderNo string) ([]*ent.BomItem, error) {
func (s *Service) ListBom(ctx context.Context, productCode string) ([]*ent.BomItem, error) {
q := s.ctx.EntClient.BomItem.Query()
if workOrderNo != "" {
q = q.Where(bomitem.WorkOrderNo(workOrderNo))
if productCode != "" {
q = q.Where(bomitem.ProductCode(productCode))
}
return q.Order(ent.Asc(bomitem.FieldID)).All(ctx)
}
+35 -7
View File
@@ -7,6 +7,7 @@ import (
"bj_power_mes/ent"
"bj_power_mes/ent/dailyplan"
"bj_power_mes/ent/workorder"
)
// parseDate 解析 yyyy-MM-dd 或标准时间
@@ -18,12 +19,12 @@ func parseDate(v string) (time.Time, error) {
}
type DailyPlanReq struct {
Id int `json:"id"`
OrderNo string `json:"orderNo"`
PlanDate string `json:"planDate"`
PlanQty int `json:"planQty"`
CompletedQty int `json:"completedQty"`
Status string `json:"status"`
Id int `json:"id"`
OrderNo string `json:"orderNo"`
PlanDate string `json:"planDate"`
PlanQty int `json:"planQty"`
CompletedQty int `json:"completedQty"`
Status string `json:"status"`
}
// SaveDailyPlan 创建/更新日排产(同一 工单+日期 唯一)
@@ -31,6 +32,28 @@ func (s *Service) SaveDailyPlan(ctx context.Context, req DailyPlanReq, operator
if req.OrderNo == "" || req.PlanDate == "" {
return errors.New("工单号和排产日期必填")
}
// 校验:该工单所有日排产计划数量累计不得超过工单总数量(保存时合计,改小可、改大不超总量)
wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(req.OrderNo)).First(ctx)
if err != nil {
return errors.New("工单不存在")
}
cur, err := s.ctx.EntClient.DailyPlan.Query().
Where(dailyplan.OrderNo(req.OrderNo)).All(ctx)
if err != nil {
return err
}
total := 0
for _, p := range cur {
if p.PlanDate == req.PlanDate && p.Status == "PENDING" {
// 覆盖同一 工单+日期 时,旧数量换掉
total += req.PlanQty
continue
}
total += p.PlanQty
}
if total > wo.Quantity {
return errors.New("排产合计数量超过工单总数量,无法保存")
}
exist, err := s.ctx.EntClient.DailyPlan.Query().
Where(dailyplan.OrderNo(req.OrderNo), dailyplan.PlanDate(req.PlanDate)).First(ctx)
status := req.Status
@@ -65,4 +88,9 @@ func (s *Service) ListDailyPlans(ctx context.Context, orderNo, planDate string)
q = q.Where(dailyplan.PlanDate(planDate))
}
return q.Order(ent.Desc(dailyplan.FieldPlanDate)).All(ctx)
}
}
// DeleteDailyPlan 删除一条日排产记录
func (s *Service) DeleteDailyPlan(ctx context.Context, id int) error {
return s.ctx.EntClient.DailyPlan.DeleteOneID(id).Exec(ctx)
}
+9 -2
View File
@@ -8,10 +8,11 @@ import (
"bj_power_mes/ent"
"bj_power_mes/ent/materialrequest"
"bj_power_mes/ent/workorder"
)
// GenerateMaterialRequest 按日排产自动算料生成备料单
// 数量 = 该日所有工单计划数量 × BOM单台用量×(1+损耗率),材料来源=BOM
// 数量 = 该日所有工单计划数量 × 产品BOM单台用量×(1+损耗率),材料来源=该工单产品的 BOM
func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string, operator string) (int, error) {
plans, err := s.ListDailyPlans(ctx, "", planDate)
if err != nil {
@@ -22,7 +23,13 @@ func (s *Service) GenerateMaterialRequest(ctx context.Context, planDate string,
}
created := 0
for _, plan := range plans {
bom, err := s.ListBom(ctx, plan.OrderNo)
// 解析工单 → 产品编码 → 该产品 BOM
wo, err := s.ctx.EntClient.WorkOrder.Query().
Where(workorder.WorkOrderNo(plan.OrderNo)).Only(ctx)
if err != nil {
continue
}
bom, err := s.ListBom(ctx, wo.ProductCode)
if err != nil {
continue
}
+2 -2
View File
@@ -33,8 +33,8 @@ func (s *Service) Seed(ctx context.Context) error {
_ = s.ctx.EntClient.Role.Create().SetName(r.name).SetCode(r.code).SetPermissionCodes(codes).Exec(ctx)
}
// 2. 超级管理员账号 admin / Hardman_2026(对已存在的旧行做对齐,避免残留禁用状态)
hash, _ := bcrypt.GenerateFromPassword([]byte("Hardman_2026"), bcrypt.DefaultCost)
// 2. 超级管理员账号 admin / 123456(对已存在的旧行做对齐,避免残留禁用状态)
hash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
adminRole, _ := s.ctx.EntClient.Role.Query().Where(role.Code("SUPER_ADMIN")).First(ctx)
roleId := 0
if adminRole != nil {
+129
View File
@@ -6,6 +6,7 @@ import (
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/torquerecord"
"bj_power_mes/ent/workpieceprocess"
@@ -97,3 +98,131 @@ func (s *Service) hasWorkpieceProcess(ctx context.Context, sn string, processCod
Where(workpieceprocess.Sn(sn), workpieceprocess.ProcessCode(processCode)).Count(ctx)
return cnt > 0
}
// ProcessStepTemplate 下发给前端渲染的参数采集模板
type ProcessStepTemplate struct {
ID int `json:"id"`
ProcessCode int `json:"processCode"`
Seq int `json:"seq"`
Name string `json:"name"`
CollectType string `json:"collectType"` // AUTO(拧紧枪自动)/MANUAL(手填)/NONE
IsTorque bool `json:"isTorque"`
Remark string `json:"remark"`
Criteria []CriterionVO `json:"criteria"`
}
// CriterionVO 考核标准视图
type CriterionVO struct {
ID int `json:"id"`
Name string `json:"name"`
Unit string `json:"unit"`
Logic string `json:"logic"` // GE/LE/GT/LT/RANGE/EQUAL/NONE
Target float64 `json:"target"`
Min *float64 `json:"min"`
Max *float64 `json:"max"`
}
// ListProcessSteps 查询某工序的步骤模板(含考核标准),供手动报工页动态渲染参数输入
func (s *Service) ListProcessSteps(ctx context.Context, processCode int) ([]*ProcessStepTemplate, error) {
steps, err := s.ctx.EntClient.ProcessStep.Query().
Where(processstep.ProcessCode(processCode)).
Order(ent.Asc(processstep.FieldSeq)).All(ctx)
if err != nil {
return nil, err
}
out := make([]*ProcessStepTemplate, 0, len(steps))
for _, st := range steps {
t := &ProcessStepTemplate{
ID: st.ID, ProcessCode: st.ProcessCode, Seq: st.Seq,
Name: st.Name, CollectType: st.CollectType, IsTorque: st.IsTorque,
Remark: st.Remark, Criteria: []CriterionVO{},
}
crits, _ := s.ctx.EntClient.StepCriterion.Query().
Where(stepcriterion.StepId(st.ID)).All(ctx)
for _, c := range crits {
t.Criteria = append(t.Criteria, CriterionVO{
ID: c.ID, Name: c.Name, Unit: c.Unit, Logic: c.Logic,
Target: c.Target, Min: c.Min, Max: c.Max,
})
}
out = append(out, t)
}
return out, nil
}
// StepItemReq 一次保存的一个步骤(含其考核标准)
type StepItemReq struct {
ID int `json:"id"`
ProcessCode int `json:"processCode"`
Seq int `json:"seq"`
Name string `json:"name"`
CollectType string `json:"collectType"`
IsTorque bool `json:"isTorque"`
Remark string `json:"remark"`
Criteria []CriterionVO `json:"criteria"`
}
// SaveProcessSteps 覆盖式保存某工序的步骤模板及考核标准(供"工艺参数"维护页调用)
func (s *Service) SaveProcessSteps(ctx context.Context, steps []StepItemReq, operator string) error {
for _, st := range steps {
if st.ProcessCode <= 0 || st.Name == "" {
continue
}
var stepID int
if st.ID > 0 {
upd := s.ctx.EntClient.ProcessStep.UpdateOneID(st.ID).
SetSeq(st.Seq).SetName(st.Name).SetCollectType(st.CollectType).
SetIsTorque(st.IsTorque).SetRemark(st.Remark)
_, err := upd.Save(ctx)
if err != nil {
return err
}
stepID = st.ID
} else {
rec, err := s.ctx.EntClient.ProcessStep.Create().
SetProcessCode(st.ProcessCode).SetSeq(st.Seq).SetName(st.Name).
SetCollectType(st.CollectType).SetIsTorque(st.IsTorque).SetRemark(st.Remark).Save(ctx)
if err != nil {
return err
}
stepID = rec.ID
}
// 覆盖式重建该步骤的考核标准
_, _ = s.ctx.EntClient.StepCriterion.Delete().Where(stepcriterion.StepId(stepID)).Exec(ctx)
for _, c := range st.Criteria {
if c.Name == "" || c.Logic == "" || c.Logic == "NONE" {
continue
}
b := s.ctx.EntClient.StepCriterion.Create().
SetStepId(stepID).SetName(c.Name).SetUnit(c.Unit).SetLogic(c.Logic).
SetTarget(c.Target)
if c.Min != nil {
b = b.SetMin(*c.Min)
}
if c.Max != nil {
b = b.SetMax(*c.Max)
}
if err := b.Exec(ctx); err != nil {
return err
}
}
}
s.ctx.EventLog.Write(ctx, "process.steps.save", "", operator, "process_step", "", "维护工艺参数", map[string]any{"count": len(steps)})
return nil
}
// AllProcessSteps 查询全部工序的步骤模板(含 12 道工序),供维护页按工序分组展示
func (s *Service) AllProcessSteps(ctx context.Context, processCode int) ([]*ProcessStepTemplate, error) {
if processCode > 0 {
return s.ListProcessSteps(ctx, processCode)
}
out := []*ProcessStepTemplate{}
for code := 1; code <= 12; code++ {
sub, err := s.ListProcessSteps(ctx, code)
if err != nil {
return nil, err
}
out = append(out, sub...)
}
return out, nil
}