Files
bj_power/bj_power_mes/internal/logic/seed.go
T
SunYF 132c3ed6bd docs(test): 更新电表箱装配业务回归测试文档
- 将文档从代码审计任务转换为端到端业务回归测试链路
- 重新组织文档结构为链路总览、示例数据基线和详细步骤
- 添加完整的业务场景清单涵盖WMS/MES/工位终端三大系统
- 定义统一的测试数据包括产品型号、物料清单、工位派工等
- 提供详细的步骤断言和数量等式验证方法
- 建立贯穿全链路的数据流向和状态变更追踪体系
2026-09-21 10:48:53 +08:00

345 lines
16 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"
"strconv"
"bj_power_mes/ent/permission"
"bj_power_mes/ent/role"
"bj_power_mes/ent/station"
"bj_power_mes/ent/user"
"golang.org/x/crypto/bcrypt"
)
// Seed 初始化:超级管理员、默认角色、默认菜单权限、产线工位(内置只读主数据;工艺流程由用户自建不预置)
func (s *Service) Seed(ctx context.Context) error {
// 1. 默认角色(已存在则合并权限码,保证新按钮权限能落到已有角色上)
roles := []struct{ name, code string }{
{"超级管理员", "SUPER_ADMIN"},
{"生产操作员", "OPERATOR"},
{"质检员", "INSPECTOR"},
}
for _, r := range roles {
codes := []string{"*"}
if r.code == "OPERATOR" {
codes = []string{
"produce.workorder", "produce.dailyplan", "produce.bom", "produce.material", "produce.scan",
"produce.trace", "produce.wip", "produce.torque", "produce.plc", "produce.product",
"produce.processflow", "produce.station", "produce.performance", "produce.processcard",
// 按钮级权限(块2
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate", "produce.material:outbound",
"produce.quality", "produce.quality:edit",
"produce.plc:send", "produce.product:add", "produce.product:edit", "produce.product:delete",
"produce.processflow:add", "produce.processflow:edit", "produce.processflow:delete",
"produce.processflow:upload", "produce.station:edit", "produce.station:add", "produce.station:delete",
// 预警中心 / 附件中心(含归档清理按钮)
"sys.alert", "sys.attachment", "sys.attachment:manage",
// 数量不符处理(上报在工位终端,处理在本页)
"produce.qty", "produce.qty:handle",
}
} else if r.code == "INSPECTOR" {
codes = []string{"produce.trace", "produce.wip", "produce.torque", "produce.performance", "sys.eventlog",
"sys.inspect", "sys.inspect:checkin", "sys.inspect:point", "sys.inspect:process",
"sys.inspect:done", "sys.inspect:alarm", "sys.inspect:view", "produce.quality", "produce.quality:edit"}
}
exist, err := s.ctx.EntClient.Role.Query().Where(role.Code(r.code)).First(ctx)
if err != nil {
_ = s.ctx.EntClient.Role.Create().SetName(r.name).SetCode(r.code).SetPermissionCodes(codes).Exec(ctx)
} else {
_ = s.ctx.EntClient.Role.UpdateOneID(exist.ID).SetPermissionCodes(mergeCodes(exist.PermissionCodes, codes)).Exec(ctx)
}
}
// 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 {
roleId = adminRole.ID
}
if s.ctx.EntClient.User.Query().Where(user.Username("admin")).ExistX(ctx) {
_ = s.ctx.EntClient.User.Update().
Where(user.Username("admin")).
SetPassword(string(hash)).SetStatus("ENABLED").SetRoleId(roleId).Exec(ctx)
} else {
_ = s.ctx.EntClient.User.Create().
SetUsername("admin").SetPassword(string(hash)).SetName("系统管理员").
SetRoleId(roleId).SetStatus("ENABLED").Exec(ctx)
}
// 3. 默认菜单权限
menus := []struct{ code, name, typ, path string }{
{"produce.workorder", "工单管理", "MENU", "/work-order"},
{"produce.dailyplan", "日排产", "MENU", "/daily-plan"},
{"produce.bom", "产品物料清单", "MENU", "/bom"},
{"produce.material", "备料单", "MENU", "/material-request"},
{"produce.qty", "数量不符处理", "MENU", "/qty-report"},
{"produce.plc", "工位组合下发", "MENU", "/plc-send"},
{"produce.torque", "拧紧查询", "MENU", "/torque"},
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
{"produce.station", "关联工位", "MENU", "/station"}, {"produce.performance", "绩效报表", "MENU", "/performance"},
{"produce.dock", "接驳台维护", "MENU", "/dock"},
{"produce.scan", "手动报工", "MENU", "/scan"},
{"produce.trace", "工件追溯", "MENU", "/trace"},
{"produce.wip", "在制品", "MENU", "/wip"},
{"produce.processcard", "生产流程卡", "MENU", "/process-card"},
{"produce.product", "物料档案", "MENU", "/product-type"},
{"produce.quality", "质量检验", "MENU", "/quality"},
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
{"sys.eventlog", "操作日志", "MENU", "/event-log"},
// sys.rbac(角色权限管理) 已拆分:账号管理 + 角色管理(对齐 WMS 系统管理菜单)
{"sys.account", "账号管理", "MENU", "/account"},
{"sys.role", "角色管理", "MENU", "/role"},
// 预警中心 / 附件中心此前只有前端菜单码(MainLayout menuGroups)、权限表未登记,
// 导致这两个菜单无法授权给任何角色(只有 SUPER_ADMIN 靠 roleCode 旁路可见),2026-09-19 补齐。
{"sys.alert", "预警中心", "MENU", "/alert-center"},
{"sys.attachment", "附件中心", "MENU", "/attachment"},
}
for _, m := range menus {
if s.ctx.EntClient.Permission.Query().Where(permission.Code(m.code)).ExistX(ctx) {
continue
}
_ = s.ctx.EntClient.Permission.Create().
SetCode(m.code).SetName(m.name).SetType(m.typ).SetPath(m.path).Exec(ctx)
}
// 3.1 按钮级权限(块2):前端按钮 + 后端接口 双重校验
// parent = 所属菜单权限码:每个按钮显式声明挂在哪个菜单下,前端据此组装权限树,
// 不再用 code.startsWith(菜单+':') 这类前缀通配推断。
buttons := []struct{ code, name, typ, path, parent string }{
{"produce.workorder:add", "工单-新增", "BUTTON", "", "produce.workorder"},
{"produce.workorder:edit", "工单-编辑", "BUTTON", "", "produce.workorder"},
{"produce.workorder:delete", "工单-删除", "BUTTON", "", "produce.workorder"},
{"produce.workorder:dailyplan", "工单-日排产", "BUTTON", "", "produce.workorder"},
{"produce.bom:edit", "物料清单-维护", "BUTTON", "", "produce.bom"},
{"produce.material:generate", "备料单-生成", "BUTTON", "", "produce.material"},
{"produce.material:outbound", "备料单-自动出库", "BUTTON", "", "produce.material"},
{"produce.qty:handle", "数量不符-处理", "BUTTON", "", "produce.qty"},
{"produce.plc:send", "工位组合-下发", "BUTTON", "", "produce.plc"},
{"produce.product:add", "物料档案-新增", "BUTTON", "", "produce.product"},
{"produce.product:edit", "物料档案-编辑", "BUTTON", "", "produce.product"},
{"produce.product:delete", "物料档案-删除", "BUTTON", "", "produce.product"},
{"produce.processflow:add", "工艺流程-新增", "BUTTON", "", "produce.processflow"},
{"produce.processflow:edit", "工艺流程-编辑", "BUTTON", "", "produce.processflow"},
{"produce.processflow:delete", "工艺流程-删除", "BUTTON", "", "produce.processflow"},
{"produce.processflow:upload", "工艺流程-上传图纸", "BUTTON", "", "produce.processflow"},
{"produce.station:edit", "工位-绑定与改名", "BUTTON", "", "produce.station"},
{"produce.station:add", "工位-新增", "BUTTON", "", "produce.station"},
{"produce.station:delete", "工位-删除", "BUTTON", "", "produce.station"},
{"sys.account:add", "账号-新增", "BUTTON", "", "sys.account"},
{"sys.account:edit", "账号-编辑", "BUTTON", "", "sys.account"},
{"sys.account:delete", "账号-删除", "BUTTON", "", "sys.account"},
{"sys.role:add", "角色-新增", "BUTTON", "", "sys.role"},
{"sys.role:edit", "角色-编辑", "BUTTON", "", "sys.role"},
{"sys.role:delete", "角色-删除", "BUTTON", "", "sys.role"},
{"sys.role:perm", "角色-权限定义", "BUTTON", "", "sys.role"},
{"sys.inspect:checkin", "巡检-开工签到", "BUTTON", "", "sys.inspect"},
{"sys.inspect:point", "巡检-工位点检", "BUTTON", "", "sys.inspect"},
{"sys.inspect:process", "巡检-过程巡检", "BUTTON", "", "sys.inspect"},
{"sys.inspect:done", "巡检-完工确认", "BUTTON", "", "sys.inspect"},
{"sys.inspect:alarm", "巡检-异常上报", "BUTTON", "", "sys.inspect"},
{"sys.inspect:view", "巡检-记录查看", "BUTTON", "", "sys.inspect"},
{"produce.quality:edit", "质量检验-录入/处置", "BUTTON", "", "produce.quality"},
// 附件中心(对齐 WMS attachment:manage 口径):删除 + 归档清理
{"sys.attachment:manage", "附件-删除/归档清理", "BUTTON", "", "sys.attachment"},
}
for _, b := range buttons {
exist, err := s.ctx.EntClient.Permission.Query().Where(permission.Code(b.code)).Only(ctx)
if err == nil && exist != nil {
// 已存在:幂等回填 parentCode(避免旧数据没有父子关系导致权限树漏按钮)
if exist.ParentCode != b.parent {
_ = exist.Update().SetParentCode(b.parent).Exec(ctx)
}
continue
}
_ = s.ctx.EntClient.Permission.Create().
SetCode(b.code).SetName(b.name).SetType(b.typ).SetPath(b.path).
SetParentCode(b.parent).Exec(ctx)
}
// 3.2 旧版权限码迁移(sys.rbac 家族 → sys.account / sys.role,幂等)
migrateLegacySysRbac(ctx, s)
// 3.3 工艺路线模块已整块删除(D1):清掉存量角色里的路线权限码与旧菜单/按钮定义行(幂等)
migrateLegacyRoute(ctx, s)
// 3.4 旧「数量不符上报」菜单(produce.qtyreport)已下线,改用 produce.qty(数量不符处理)。
// 清掉存量角色里的旧码与旧菜单定义行(幂等);新菜单 produce.qty 在 1. 的 seedMenus 中定义。
migrateLegacyQtyReport(ctx, s)
// 4. 初始化产线工位(内置只读主数据);工艺流程/工序步骤由用户在「工艺流程」页自建,不预置
seedStations(ctx, s)
// 5. 接驳台主数据(真源在 MES2026-09-21 自 WMS 迁入);系统预置只读
seedDocks(ctx, s)
return nil
}
// seedDocks 接驳台主数据:DOCK01~20(产线, DOCKn↔工位n) + DOCK21(库房收货)。
// 系统预置只读主数据(不提供增删改),与工位种子同源一次性写入。
func seedDocks(ctx context.Context, s *Service) {
count, _ := s.ctx.EntClient.Dock.Query().Count(ctx)
if count > 0 {
return
}
for i := 1; i <= 20; i++ {
code := "DOCK" + pad2(i)
_, _ = s.ctx.EntClient.Dock.Create().
SetDockCode(code).
SetName("产线接驳台 " + code + "(工位" + strconv.Itoa(i) + "").
SetDockType("line").
SetStationNo(i).
SetHasPallet(false).
SetStatus("ENABLED").
Save(ctx)
}
_, _ = s.ctx.EntClient.Dock.Create().
SetDockCode("DOCK21").
SetName("库房收货接驳台").
SetDockType("store").
SetStationNo(0).
SetHasPallet(false).
SetStatus("ENABLED").
Save(ctx)
}
func seedStations(ctx context.Context, s *Service) {
// 工位是产线固定物理工位,一律「内置工位」:需连 PLC、按工位号顺序流转、不可改类型、不可删除、仅可改名,
// 属系统内置只读主数据(同 WMS 接驳台),故由种子初始化。12 个装配工位(1~10 带接驳台,11/12 产线末端无接驳台);
// 工位数量以数据库为准,要 13 个就往 station 表插一行。
// 工艺流程/工序步骤不在此预置:流程可在「工艺流程」页新建/编辑/删除/启停,是用户可维护的业务数据,
// 由实施按产品工艺自建、并在「关联工位」页绑定到工位(station.flow_id),不硬编码进种子。
for i := 1; i <= 12; i++ {
if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx); err != nil {
_, _ = s.ctx.EntClient.Station.Create().
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").
SetIsBuiltin(true).SetHasDock(i <= 10).Save(ctx)
}
}
// 0 号(上线位) / 13 号(下线位) 虚拟工位:OFFLINE 类型,用 PAD 操作,不连 PLC、
// 不叫料、不进入产线组合排序;仅做上下线登记,不参与物理工位计数。
for _, v := range []struct {
no int
name string
}{
{0, "上线位(虚拟)"},
{13, "下线位(虚拟)"},
} {
if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(v.no)).First(ctx); err != nil {
_, _ = s.ctx.EntClient.Station.Create().
SetStationNo(v.no).SetName(v.name).SetStationType("OFFLINE").
SetIsBuiltin(true).SetHasDock(false).SetStatus("ENABLED").Save(ctx)
}
}
}
// mergeCodes 合并两份权限码列表(去重、保序)
func mergeCodes(a, b []string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(a)+len(b))
for _, c := range append(append([]string{}, a...), b...) {
if c == "" || seen[c] {
continue
}
seen[c] = true
out = append(out, c)
}
return out
}
// migrateLegacySysRbac 迁移旧 sys.rbac 家族权限码(幂等,可重复执行):
// sys.rbac(角色权限管理) 已拆分为 sys.account(账号管理)+sys.role(角色管理)。
// 对存量角色 permissionCodes 做码替换,并删除已废弃的权限定义行。
func migrateLegacySysRbac(ctx context.Context, s *Service) {
legacyMap := map[string][]string{
"sys.rbac": {"sys.account", "sys.role"},
"sys.rbac:user": {"sys.account:add", "sys.account:edit", "sys.account:delete"},
"sys.rbac:role": {"sys.role:add", "sys.role:edit", "sys.role:delete"},
"sys.rbac:perm": {"sys.role:perm"},
}
// 1) 角色 permissionCodes 替换
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
for _, rl := range roleList {
changed := false
out := make([]string, 0, len(rl.PermissionCodes))
for _, c := range rl.PermissionCodes {
if repl, ok := legacyMap[c]; ok {
out = append(out, repl...)
changed = true
continue
}
out = append(out, c)
}
if changed {
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
SetPermissionCodes(mergeCodes(out, nil)).Exec(ctx)
}
}
// 2) 删除旧权限定义行
for oldCode := range legacyMap {
_, _ = s.ctx.EntClient.Permission.Delete().
Where(permission.Code(oldCode)).Exec(ctx)
}
}
// migrateLegacyRoute 清理已删除的「工艺路线」模块残留(幂等,可重复执行):
// 产线定义已简化为「关联工位(station.flow_id) + 工位号 1→12 顺序 + 工单工位组合」,
// 工艺路线/路线段整块删除,故需从存量角色的 permissionCodes 中摘除路线权限码,
// 并删除对应的菜单/按钮权限定义行,避免菜单树里留下失效入口。
func migrateLegacyRoute(ctx context.Context, s *Service) {
legacyCodes := map[string]bool{
"produce.route": true,
"produce.route:add": true,
"produce.route:edit": true,
"produce.route:delete": true,
}
// 1) 角色 permissionCodes 剔除
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
for _, rl := range roleList {
kept := make([]string, 0, len(rl.PermissionCodes))
changed := false
for _, c := range rl.PermissionCodes {
if legacyCodes[c] {
changed = true
continue
}
kept = append(kept, c)
}
if changed {
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
SetPermissionCodes(mergeCodes(kept, nil)).Exec(ctx)
}
}
// 2) 删除权限定义行
for code := range legacyCodes {
_, _ = s.ctx.EntClient.Permission.Delete().
Where(permission.Code(code)).Exec(ctx)
}
}
// migrateLegacyQtyReport 清理已下线的旧「数量不符上报」菜单码 produce.qtyreport(幂等,可重复执行):
// 该码已被 produce.qty(数量不符处理,2026-09-19 归属 MES)替代 —— 只摘旧码,
// 不动新码 produce.qty / produce.qty:handle。
// 从存量角色的 permissionCodes 摘除 produce.qtyreport 并删除其权限定义行。
func migrateLegacyQtyReport(ctx context.Context, s *Service) {
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
for _, rl := range roleList {
kept := make([]string, 0, len(rl.PermissionCodes))
changed := false
for _, c := range rl.PermissionCodes {
if c == "produce.qtyreport" {
changed = true
continue
}
kept = append(kept, c)
}
if changed {
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
SetPermissionCodes(mergeCodes(kept, nil)).Exec(ctx)
}
}
_, _ = s.ctx.EntClient.Permission.Delete().
Where(permission.Code("produce.qtyreport")).Exec(ctx)
}