feat: 完成产线MES系统全流程功能迭代与优化
本提交完成了多个核心模块的功能完善与业务对齐: 1. 工位终端:固定工位配置、移除自选工位逻辑、适配配置化工位号 2. MES核心:重构工位组合逻辑、新增工单/流程状态管理、补全报工/追溯逻辑 3. WMS客户端:新增修改密码功能、优化帮助弹窗逻辑 4. 文档与权限:补充完整操作手册、统一菜单名称与权限描述 5. 修复多业务校验:排产数量校验、工单状态校验、工位绑定规则
This commit is contained in:
@@ -37,15 +37,21 @@ func (s *Service) SaveDailyPlan(ctx context.Context, req DailyPlanReq, operator
|
||||
if err != nil {
|
||||
return errors.New("工单不存在")
|
||||
}
|
||||
if wo.Status == "DONE" || wo.Status == "CLOSED" {
|
||||
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" {
|
||||
// 覆盖同一 工单+日期 时,旧数量换掉
|
||||
if p.PlanDate == req.PlanDate {
|
||||
if p.CompletedQty > 0 {
|
||||
return errors.New("该排产日已有完工数量,不能修改计划数量")
|
||||
}
|
||||
total += req.PlanQty
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func (s *Service) DashboardAlarms(ctx context.Context) (any, error) {
|
||||
alarms = append(alarms, map[string]any{"level": "WARN", "type": "torque_ng", "message": "存在拧紧NG记录", "count": ngTorque})
|
||||
}
|
||||
if openPlc > 0 {
|
||||
alarms = append(alarms, map[string]any{"level": "INFO", "type": "plc_pending", "message": "存在待完成PLC工序", "count": openPlc})
|
||||
alarms = append(alarms, map[string]any{"level": "INFO", "type": "plc_pending", "message": "存在待完成工位组合", "count": openPlc})
|
||||
}
|
||||
if plcTimeout > 0 {
|
||||
alarms = append(alarms, map[string]any{"level": "CRITICAL", "type": "plc_timeout", "message": "存在PLC下发超时", "count": plcTimeout})
|
||||
|
||||
@@ -20,7 +20,7 @@ type PlcSendReq struct {
|
||||
// 约束:上一条工序未收到完成信号,不下发下一条(模拟 S7 握手)。
|
||||
func (s *Service) SendProcess(ctx context.Context, req PlcSendReq, operator string) (*ent.PlcSendLog, error) {
|
||||
if req.ProcessCombination == "" {
|
||||
return nil, errors.New("工序组合不能为空")
|
||||
return nil, errors.New("工位组合不能为空")
|
||||
}
|
||||
// 检查是否存在未完成的同工位下发
|
||||
last, err := s.ctx.EntClient.PlcSendLog.Query().
|
||||
@@ -37,7 +37,7 @@ func (s *Service) SendProcess(ctx context.Context, req PlcSendReq, operator stri
|
||||
now := time.Now()
|
||||
rec, err := s.ctx.EntClient.PlcSendLog.Create().
|
||||
SetOrderNo(req.OrderNo).SetSn(req.Sn).SetStationNo(req.StationNo).
|
||||
SetProcessCombination(req.ProcessCombination).SetProcessCodes(parseCodes(req.ProcessCombination)).
|
||||
SetProcessCombination(req.ProcessCombination).SetProcessCodes(ParseProcessSeq(req.ProcessCombination)).
|
||||
SetStatus("SENT").SetSendTime(now).SetOperator(operator).Save(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -47,7 +47,7 @@ func (s *Service) SendProcess(ctx context.Context, req PlcSendReq, operator stri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "plc.send", req.OrderNo, operator, "plc_send_log", req.Sn, "PLC工序码下发", map[string]any{"combination": req.ProcessCombination})
|
||||
s.ctx.EventLog.Write(ctx, "plc.send", req.OrderNo, operator, "plc_send_log", req.Sn, "工位组合下发", map[string]any{"combination": req.ProcessCombination})
|
||||
|
||||
// mock 完成信号:延时后置为完成(演示"未完成不下发下一条"握手)
|
||||
go func(id int) {
|
||||
@@ -60,19 +60,6 @@ func (s *Service) SendProcess(ctx context.Context, req PlcSendReq, operator stri
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
func parseCodes(combination string) []int {
|
||||
var out []int
|
||||
for _, ch := range combination {
|
||||
if ch >= '1' && ch <= '9' {
|
||||
out = append(out, int(ch-'0'))
|
||||
} else if ch == '1' || ch == '0' {
|
||||
// 允许 "10","11","12" 两位工序
|
||||
continue
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ListPlcSendLogs 查询 PLC 下发日志
|
||||
func (s *Service) ListPlcSendLogs(ctx context.Context, orderNo, status string) ([]*ent.PlcSendLog, error) {
|
||||
q := s.ctx.EntClient.PlcSendLog.Query()
|
||||
|
||||
@@ -3,6 +3,7 @@ package logic
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -18,33 +19,34 @@ import (
|
||||
// ---------- 工艺流程(块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"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StationNo int `json:"stationNo"`
|
||||
PdfFile string `json:"pdfFile"`
|
||||
Status string `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
Steps []StepItemReq `json:"steps"`
|
||||
}
|
||||
|
||||
// FlowVO 流程图视图(含步骤模板 + 绑定该流程的工位号)
|
||||
// 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"`
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StationNo int `json:"stationNo"`
|
||||
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) {
|
||||
// ListFlows 查询工艺流程(可按工位号过滤)
|
||||
func (s *Service) ListFlows(ctx context.Context, stationNo 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))
|
||||
if stationNo > 0 {
|
||||
q = q.Where(processflow.ProcessCode(stationNo))
|
||||
}
|
||||
flows, err := q.All(ctx)
|
||||
if err != nil {
|
||||
@@ -54,7 +56,7 @@ func (s *Service) ListFlows(ctx context.Context, processCode int) ([]*FlowVO, er
|
||||
out := make([]*FlowVO, 0, len(flows))
|
||||
for _, f := range flows {
|
||||
vo := &FlowVO{
|
||||
Id: f.ID, Name: f.Name, ProcessCode: f.ProcessCode,
|
||||
Id: f.ID, Name: f.Name, StationNo: f.ProcessCode,
|
||||
PdfFile: f.PdfFile, Status: f.Status, Remark: f.Remark,
|
||||
Stations: []int{}, Steps: s.flowSteps(ctx, f.ID, f.ProcessCode), CreatedAt: f.CreatedAt,
|
||||
}
|
||||
@@ -69,17 +71,36 @@ func (s *Service) ListFlows(ctx context.Context, processCode int) ([]*FlowVO, er
|
||||
}
|
||||
|
||||
func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) error {
|
||||
if req.Name == "" || req.ProcessCode <= 0 {
|
||||
return errors.New("流程名称与工序编号必填")
|
||||
if req.Name == "" {
|
||||
return errors.New("流程名称必填")
|
||||
}
|
||||
if req.StationNo < 1 || req.StationNo > 12 {
|
||||
return errors.New("请选择工位号(1~12,固定12个工位)")
|
||||
}
|
||||
status := req.Status
|
||||
if status == "" {
|
||||
status = "ACTIVE"
|
||||
}
|
||||
oldStationNo := 0
|
||||
if req.Id > 0 {
|
||||
if old, err := s.ctx.EntClient.ProcessFlow.Get(ctx, req.Id); err == nil {
|
||||
oldStationNo = old.ProcessCode
|
||||
}
|
||||
}
|
||||
if status == "ACTIVE" {
|
||||
q := s.ctx.EntClient.ProcessFlow.Query().
|
||||
Where(processflow.ProcessCode(req.StationNo), processflow.Status("ACTIVE"))
|
||||
if req.Id > 0 {
|
||||
q = q.Where(processflow.IDNEQ(req.Id))
|
||||
}
|
||||
if exist, err := q.Exist(ctx); err == nil && exist {
|
||||
return fmt.Errorf("工位 %d 已存在启用的工艺流程,请先停用旧流程", req.StationNo)
|
||||
}
|
||||
}
|
||||
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)
|
||||
SetName(req.Name).SetProcessCode(req.StationNo).SetStatus(status).SetRemark(req.Remark)
|
||||
if req.PdfFile != "" {
|
||||
upd.SetPdfFile(req.PdfFile)
|
||||
}
|
||||
@@ -89,21 +110,35 @@ func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) er
|
||||
flowID = req.Id
|
||||
} else {
|
||||
f, err := s.ctx.EntClient.ProcessFlow.Create().
|
||||
SetName(req.Name).SetProcessCode(req.ProcessCode).SetPdfFile(req.PdfFile).
|
||||
SetName(req.Name).SetProcessCode(req.StationNo).SetPdfFile(req.PdfFile).
|
||||
SetStatus(status).SetRemark(req.Remark).Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
flowID = f.ID
|
||||
}
|
||||
// 覆盖式重建该流程的步骤模板及考核标准
|
||||
// 工位号变更:解绑旧工位
|
||||
if oldStationNo > 0 && oldStationNo != req.StationNo {
|
||||
_, _ = s.ctx.EntClient.Station.Update().
|
||||
Where(station.StationNo(oldStationNo), station.FlowId(flowID)).
|
||||
SetFlowId(0).Save(ctx)
|
||||
}
|
||||
// 绑定同步:工位N 绑定该启用流程
|
||||
if status == "ACTIVE" {
|
||||
s.syncStationFlow(ctx, req.StationNo, flowID)
|
||||
} else {
|
||||
_, _ = s.ctx.EntClient.Station.Update().
|
||||
Where(station.StationNo(req.StationNo), station.FlowId(flowID)).
|
||||
SetFlowId(0).Save(ctx)
|
||||
}
|
||||
// 覆盖式重建该流程的工序步骤及考核标准
|
||||
_, _ = 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).
|
||||
SetFlowId(flowID).SetProcessCode(req.StationNo).SetSeq(st.Seq).SetName(st.Name).
|
||||
SetCollectType(st.CollectType).SetIsTorque(st.IsTorque).SetRemark(st.Remark).Save(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -127,10 +162,22 @@ func (s *Service) SaveFlow(ctx context.Context, req FlowReq, operator string) er
|
||||
}
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "process.flow.save", "", operator, "process_flow", "", "维护工艺流程",
|
||||
map[string]any{"flowId": flowID, "processCode": req.ProcessCode, "steps": len(req.Steps)})
|
||||
map[string]any{"flowId": flowID, "stationNo": req.StationNo, "steps": len(req.Steps)})
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncStationFlow 同步工位绑定:工位 stationNo 绑定到 flowID(无工位记录时自动创建)
|
||||
func (s *Service) syncStationFlow(ctx context.Context, stationNo, flowID int) {
|
||||
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(stationNo)).First(ctx)
|
||||
if err != nil {
|
||||
_, _ = s.ctx.EntClient.Station.Create().
|
||||
SetStationNo(stationNo).SetName(fmt.Sprintf("工位%d", stationNo)).
|
||||
SetFlowId(flowID).Save(ctx)
|
||||
return
|
||||
}
|
||||
_, _ = s.ctx.EntClient.Station.UpdateOneID(st.ID).SetFlowId(flowID).Save(ctx)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -142,7 +189,7 @@ func (s *Service) DeleteFlow(ctx context.Context, id int, operator string) error
|
||||
return s.ctx.EntClient.ProcessFlow.DeleteOneID(id).Exec(ctx)
|
||||
}
|
||||
|
||||
// flowSteps 查询某流程的步骤模板(含考核标准)
|
||||
// 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)
|
||||
@@ -214,18 +261,65 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
|
||||
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 {
|
||||
SetStationNo(req.StationNo).SetName(req.Name).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 {
|
||||
// 仅改名称:flowId 不传时保留原绑定
|
||||
upd := s.ctx.EntClient.Station.UpdateOneID(st.ID).SetName(req.Name)
|
||||
if req.FlowId > 0 {
|
||||
flow, fErr := s.ctx.EntClient.ProcessFlow.Get(ctx, req.FlowId)
|
||||
if fErr != nil {
|
||||
return errors.New("所选工艺流程不存在")
|
||||
}
|
||||
if flow.Status != "ACTIVE" {
|
||||
return errors.New("该工艺流程已停用,请先启用后再绑定工位")
|
||||
}
|
||||
upd.SetFlowId(req.FlowId)
|
||||
}
|
||||
if _, err := upd.Save(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "station.save", "", operator, "station", "", "维护工位绑定",
|
||||
map[string]any{"stationNo": req.StationNo, "flowId": req.FlowId})
|
||||
s.ctx.EventLog.Write(ctx, "station.save", "", operator, "station", "", "维护工位",
|
||||
map[string]any{"stationNo": req.StationNo})
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFlowStatus 工艺流程启用/停用。
|
||||
// 停用:自动解绑对应工位;启用:校验该工位无其他启用流程后自动绑定。
|
||||
func (s *Service) SetFlowStatus(ctx context.Context, id int, status, operator string) error {
|
||||
if id <= 0 {
|
||||
return errors.New("缺少 id")
|
||||
}
|
||||
if status != "ACTIVE" && status != "INACTIVE" {
|
||||
return errors.New("非法状态")
|
||||
}
|
||||
flow, err := s.ctx.EntClient.ProcessFlow.Get(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("工艺流程不存在")
|
||||
}
|
||||
if status == "INACTIVE" {
|
||||
// 自动解绑该流程对应的工位
|
||||
_, _ = s.ctx.EntClient.Station.Update().
|
||||
Where(station.StationNo(flow.ProcessCode), station.FlowId(flow.ID)).
|
||||
SetFlowId(0).Save(ctx)
|
||||
} else {
|
||||
q := s.ctx.EntClient.ProcessFlow.Query().
|
||||
Where(processflow.ProcessCode(flow.ProcessCode), processflow.Status("ACTIVE"), processflow.IDNEQ(flow.ID))
|
||||
if exist, err := q.Exist(ctx); err == nil && exist {
|
||||
return fmt.Errorf("工位 %d 已存在其他启用的工艺流程,请先停用旧流程", flow.ProcessCode)
|
||||
}
|
||||
s.syncStationFlow(ctx, flow.ProcessCode, flow.ID)
|
||||
}
|
||||
if err := s.ctx.EntClient.ProcessFlow.UpdateOneID(id).SetStatus(status).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
action := "停用工艺流程"
|
||||
if status == "ACTIVE" {
|
||||
action = "启用工艺流程"
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "process.flow.status", "", operator, "process_flow", "", action, map[string]any{"flowId": id, "status": status, "name": flow.Name, "stationNo": flow.ProcessCode})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -264,6 +358,7 @@ func (s *Service) StationTask(ctx context.Context, stationNo int) (map[string]an
|
||||
|
||||
type WorkloadRow struct {
|
||||
Operator string `json:"operator"`
|
||||
StationNo string `json:"stationNo"`
|
||||
Date string `json:"date"`
|
||||
ProcessCode int `json:"processCode"`
|
||||
ProcessName string `json:"processName"`
|
||||
@@ -272,11 +367,14 @@ type WorkloadRow struct {
|
||||
NgCount int `json:"ngCount"`
|
||||
}
|
||||
|
||||
func (s *Service) Workload(ctx context.Context, operator, from, to string) (map[string]any, error) {
|
||||
func (s *Service) Workload(ctx context.Context, operator, stationNo, from, to string) (map[string]any, error) {
|
||||
q := s.ctx.EntClient.WorkpieceProcess.Query()
|
||||
if operator != "" {
|
||||
q = q.Where(workpieceprocess.Operator(operator))
|
||||
}
|
||||
if stationNo != "" {
|
||||
q = q.Where(workpieceprocess.StationNo(stationNo))
|
||||
}
|
||||
if from != "" {
|
||||
if f, err := time.Parse("2006-01-02", from); err == nil {
|
||||
q = q.Where(workpieceprocess.CreatedAtGTE(f))
|
||||
@@ -291,13 +389,13 @@ func (s *Service) Workload(ctx context.Context, operator, from, to string) (map[
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type key struct{ op, date string; code int }
|
||||
type key struct{ op, station, date string; code int }
|
||||
agg := map[key]*WorkloadRow{}
|
||||
for _, r := range rows {
|
||||
k := key{r.Operator, r.CreatedAt.Format("2006-01-02"), r.ProcessCode}
|
||||
k := key{r.Operator, r.StationNo, 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}
|
||||
row = &WorkloadRow{Operator: r.Operator, StationNo: r.StationNo, Date: k.date, ProcessCode: r.ProcessCode, ProcessName: r.ProcessName}
|
||||
agg[k] = row
|
||||
}
|
||||
row.DoneCount++
|
||||
@@ -315,6 +413,9 @@ func (s *Service) Workload(ctx context.Context, operator, from, to string) (map[
|
||||
if list[i].Operator != list[j].Operator {
|
||||
return list[i].Operator < list[j].Operator
|
||||
}
|
||||
if list[i].StationNo != list[j].StationNo {
|
||||
return list[i].StationNo < list[j].StationNo
|
||||
}
|
||||
if list[i].Date != list[j].Date {
|
||||
return list[i].Date < list[j].Date
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FullProcessSeq 全部 12 个工位的完整组合(按顺序执行)
|
||||
const FullProcessSeq = "1,2,3,4,5,6,7,8,9,10,11,12"
|
||||
|
||||
// ParseProcessSeq 解析工位组合字符串为升序去重的工位码列表。
|
||||
// 唯一格式:逗号分隔 "1,3,5" / "10,11,12"。传送带不回走,故组合天然按工位号升序执行。
|
||||
func ParseProcessSeq(s string) []int {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
seen := map[int]bool{}
|
||||
out := make([]int, 0, 12)
|
||||
for _, p := range strings.Split(s, ",") {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(p)
|
||||
if err != nil || n < 1 || n > 12 || seen[n] {
|
||||
continue
|
||||
}
|
||||
seen[n] = true
|
||||
out = append(out, n)
|
||||
}
|
||||
sort.Ints(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// NormalizeProcessSeq 规范化:解析后重排为逗号分隔升序,非法/空返回空串。
|
||||
func NormalizeProcessSeq(s string) string {
|
||||
codes := ParseProcessSeq(s)
|
||||
if len(codes) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, len(codes))
|
||||
for i, c := range codes {
|
||||
parts[i] = strconv.Itoa(c)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
// ValidateProcessSeq 校验工位组合:非空、1~12 升序执行。
|
||||
func ValidateProcessSeq(s string) error {
|
||||
if len(ParseProcessSeq(s)) == 0 {
|
||||
return errors.New("工位组合不能为空,请至少选择一个工位")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"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"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -28,7 +28,7 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
if r.code == "OPERATOR" {
|
||||
codes = []string{
|
||||
"produce.workorder", "produce.dailyplan", "produce.bom", "produce.material", "produce.scan",
|
||||
"produce.trace", "produce.torque", "produce.plc", "produce.product", "produce.step",
|
||||
"produce.trace", "produce.torque", "produce.plc", "produce.product",
|
||||
"produce.processflow", "produce.station", "produce.performance",
|
||||
// 按钮级权限(块2)
|
||||
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
|
||||
@@ -73,13 +73,12 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.dailyplan", "日排产", "MENU", "/daily-plan"},
|
||||
{"produce.bom", "物料清单", "MENU", "/bom"},
|
||||
{"produce.material", "备料单", "MENU", "/material-request"},
|
||||
{"produce.plc", "PLC工序下发", "MENU", "/plc-send"},
|
||||
{"produce.plc", "工位组合下发", "MENU", "/plc-send"},
|
||||
{"produce.torque", "拧紧查询", "MENU", "/torque"},
|
||||
{"produce.step", "工艺参数", "MENU", "/process-step"},
|
||||
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
|
||||
{"produce.station", "工位管理", "MENU", "/station"},
|
||||
{"produce.station", "关联工位", "MENU", "/station"},
|
||||
{"produce.performance", "绩效报表", "MENU", "/performance"},
|
||||
{"produce.scan", "扫码报工", "MENU", "/scan"},
|
||||
{"produce.scan", "手动报工", "MENU", "/scan"},
|
||||
{"produce.trace", "工件追溯", "MENU", "/trace"},
|
||||
{"produce.product", "产品类型", "MENU", "/product-type"},
|
||||
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
|
||||
@@ -102,7 +101,7 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
{"produce.workorder:dailyplan", "工单-日排产", "BUTTON", ""},
|
||||
{"produce.bom:edit", "物料清单-维护", "BUTTON", ""},
|
||||
{"produce.material:generate", "备料单-生成", "BUTTON", ""},
|
||||
{"produce.plc:send", "PLC工序-下发", "BUTTON", ""},
|
||||
{"produce.plc:send", "工位组合-下发", "BUTTON", ""},
|
||||
{"produce.product:add", "产品类型-新增", "BUTTON", ""},
|
||||
{"produce.product:edit", "产品类型-编辑", "BUTTON", ""},
|
||||
{"produce.product:delete", "产品类型-删除", "BUTTON", ""},
|
||||
@@ -129,29 +128,25 @@ func (s *Service) Seed(ctx context.Context) error {
|
||||
SetCode(b.code).SetName(b.name).SetType(b.typ).SetPath(b.path).Exec(ctx)
|
||||
}
|
||||
|
||||
// 4. 12 道工序默认工艺流程 + 工位绑定(流程挂 process_code,工位绑流程)
|
||||
// 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) {
|
||||
continue
|
||||
}
|
||||
_ = s.ctx.EntClient.StationProcess.Create().
|
||||
SetStationNo(i).SetProcessCode(i).SetEffectDate("").Exec(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedFlowsAndStations(ctx context.Context, s *Service) {
|
||||
// 每个工序编号(1~12)创建默认工艺流程,并为每道工序补默认步骤模板(挂到流程下)
|
||||
// 每个工位(1~12)若未配置工艺流程,则创建默认流程并绑定;已有流程则跳过
|
||||
for i := 1; i <= 12; i++ {
|
||||
flow, err := s.ctx.EntClient.ProcessFlow.Query().
|
||||
Where(processflow.ProcessCode(i), processflow.Name("工序"+strconv.Itoa(i)+"_默认")).First(ctx)
|
||||
Where(processflow.ProcessCode(i)).
|
||||
Order(ent.Asc(processflow.FieldID)).First(ctx)
|
||||
if err != nil {
|
||||
flow, _ = s.ctx.EntClient.ProcessFlow.Create().
|
||||
SetName("工序" + strconv.Itoa(i) + "_默认").
|
||||
SetName("工位" + strconv.Itoa(i) + "_默认").
|
||||
SetProcessCode(i).SetStatus("ACTIVE").SetRemark("默认工艺流程").Save(ctx)
|
||||
}
|
||||
if flow == nil {
|
||||
continue
|
||||
}
|
||||
cnt, _ := s.ctx.EntClient.ProcessStep.Query().Where(processstep.FlowId(flow.ID)).Count(ctx)
|
||||
if cnt == 0 {
|
||||
stepID := 0
|
||||
@@ -166,7 +161,7 @@ func seedFlowsAndStations(ctx context.Context, s *Service) {
|
||||
SetCollectType("AUTO").SetIsTorque(true).Save(ctx)
|
||||
}
|
||||
}
|
||||
// 工位 N 绑定工序 N 的默认流程
|
||||
// 工位 N 绑定工位 N 的默认流程
|
||||
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx)
|
||||
if err != nil {
|
||||
_, _ = s.ctx.EntClient.Station.Create().
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/processstep"
|
||||
"bj_power_mes/ent/processflow"
|
||||
"bj_power_mes/ent/stepcriterion"
|
||||
"bj_power_mes/ent/torquerecord"
|
||||
"bj_power_mes/ent/workpieceprocess"
|
||||
@@ -122,32 +122,19 @@ type CriterionVO struct {
|
||||
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)
|
||||
// ListStationSteps 按工位号加载该工位「启用」工艺流程的工序步骤(含考核标准)。
|
||||
// 手动报工页:选工位 → 自动带出该工位的工序步骤。
|
||||
func (s *Service) ListStationSteps(ctx context.Context, stationNo int) ([]*ProcessStepTemplate, error) {
|
||||
if stationNo < 1 || stationNo > 12 {
|
||||
return []*ProcessStepTemplate{}, nil
|
||||
}
|
||||
flow, err := s.ctx.EntClient.ProcessFlow.Query().
|
||||
Where(processflow.ProcessCode(stationNo), processflow.Status("ACTIVE")).First(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 该工位未配置启用的工艺流程:返回空模板(前端提示)
|
||||
return []*ProcessStepTemplate{}, nil
|
||||
}
|
||||
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
|
||||
return s.flowSteps(ctx, flow.ID, flow.ProcessCode), nil
|
||||
}
|
||||
|
||||
// StepItemReq 一次保存的一个步骤(含其考核标准)
|
||||
@@ -161,68 +148,3 @@ type StepItemReq struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"errors"
|
||||
|
||||
"bj_power_mes/ent"
|
||||
"bj_power_mes/ent/dailyplan"
|
||||
"bj_power_mes/ent/materialrequest"
|
||||
"bj_power_mes/ent/workorder"
|
||||
"bj_power_mes/ent/workpiece"
|
||||
)
|
||||
|
||||
type WorkOrderReq struct {
|
||||
@@ -37,8 +40,11 @@ func (s *Service) CreateWorkOrder(ctx context.Context, req WorkOrderReq, operato
|
||||
status = "CREATED"
|
||||
}
|
||||
if req.ProcessSeq == "" {
|
||||
req.ProcessSeq = "123456789101112"
|
||||
req.ProcessSeq = FullProcessSeq
|
||||
} else if err := ValidateProcessSeq(req.ProcessSeq); err != nil {
|
||||
return err
|
||||
}
|
||||
req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq)
|
||||
b := s.ctx.EntClient.WorkOrder.Create().
|
||||
SetWorkOrderNo(req.WorkOrderNo).
|
||||
SetProductTypeId(req.ProductTypeId).
|
||||
@@ -68,8 +74,25 @@ func (s *Service) UpdateWorkOrder(ctx context.Context, req WorkOrderReq, operato
|
||||
if req.Id == 0 {
|
||||
return errors.New("缺少 id")
|
||||
}
|
||||
u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).
|
||||
SetStatus(req.Status).SetQuantity(req.Quantity).SetProcessSeq(req.ProcessSeq)
|
||||
if req.ProcessSeq != "" {
|
||||
if err := ValidateProcessSeq(req.ProcessSeq); err != nil {
|
||||
return err
|
||||
}
|
||||
req.ProcessSeq = NormalizeProcessSeq(req.ProcessSeq)
|
||||
}
|
||||
u := s.ctx.EntClient.WorkOrder.UpdateOneID(req.Id).SetProcessSeq(req.ProcessSeq)
|
||||
if req.Status != "" {
|
||||
u.SetStatus(req.Status)
|
||||
}
|
||||
if req.Quantity > 0 {
|
||||
u.SetQuantity(req.Quantity)
|
||||
}
|
||||
if req.ProductCode != "" {
|
||||
u.SetProductCode(req.ProductCode)
|
||||
}
|
||||
if req.ProductName != "" {
|
||||
u.SetProductName(req.ProductName)
|
||||
}
|
||||
if req.PlanStart != "" {
|
||||
if t, err := parseDate(req.PlanStart); err == nil {
|
||||
u.SetPlanStart(t)
|
||||
@@ -90,8 +113,8 @@ func (s *Service) UpdateWorkOrder(ctx context.Context, req WorkOrderReq, operato
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListWorkOrders 查询工单(支持工单号/状态过滤)
|
||||
func (s *Service) ListWorkOrders(ctx context.Context, orderNo, status string) ([]*ent.WorkOrder, error) {
|
||||
// ListWorkOrders 查询工单(支持工单号/状态/产品编码/产品名称 过滤)
|
||||
func (s *Service) ListWorkOrders(ctx context.Context, orderNo, status, productCode, productName string) ([]*ent.WorkOrder, error) {
|
||||
q := s.ctx.EntClient.WorkOrder.Query()
|
||||
if orderNo != "" {
|
||||
q = q.Where(workorder.WorkOrderNoContains(orderNo))
|
||||
@@ -99,6 +122,12 @@ func (s *Service) ListWorkOrders(ctx context.Context, orderNo, status string) ([
|
||||
if status != "" {
|
||||
q = q.Where(workorder.Status(status))
|
||||
}
|
||||
if productCode != "" {
|
||||
q = q.Where(workorder.ProductCodeContains(productCode))
|
||||
}
|
||||
if productName != "" {
|
||||
q = q.Where(workorder.ProductNameContains(productName))
|
||||
}
|
||||
return q.Order(ent.Desc(workorder.FieldID)).All(ctx)
|
||||
}
|
||||
|
||||
@@ -107,4 +136,57 @@ func (s *Service) GetWorkOrder(ctx context.Context, id int) (*ent.WorkOrder, err
|
||||
return s.ctx.EntClient.WorkOrder.Get(ctx, id)
|
||||
}
|
||||
return s.ctx.EntClient.WorkOrder.Query().Order(ent.Desc(workorder.FieldID)).First(ctx)
|
||||
}
|
||||
|
||||
// 工单状态白名单
|
||||
var workOrderStatuses = map[string]bool{
|
||||
"CREATED": true, "RELEASED": true, "IN_PROGRESS": true, "DONE": true,
|
||||
"PENDING": true, "CONFIRMED": true,
|
||||
}
|
||||
|
||||
// SetWorkOrderStatus 工单状态流转(发布/开工/完工等),操作列快捷操作。
|
||||
func (s *Service) SetWorkOrderStatus(ctx context.Context, id int, status, operator string) error {
|
||||
if id <= 0 {
|
||||
return errors.New("缺少 id")
|
||||
}
|
||||
if !workOrderStatuses[status] {
|
||||
return errors.New("非法的工单状态")
|
||||
}
|
||||
if err := s.ctx.EntClient.WorkOrder.UpdateOneID(id).SetStatus(status).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, id); err == nil {
|
||||
s.ctx.EventLog.Write(ctx, "work.order.status", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "变更工单状态", map[string]any{"status": status})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWorkOrder 删除工单。
|
||||
// 安全条件:① 状态仅限 已创建/待排产/已发布(未开始生产);② 无日排产记录;
|
||||
// ③ 未生成备料单;④ 无任何工件生产数据。
|
||||
func (s *Service) DeleteWorkOrder(ctx context.Context, id int, operator string) error {
|
||||
if id <= 0 {
|
||||
return errors.New("缺少 id")
|
||||
}
|
||||
wo, err := s.ctx.EntClient.WorkOrder.Get(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("工单不存在")
|
||||
}
|
||||
if wo.Status != "CREATED" && wo.Status != "PENDING" && wo.Status != "RELEASED" {
|
||||
return errors.New("仅「已创建 / 待排产 / 已发布」状态的工单可删除,生产中或已完成的工单不能删除")
|
||||
}
|
||||
if n, err := s.ctx.EntClient.DailyPlan.Query().Where(dailyplan.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 {
|
||||
return errors.New("该工单已有日排产记录,请先删除对应排产后再删除工单")
|
||||
}
|
||||
if n, err := s.ctx.EntClient.MaterialRequest.Query().Where(materialrequest.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 {
|
||||
return errors.New("该工单已生成备料单,不能删除")
|
||||
}
|
||||
if n, err := s.ctx.EntClient.Workpiece.Query().Where(workpiece.OrderNo(wo.WorkOrderNo)).Count(ctx); err == nil && n > 0 {
|
||||
return errors.New("该工单已有工件生产数据,不能删除")
|
||||
}
|
||||
if err := s.ctx.EntClient.WorkOrder.DeleteOneID(id).Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
s.ctx.EventLog.Write(ctx, "work.order.delete", wo.WorkOrderNo, operator, "work_order", wo.WorkOrderNo, "删除工单", map[string]any{"status": wo.Status})
|
||||
return nil
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func processName(code int) string {
|
||||
if n, ok := names[code]; ok {
|
||||
return n
|
||||
}
|
||||
return "工序" + itoa(code)
|
||||
return "工位" + itoa(code)
|
||||
}
|
||||
|
||||
// evalLogic 依据考核逻辑判定
|
||||
@@ -73,7 +73,7 @@ func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator s
|
||||
}
|
||||
seq := req.ProcessSeq
|
||||
if seq == "" {
|
||||
seq = "123456789101112"
|
||||
seq = FullProcessSeq
|
||||
}
|
||||
if req.WorkOrderId == 0 {
|
||||
if wo, err := s.ctx.EntClient.WorkOrder.Query().Where(workorder.WorkOrderNo(req.OrderNo)).First(ctx); err == nil {
|
||||
@@ -83,6 +83,7 @@ func (s *Service) OnlineWorkpiece(ctx context.Context, req OnlineReq, operator s
|
||||
}
|
||||
}
|
||||
}
|
||||
seq = NormalizeProcessSeq(seq)
|
||||
err := s.ctx.EntClient.Workpiece.Create().
|
||||
SetSn(req.Sn).SetOrderNo(req.OrderNo).SetWorkOrderId(req.WorkOrderId).
|
||||
SetProcessSeq(seq).SetStatus("ONLINE").SetOnlineAt(time.Now()).Exec(ctx)
|
||||
@@ -100,7 +101,7 @@ type ReportProcessReq struct {
|
||||
Steps []StepDataReq `json:"steps"`
|
||||
}
|
||||
|
||||
// ReportProcess 工位工序报工:写工序实绩 + 步骤考核(step_data,扭矩在RANGE自动判定 OK/NG)
|
||||
// ReportProcess 工位报工:写报工实绩 + 步骤考核(step_data,扭矩在RANGE自动判定 OK/NG)
|
||||
func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, operator string) error {
|
||||
if req.Sn == "" || req.ProcessCode == 0 {
|
||||
return errors.New("sn 与 processCode 必填")
|
||||
@@ -139,7 +140,7 @@ func (s *Service) ReportProcess(ctx context.Context, req ReportProcessReq, opera
|
||||
SetCurrentProcess(req.ProcessCode).SetStatus("PROCESSING").Save(ctx)
|
||||
|
||||
s.ctx.EventLog.Write(ctx, "workpiece.process.report", wp.OrderNo, operator, "workpiece", req.Sn,
|
||||
"工位工序报工", map[string]any{"processCode": req.ProcessCode, "result": result})
|
||||
"工位报工", map[string]any{"processCode": req.ProcessCode, "result": result})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user