feat: 完成MES工位终端系统全量功能迭代

本次提交包含以下核心变更:
1. 新增按钮级权限控制框架与前端权限校验
2. 新增工位管理、工艺流程、巡检终端等核心业务模块
3. 完善用户体系,支持工位终端专属登录权限
4. 新增文件上传下载、报表查询等配套功能
5. 修复多系统兼容性与交互体验问题
6. 完成所有文档与配置项的规范化更新
This commit is contained in:
SunYF
2026-08-31 08:06:55 +08:00
parent dab03ac0cf
commit 4ec6784629
99 changed files with 17436 additions and 479 deletions
+205 -12
View File
@@ -10,6 +10,7 @@ import (
"bj_power_mes/ent/permission"
"bj_power_mes/ent/role"
"bj_power_mes/ent/user"
"bj_power_mes/ent/userstation"
"golang.org/x/crypto/bcrypt"
)
@@ -181,15 +182,118 @@ func (s *Service) ChangePassword(ctx context.Context, userId int, req ChangePass
return s.ctx.EntClient.User.UpdateOneID(userId).SetPassword(string(hash)).Exec(ctx)
}
// ------ 工位终端登录(块1:用户体系来源于 MES,终端登录改调本接口) ------
type StationLoginReq struct {
Username string `json:"username"`
Password string `json:"password"`
StationNo int `json:"stationNo"`
}
type StationLoginResp struct {
AccessToken string `json:"accessToken"`
ExpireAt int64 `json:"expireAt"`
User UserInfoResp `json:"user"`
Stations []int `json:"stations"`
StationNo int `json:"stationNo"`
}
// StationLogin 工位终端登录:校验 用户名 + 工位终端密码 + 工位权限
func (s *Service) StationLogin(ctx context.Context, req StationLoginReq) (StationLoginResp, error) {
var resp StationLoginResp
if req.Username == "" || req.Password == "" {
return resp, errors.New("用户名和密码必填")
}
usr, err := s.ctx.EntClient.User.Query().Where(user.Username(req.Username)).First(ctx)
if err != nil {
return resp, errors.New("用户名或密码错误")
}
if usr.Status != "ENABLED" {
return resp, errors.New("账号已禁用")
}
if !usr.CanLoginWorkstation {
return resp, errors.New("该账号未开通工位终端登录权限")
}
if usr.WorkstationPassword == "" || bcrypt.CompareHashAndPassword([]byte(usr.WorkstationPassword), []byte(req.Password)) != nil {
return resp, errors.New("用户名或密码错误")
}
stations := s.UserStations(ctx, usr.ID)
if len(stations) == 0 {
for i := 1; i <= 12; i++ {
stations = append(stations, i)
}
}
if req.StationNo > 0 {
allowed := false
for _, no := range stations {
if no == req.StationNo {
allowed = true
break
}
}
if !allowed {
return resp, errors.New("该账号无此工位的操作权限")
}
}
roleEntity, _ := s.ctx.EntClient.Role.Get(ctx, usr.RoleId)
claims := tokenx.Claims{UserId: usr.ID, Username: usr.Username, Name: usr.Name, RoleId: usr.RoleId, StationNo: req.StationNo}
if roleEntity != nil {
claims.RoleCode = roleEntity.Code
claims.RoleName = roleEntity.Name
}
access, err := tokenx.Issue(s.ctx.Config.Auth.AccessSecret, time.Duration(s.ctx.Config.Auth.AccessExpire)*time.Second, claims)
if err != nil {
return resp, err
}
resp.AccessToken = access
resp.ExpireAt = time.Now().Add(time.Duration(s.ctx.Config.Auth.AccessExpire) * time.Second).Unix()
resp.User = UserInfoResp{UserId: usr.ID, Username: usr.Username, Name: usr.Name, RoleId: usr.RoleId}
if roleEntity != nil {
resp.User.RoleCode = roleEntity.Code
resp.User.RoleName = roleEntity.Name
}
resp.Stations = stations
resp.StationNo = req.StationNo
return resp, nil
}
// StationChangePassword 工位终端修改自己的工位终端密码
func (s *Service) StationChangePassword(ctx context.Context, userId int, oldPwd, newPwd string) error {
if userId <= 0 {
return errors.New("未登录")
}
if oldPwd == "" || newPwd == "" {
return errors.New("请输入原密码与新密码")
}
if err := checkPasswordStrong(newPwd); err != nil {
return err
}
usr, err := s.ctx.EntClient.User.Get(ctx, userId)
if err != nil {
return errors.New("用户不存在")
}
if !usr.CanLoginWorkstation {
return errors.New("该账号未开通工位终端登录权限")
}
if bcrypt.CompareHashAndPassword([]byte(usr.WorkstationPassword), []byte(oldPwd)) != nil {
return errors.New("原工位终端密码不正确")
}
hash, _ := bcrypt.GenerateFromPassword([]byte(newPwd), bcrypt.DefaultCost)
return s.ctx.EntClient.User.UpdateOneID(userId).SetWorkstationPassword(string(hash)).Exec(ctx)
}
// ------ 用户 / 角色 / 权限 管理 ------
type UserReq struct {
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
RoleId int `json:"roleId"`
Status string `json:"status"`
Id int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Name string `json:"name"`
RoleId int `json:"roleId"`
Status string `json:"status"`
CanLoginWorkstation bool `json:"canLoginWorkstation"`
WorkstationPassword string `json:"workstationPassword"`
Stations []int `json:"stations"` // 允许操作的工位号列表
}
// checkPasswordStrong 校验密码:长度>=6 且不能是纯数字
@@ -222,17 +326,37 @@ func (s *Service) CreateUser(ctx context.Context, req UserReq) error {
if status == "" {
status = "ENABLED"
}
return s.ctx.EntClient.User.Create().
create := s.ctx.EntClient.User.Create().
SetUsername(req.Username).
SetPassword(string(hash)).
SetName(req.Name).
SetRoleId(req.RoleId).
SetStatus(status).
Exec(ctx)
SetCanLoginWorkstation(req.CanLoginWorkstation)
if req.CanLoginWorkstation {
wsPwd := req.WorkstationPassword
if wsPwd == "" {
wsPwd = req.Password // 默认复用登录密码
}
if err := checkPasswordStrong(wsPwd); err != nil {
return errors.New("工位终端密码" + err.Error())
}
wsHash, err := bcrypt.GenerateFromPassword([]byte(wsPwd), bcrypt.DefaultCost)
if err != nil {
return err
}
create.SetWorkstationPassword(string(wsHash))
}
usr, err := create.Save(ctx)
if err != nil {
return err
}
return s.replaceUserStations(ctx, usr.ID, req.Stations)
}
func (s *Service) UpdateUser(ctx context.Context, req UserReq) error {
u := s.ctx.EntClient.User.UpdateOneID(req.Id).SetRoleId(req.RoleId).SetName(req.Name)
u := s.ctx.EntClient.User.UpdateOneID(req.Id).SetRoleId(req.RoleId).SetName(req.Name).
SetCanLoginWorkstation(req.CanLoginWorkstation)
if req.Password != "" {
if err := checkPasswordStrong(req.Password); err != nil {
return err
@@ -240,10 +364,52 @@ func (s *Service) UpdateUser(ctx context.Context, req UserReq) error {
hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
u.SetPassword(string(hash))
}
if req.CanLoginWorkstation {
if req.WorkstationPassword != "" {
if err := checkPasswordStrong(req.WorkstationPassword); err != nil {
return errors.New("工位终端密码" + err.Error())
}
wsHash, _ := bcrypt.GenerateFromPassword([]byte(req.WorkstationPassword), bcrypt.DefaultCost)
u.SetWorkstationPassword(string(wsHash))
}
} else {
u.SetWorkstationPassword("")
}
if req.Status != "" {
u.SetStatus(req.Status)
}
return u.Exec(ctx)
if err := u.Exec(ctx); err != nil {
return err
}
return s.replaceUserStations(ctx, req.Id, req.Stations)
}
// replaceUserStations 全量替换用户-工位关联
func (s *Service) replaceUserStations(ctx context.Context, userId int, stations []int) error {
if _, err := s.ctx.EntClient.UserStation.Delete().Where(userstation.UserId(userId)).Exec(ctx); err != nil {
return err
}
for _, no := range stations {
if no <= 0 {
continue
}
if err := s.ctx.EntClient.UserStation.Create().
SetUserId(userId).SetStationNo(no).Exec(ctx); err != nil {
return err
}
}
return nil
}
// UserStations 查询用户绑定的工位号
func (s *Service) UserStations(ctx context.Context, userId int) []int {
rows, _ := s.ctx.EntClient.UserStation.Query().
Where(userstation.UserId(userId)).All(ctx)
out := make([]int, 0, len(rows))
for _, r := range rows {
out = append(out, r.StationNo)
}
return out
}
func (s *Service) DeleteUser(ctx context.Context, id int) error {
@@ -253,8 +419,35 @@ func (s *Service) DeleteUser(ctx context.Context, id int) error {
return s.ctx.EntClient.User.DeleteOneID(id).Exec(ctx)
}
func (s *Service) ListUsers(ctx context.Context) ([]*ent.User, error) {
return s.ctx.EntClient.User.Query().Order(ent.Asc(user.FieldID)).All(ctx)
// UserVO 用户列表返回体(含工位终端属性与允许操作工位)
type UserVO struct {
Id int `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
RoleId int `json:"roleId"`
Status string `json:"status"`
CanLoginWorkstation bool `json:"canLoginWorkstation"`
Stations []int `json:"stations"` // 允许登录/操作的工位号(空=全部工位)
}
func (s *Service) ListUsers(ctx context.Context) ([]UserVO, error) {
usrs, err := s.ctx.EntClient.User.Query().Order(ent.Asc(user.FieldID)).All(ctx)
if err != nil {
return nil, err
}
out := make([]UserVO, 0, len(usrs))
for _, u := range usrs {
out = append(out, UserVO{
Id: u.ID,
Username: u.Username,
Name: u.Name,
RoleId: u.RoleId,
Status: u.Status,
CanLoginWorkstation: u.CanLoginWorkstation,
Stations: s.UserStations(ctx, u.ID),
})
}
return out, nil
}
type RoleReq struct {
+109
View File
@@ -0,0 +1,109 @@
package logic
import (
"context"
"encoding/json"
"errors"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/inspectionrecord"
)
// InspectionReq 巡检记录请求(PAD 巡检终端,块8)
type InspectionReq struct {
Category string `json:"category"` // CHECKIN/POINT/PROCESS/DONE/ALARM
StationNo string `json:"stationNo"`
Shift string `json:"shift"` // 早/中/晚
OrderNo string `json:"orderNo"` // 工单号
Sn string `json:"sn"` // 工件SN
Result string `json:"result"` // OK/NG
Items []string `json:"items"` // 点检项/异常类型
Photo string `json:"photo"` // 拍照文件名
Remark string `json:"remark"` // 文字描述/签字
Operator string `json:"operator"` // 操作人
}
// CreateInspection 提交巡检记录;ALARM 类型同时触发看板报警(SSE + 清缓存)
func (s *Service) CreateInspection(ctx context.Context, req InspectionReq, operator string) error {
if req.Category == "" {
return errors.New("记录类型必填")
}
category := req.Category
if !oneOf(category, "CHECKIN", "POINT", "PROCESS", "DONE", "ALARM") {
return errors.New("记录类型不合法")
}
result := req.Result
if result == "" {
result = "OK"
}
if req.Operator == "" {
req.Operator = operator
}
_, err := s.ctx.EntClient.InspectionRecord.Create().
SetCategory(category).
SetStationNo(req.StationNo).
SetShift(req.Shift).
SetOrderNo(req.OrderNo).
SetSn(req.Sn).
SetResult(result).
SetItems(req.Items).
SetPhoto(req.Photo).
SetRemark(req.Remark).
SetOperator(req.Operator).
Save(ctx)
if err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "inspection."+category, req.OrderNo, req.Operator, "inspection_record",
req.Sn, "巡检终端提交", map[string]any{"category": category, "stationNo": req.StationNo, "result": result})
if category == "ALARM" {
// 触发看板报警:SSE 推送 + 使 Redis 看板报警缓存失效
alarmJSON, _ := json.Marshal(map[string]any{
"level": "CRITICAL",
"type": "inspection_alarm",
"message": "巡检异常上报:" + req.Remark,
"station": req.StationNo,
"time": time.Now().Format("2006-01-02 15:04:05"),
})
if s.ctx.SSE != nil {
s.ctx.SSE.Publish("dashboard.alarm", string(alarmJSON))
}
if s.ctx.RedisClient != nil {
_, _ = s.ctx.RedisClient.Del("dashboard:alarms")
}
}
return nil
}
// ListInspections 查询巡检记录
func (s *Service) ListInspections(ctx context.Context, category, operator, from, to string) ([]*ent.InspectionRecord, error) {
q := s.ctx.EntClient.InspectionRecord.Query().Order(ent.Desc(inspectionrecord.FieldID))
if category != "" {
q = q.Where(inspectionrecord.Category(category))
}
if operator != "" {
q = q.Where(inspectionrecord.Operator(operator))
}
if from != "" {
if f, err := time.Parse("2006-01-02", from); err == nil {
q = q.Where(inspectionrecord.CreatedAtGTE(f))
}
}
if to != "" {
if t, err := time.Parse("2006-01-02", to); err == nil {
q = q.Where(inspectionrecord.CreatedAtLT(t.Add(24 * time.Hour)))
}
}
return q.Limit(1000).All(ctx)
}
func oneOf(v string, items ...string) bool {
for _, it := range items {
if v == it {
return true
}
}
return false
}
+324
View File
@@ -0,0 +1,324 @@
package logic
import (
"context"
"errors"
"sort"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/processflow"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stepcriterion"
"bj_power_mes/ent/workorder"
"bj_power_mes/ent/workpieceprocess"
)
// ---------- 工艺流程(块3 ----------
type FlowReq struct {
Id int `json:"id"`
Name string `json:"name"`
ProcessCode int `json:"processCode"`
PdfFile string `json:"pdfFile"`
Status string `json:"status"`
Remark string `json:"remark"`
Steps []StepItemReq `json:"steps"`
}
// FlowVO 流程图视图(含步骤模板 + 绑定该流程的工位号)
type FlowVO struct {
Id int `json:"id"`
Name string `json:"name"`
ProcessCode int `json:"processCode"`
PdfFile string `json:"pdfFile"`
Status string `json:"status"`
Remark string `json:"remark"`
Stations []int `json:"stations"`
Steps []*ProcessStepTemplate `json:"steps"`
CreatedAt time.Time `json:"createdAt"`
}
func (s *Service) ListFlows(ctx context.Context, processCode int) ([]*FlowVO, error) {
q := s.ctx.EntClient.ProcessFlow.Query().
Order(ent.Asc(processflow.FieldProcessCode), ent.Asc(processflow.FieldID))
if processCode > 0 {
q = q.Where(processflow.ProcessCode(processCode))
}
flows, err := q.All(ctx)
if err != nil {
return nil, err
}
stations, _ := s.ctx.EntClient.Station.Query().All(ctx)
out := make([]*FlowVO, 0, len(flows))
for _, f := range flows {
vo := &FlowVO{
Id: f.ID, Name: f.Name, ProcessCode: f.ProcessCode,
PdfFile: f.PdfFile, Status: f.Status, Remark: f.Remark,
Stations: []int{}, Steps: s.flowSteps(ctx, f.ID, f.ProcessCode), CreatedAt: f.CreatedAt,
}
for _, st := range stations {
if st.FlowId == f.ID {
vo.Stations = append(vo.Stations, st.StationNo)
}
}
out = append(out, vo)
}
return out, nil
}
func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) error {
if req.Name == "" || req.ProcessCode <= 0 {
return errors.New("流程名称与工序编号必填")
}
status := req.Status
if status == "" {
status = "ACTIVE"
}
var flowID int
if req.Id > 0 {
upd := s.ctx.EntClient.ProcessFlow.UpdateOneID(req.Id).
SetName(req.Name).SetProcessCode(req.ProcessCode).SetStatus(status).SetRemark(req.Remark)
if req.PdfFile != "" {
upd.SetPdfFile(req.PdfFile)
}
if _, err := upd.Save(ctx); err != nil {
return err
}
flowID = req.Id
} else {
f, err := s.ctx.EntClient.ProcessFlow.Create().
SetName(req.Name).SetProcessCode(req.ProcessCode).SetPdfFile(req.PdfFile).
SetStatus(status).SetRemark(req.Remark).Save(ctx)
if err != nil {
return err
}
flowID = f.ID
}
// 覆盖式重建该流程的步骤模板及考核标准
_, _ = s.ctx.EntClient.ProcessStep.Delete().Where(processstep.FlowId(flowID)).Exec(ctx)
for _, st := range req.Steps {
if st.Name == "" {
continue
}
rec, err := s.ctx.EntClient.ProcessStep.Create().
SetFlowId(flowID).SetProcessCode(req.ProcessCode).SetSeq(st.Seq).SetName(st.Name).
SetCollectType(st.CollectType).SetIsTorque(st.IsTorque).SetRemark(st.Remark).Save(ctx)
if err != nil {
return err
}
_, _ = s.ctx.EntClient.StepCriterion.Delete().Where(stepcriterion.StepId(rec.ID)).Exec(ctx)
for _, c := range st.Criteria {
if c.Name == "" || c.Logic == "" || c.Logic == "NONE" {
continue
}
b := s.ctx.EntClient.StepCriterion.Create().
SetStepId(rec.ID).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.flow.save", "", operator, "process_flow", "", "维护工艺流程",
map[string]any{"flowId": flowID, "processCode": req.ProcessCode, "steps": len(req.Steps)})
return nil
}
func (s *Service) DeleteFlow(ctx context.Context, id int, operator string) error {
// 有工位绑定的流程不允许删除
cnt, err := s.ctx.EntClient.Station.Query().Where(station.FlowId(id)).Count(ctx)
if err == nil && cnt > 0 {
return errors.New("该流程已被工位绑定,请先解绑工位")
}
_, _ = s.ctx.EntClient.ProcessStep.Delete().Where(processstep.FlowId(id)).Exec(ctx)
s.ctx.EventLog.Write(ctx, "process.flow.delete", "", operator, "process_flow", "", "删除工艺流程", map[string]any{"flowId": id})
return s.ctx.EntClient.ProcessFlow.DeleteOneID(id).Exec(ctx)
}
// flowSteps 查询某流程的步骤模板(含考核标准)
func (s *Service) flowSteps(ctx context.Context, flowId, processCode int) []*ProcessStepTemplate {
steps, _ := s.ctx.EntClient.ProcessStep.Query().
Where(processstep.FlowId(flowId)).Order(ent.Asc(processstep.FieldSeq)).All(ctx)
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
}
// ---------- 工位主数据(块3 ----------
type StationReq struct {
Id int `json:"id"`
StationNo int `json:"stationNo"`
Name string `json:"name"`
FlowId int `json:"flowId"`
}
type StationVO struct {
Id int `json:"id"`
StationNo int `json:"stationNo"`
Name string `json:"name"`
FlowId int `json:"flowId"`
FlowName string `json:"flowName"`
ProcessCode int `json:"processCode"`
Status string `json:"status"`
}
func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
stas, err := s.ctx.EntClient.Station.Query().Order(ent.Asc(station.FieldStationNo)).All(ctx)
if err != nil {
return nil, err
}
flows, _ := s.ctx.EntClient.ProcessFlow.Query().All(ctx)
flowMap := map[int]*ent.ProcessFlow{}
for _, f := range flows {
flowMap[f.ID] = f
}
out := make([]*StationVO, 0, len(stas))
for _, st := range stas {
vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, Status: st.Status}
if f, ok := flowMap[st.FlowId]; ok {
vo.FlowName = f.Name
vo.ProcessCode = f.ProcessCode
}
out = append(out, vo)
}
return out, nil
}
func (s *Service) SaveStation(ctx context.Context, req StationReq, operator string) error {
if req.StationNo <= 0 || req.StationNo > 12 {
return errors.New("工位号必须在 1~12")
}
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(req.StationNo)).First(ctx)
if err != nil {
if _, err := s.ctx.EntClient.Station.Create().
SetStationNo(req.StationNo).SetName(req.Name).SetFlowId(req.FlowId).
SetStatus("ENABLED").Save(ctx); err != nil {
return err
}
} else {
if _, err := s.ctx.EntClient.Station.UpdateOneID(st.ID).
SetName(req.Name).SetFlowId(req.FlowId).Save(ctx); err != nil {
return err
}
}
s.ctx.EventLog.Write(ctx, "station.save", "", operator, "station", "", "维护工位绑定",
map[string]any{"stationNo": req.StationNo, "flowId": req.FlowId})
return nil
}
// ---------- 工位任务(块4/5:工位终端拉取) ----------
func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]any, error) {
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx)
if err != nil {
return nil, errors.New("工位不存在或未配置")
}
var flow *ent.ProcessFlow
if st.FlowId > 0 {
flow, _ = s.ctx.EntClient.ProcessFlow.Get(ctx, st.FlowId)
}
steps := []*ProcessStepTemplate{}
if flow != nil {
steps = s.flowSteps(ctx, flow.ID, flow.ProcessCode)
}
orderNos := []string{}
wos, _ := s.ctx.EntClient.WorkOrder.Query().
Where(workorder.StatusIn("CREATED", "RELEASED", "IN_PROGRESS")).
Order(ent.Asc(workorder.FieldID)).Limit(10).All(ctx)
for _, w := range wos {
orderNos = append(orderNos, w.WorkOrderNo)
}
return map[string]any{
"stationNo": st.StationNo,
"stationName": st.Name,
"flow": flow,
"steps": steps,
"orderNos": orderNos,
}, nil
}
// ---------- 工作量/绩效报表(块6 ----------
type WorkloadRow struct {
Operator string `json:"operator"`
Date string `json:"date"`
ProcessCode int `json:"processCode"`
ProcessName string `json:"processName"`
DoneCount int `json:"doneCount"`
OkCount int `json:"okCount"`
NgCount int `json:"ngCount"`
}
func (s *Service) Workload(ctx context.Context, operator, from, to string) (map[string]any, error) {
q := s.ctx.EntClient.WorkpieceProcess.Query()
if operator != "" {
q = q.Where(workpieceprocess.Operator(operator))
}
if from != "" {
if f, err := time.Parse("2006-01-02", from); err == nil {
q = q.Where(workpieceprocess.CreatedAtGTE(f))
}
}
if to != "" {
if t, err := time.Parse("2006-01-02", to); err == nil {
q = q.Where(workpieceprocess.CreatedAtLT(t.Add(24 * time.Hour)))
}
}
rows, err := q.All(ctx)
if err != nil {
return nil, err
}
type key struct{ op, date string; code int }
agg := map[key]*WorkloadRow{}
for _, r := range rows {
k := key{r.Operator, r.CreatedAt.Format("2006-01-02"), r.ProcessCode}
row, ok := agg[k]
if !ok {
row = &WorkloadRow{Operator: r.Operator, Date: k.date, ProcessCode: r.ProcessCode, ProcessName: r.ProcessName}
agg[k] = row
}
row.DoneCount++
if r.Result == "OK" {
row.OkCount++
} else {
row.NgCount++
}
}
list := make([]*WorkloadRow, 0, len(agg))
for _, v := range agg {
list = append(list, v)
}
sort.Slice(list, func(i, j int) bool {
if list[i].Operator != list[j].Operator {
return list[i].Operator < list[j].Operator
}
if list[i].Date != list[j].Date {
return list[i].Date < list[j].Date
}
return list[i].ProcessCode < list[j].ProcessCode
})
return map[string]any{"rows": list, "detail": rows}, nil
}
+112 -19
View File
@@ -2,10 +2,13 @@ package logic
import (
"context"
"strconv"
"bj_power_mes/ent/permission"
"bj_power_mes/ent/processflow"
"bj_power_mes/ent/processstep"
"bj_power_mes/ent/role"
"bj_power_mes/ent/station"
"bj_power_mes/ent/stationprocess"
"bj_power_mes/ent/user"
@@ -14,23 +17,37 @@ import (
// Seed 初始化:超级管理员、默认角色、默认菜单权限、12 道工序与工位派工
func (s *Service) Seed(ctx context.Context) error {
// 1. 默认角色
// 1. 默认角色(已存在则合并权限码,保证新按钮权限能落到已有角色上)
roles := []struct{ name, code string }{
{"超级管理员", "SUPER_ADMIN"},
{"生产操作员", "OPERATOR"},
{"质检员", "INSPECTOR"},
}
for _, r := range roles {
if s.ctx.EntClient.Role.Query().Where(role.Code(r.code)).ExistX(ctx) {
continue
}
codes := []string{"*"}
if r.code == "OPERATOR" {
codes = []string{"produce.workorder", "produce.bom", "produce.material", "produce.scan", "produce.trace", "produce.torque", "produce.plc", "produce.product"}
codes = []string{
"produce.workorder", "produce.dailyplan", "produce.bom", "produce.material", "produce.scan",
"produce.trace", "produce.torque", "produce.plc", "produce.product", "produce.step",
"produce.processflow", "produce.station", "produce.performance",
// 按钮级权限(块2
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
"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",
}
} else if r.code == "INSPECTOR" {
codes = []string{"produce.trace", "produce.torque", "sys.eventlog"}
codes = []string{"produce.trace", "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"}
}
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)
}
_ = s.ctx.EntClient.Role.Create().SetName(r.name).SetCode(r.code).SetPermissionCodes(codes).Exec(ctx)
}
// 2. 超级管理员账号 admin / 123456(对已存在的旧行做对齐,避免残留禁用状态)
@@ -53,13 +70,19 @@ func (s *Service) Seed(ctx context.Context) error {
// 3. 默认菜单权限
menus := []struct{ code, name, typ, path string }{
{"produce.workorder", "工单管理", "MENU", "/work-order"},
{"produce.bom", "BOM", "MENU", "/bom"},
{"produce.dailyplan", "日排产", "MENU", "/daily-plan"},
{"produce.bom", "物料清单", "MENU", "/bom"},
{"produce.material", "备料单", "MENU", "/material-request"},
{"produce.plc", "PLC工序下发", "MENU", "/plc-send"},
{"produce.torque", "拧紧查询", "MENU", "/torque"},
{"produce.step", "工艺参数", "MENU", "/process-step"},
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
{"produce.station", "工位管理", "MENU", "/station"},
{"produce.performance", "绩效报表", "MENU", "/performance"},
{"produce.scan", "扫码报工", "MENU", "/scan"},
{"produce.trace", "工件追溯", "MENU", "/trace"},
{"produce.product", "产品类型", "MENU", "/product-type"},
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
{"sys.eventlog", "操作日志", "MENU", "/event-log"},
{"sys.rbac", "角色权限管理", "MENU", "/rbac"},
}
@@ -71,8 +94,43 @@ func (s *Service) Seed(ctx context.Context) error {
SetCode(m.code).SetName(m.name).SetType(m.typ).SetPath(m.path).Exec(ctx)
}
// 4. 12 道工序步骤模板(每道工序一个"装配完成"步骤 + 拧紧台阶步骤)
seedProcessSteps(ctx, s)
// 3.1 按钮级权限(块2):前端按钮 + 后端接口 双重校验
buttons := []struct{ code, name, typ, path string }{
{"produce.workorder:add", "工单-新增", "BUTTON", ""},
{"produce.workorder:edit", "工单-编辑", "BUTTON", ""},
{"produce.workorder:delete", "工单-删除", "BUTTON", ""},
{"produce.workorder:dailyplan", "工单-日排产", "BUTTON", ""},
{"produce.bom:edit", "物料清单-维护", "BUTTON", ""},
{"produce.material:generate", "备料单-生成", "BUTTON", ""},
{"produce.plc:send", "PLC工序-下发", "BUTTON", ""},
{"produce.product:add", "产品类型-新增", "BUTTON", ""},
{"produce.product:edit", "产品类型-编辑", "BUTTON", ""},
{"produce.product:delete", "产品类型-删除", "BUTTON", ""},
{"produce.processflow:add", "工艺流程-新增", "BUTTON", ""},
{"produce.processflow:edit", "工艺流程-编辑", "BUTTON", ""},
{"produce.processflow:delete", "工艺流程-删除", "BUTTON", ""},
{"produce.processflow:upload", "工艺流程-上传图纸", "BUTTON", ""},
{"produce.station:edit", "工位-绑定", "BUTTON", ""},
{"sys.rbac:user", "权限-用户", "BUTTON", ""},
{"sys.rbac:role", "权限-角色", "BUTTON", ""},
{"sys.rbac:perm", "权限-权限码", "BUTTON", ""},
{"sys.inspect:checkin", "巡检-开工签到", "BUTTON", ""},
{"sys.inspect:point", "巡检-工位点检", "BUTTON", ""},
{"sys.inspect:process", "巡检-过程巡检", "BUTTON", ""},
{"sys.inspect:done", "巡检-完工确认", "BUTTON", ""},
{"sys.inspect:alarm", "巡检-异常上报", "BUTTON", ""},
{"sys.inspect:view", "巡检-记录查看", "BUTTON", ""},
}
for _, b := range buttons {
if s.ctx.EntClient.Permission.Query().Where(permission.Code(b.code)).ExistX(ctx) {
continue
}
_ = s.ctx.EntClient.Permission.Create().
SetCode(b.code).SetName(b.name).SetType(b.typ).SetPath(b.path).Exec(ctx)
}
// 4. 12 道工序默认工艺流程 + 工位绑定(流程挂 process_code,工位绑流程)
seedFlowsAndStations(ctx, s)
// 5. 12 工位 ↔ 工序 派工(工位N 默认做工序N)
for i := 1; i <= 12; i++ {
if s.ctx.EntClient.StationProcess.Query().Where(stationprocess.StationNo(i), stationprocess.ProcessCode(i)).ExistX(ctx) {
@@ -84,17 +142,52 @@ func (s *Service) Seed(ctx context.Context) error {
return nil
}
func seedProcessSteps(ctx context.Context, s *Service) {
func seedFlowsAndStations(ctx context.Context, s *Service) {
// 每个工序编号(1~12)创建默认工艺流程,并为每道工序补默认步骤模板(挂到流程下)
for i := 1; i <= 12; i++ {
cnt, _ := s.ctx.EntClient.ProcessStep.Query().Where(processstep.ProcessCode(i)).Count(ctx)
if cnt > 0 {
continue
flow, err := s.ctx.EntClient.ProcessFlow.Query().
Where(processflow.ProcessCode(i), processflow.Name("工序"+strconv.Itoa(i)+"_默认")).First(ctx)
if err != nil {
flow, _ = s.ctx.EntClient.ProcessFlow.Create().
SetName("工序" + strconv.Itoa(i) + "_默认").
SetProcessCode(i).SetStatus("ACTIVE").SetRemark("默认工艺流程").Save(ctx)
}
_ = s.ctx.EntClient.ProcessStep.Create().
SetProcessCode(i).SetSeq(1).SetName("装配完成").SetCollectType("NONE").Exec(ctx)
if i == 3 { // 示例:工序3 增加一个拧紧采集步骤
_ = s.ctx.EntClient.ProcessStep.Create().
SetProcessCode(3).SetSeq(2).SetName("拧紧扭矩").SetCollectType("AUTO").SetIsTorque(true).Exec(ctx)
cnt, _ := s.ctx.EntClient.ProcessStep.Query().Where(processstep.FlowId(flow.ID)).Count(ctx)
if cnt == 0 {
stepID := 0
if step, err := s.ctx.EntClient.ProcessStep.Create().
SetFlowId(flow.ID).SetProcessCode(i).SetSeq(1).SetName("装配完成").
SetCollectType("NONE").Save(ctx); err == nil {
stepID = step.ID
}
if i == 3 && stepID > 0 {
_, _ = s.ctx.EntClient.ProcessStep.Create().
SetFlowId(flow.ID).SetProcessCode(i).SetSeq(2).SetName("拧紧扭矩").
SetCollectType("AUTO").SetIsTorque(true).Save(ctx)
}
}
// 工位 N 绑定工序 N 的默认流程
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx)
if err != nil {
_, _ = s.ctx.EntClient.Station.Create().
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).
SetFlowId(flow.ID).SetStatus("ENABLED").Save(ctx)
} else if st.FlowId == 0 {
_, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flow.ID).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
}