feat: 完成MES工位终端系统全量功能迭代
本次提交包含以下核心变更: 1. 新增按钮级权限控制框架与前端权限校验 2. 新增工位管理、工艺流程、巡检终端等核心业务模块 3. 完善用户体系,支持工位终端专属登录权限 4. 新增文件上传下载、报表查询等配套功能 5. 修复多系统兼容性与交互体验问题 6. 完成所有文档与配置项的规范化更新
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user