feat:多项功能优化

This commit is contained in:
zhoujianjin
2026-09-18 18:54:54 +08:00
parent 672cd23329
commit 543655d441
48 changed files with 1289 additions and 447 deletions
+9
View File
@@ -120,6 +120,15 @@ var schemaPatchSQL = []string{
`ALTER TABLE work_order ADD COLUMN IF NOT EXISTS created_by varchar(64) NOT NULL DEFAULT ''`,
`ALTER TABLE work_order ADD COLUMN IF NOT EXISTS due_date timestamp`,
`ALTER TABLE workpiece_process ADD COLUMN IF NOT EXISTS duration_sec integer NOT NULL DEFAULT 0`,
// 工位内置标识 + 是否有接驳台(实体位置由内置数据给定,页面不可手改)
`ALTER TABLE station ADD COLUMN IF NOT EXISTS is_builtin boolean NOT NULL DEFAULT false`,
`ALTER TABLE station ADD COLUMN IF NOT EXISTS has_dock boolean NOT NULL DEFAULT false`,
// 存量数据回填:种子初始化的工位号(0/1..13)一律为内置工位;
// 其中 1~10 号工位对应实体接驳台(R1/R2),0/11/12/13 无接驳台。
// 页面新增的工位号从 14 起,不受本回填影响。
`UPDATE station SET is_builtin = true WHERE station_no BETWEEN 0 AND 13`,
`UPDATE station SET has_dock = true WHERE station_no BETWEEN 1 AND 10`,
}
// EnsureSchema ent 建表 + 幂等列补丁(只增,不删数据)
+1 -2
View File
@@ -31,8 +31,7 @@ var pathPermMap = map[string]string{
"POST:/api/v1/process-flows/upload": "produce.processflow:upload",
"POST:/api/v1/process-steps": "produce.processflow:edit",
"POST:/api/v1/stations": "produce.station:edit",
// 数量不符上报(菜单 produce.qtyreport 下新增)
"POST:/api/v1/qty-reports": "produce.qtyreport",
"DELETE:/api/v1/stations/*": "produce.station:delete",
// 过程巡检汇总生成《工序间检验记录》(巡检终端操作域)
"POST:/api/v1/inspections/generate-inter-process": "sys.inspect:process",
// 质量检验处置(过程检/完工检不合格处置 退货/返修/退换)
@@ -112,6 +112,17 @@ func SaveStationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
// DeleteStationHandler DELETE /stations/:id(删除工位:内置工位不允许删除)
func DeleteStationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := logic.New(svcCtx).DeleteStation(r.Context(), pathId(r), operator(r, "")); err != nil {
httpx.Fail(w, 2208, err.Error())
return
}
httpx.OkMessage(w, "工位已删除", nil)
}
}
// SetFlowStatusHandler POST /process-flows/status(工艺流程启用/停用)
func SetFlowStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -81,16 +81,29 @@ func ListAlertRulesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
// ListAlertsHandler GET /alerts?status= 预警消息收件箱
// ListAlertsHandler GET /alerts?status=&type=&from=&to=&page=&pageSize=
// 预警消息收件箱:支持 状态/类型/时间范围(YYYY-MM-DD) 筛选;带 page 返回分页对象,否则返回全量数组(顶部铃铛未读数复用)。
func ListAlertsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
data, err := logic.New(svcCtx).ListAlerts(r.Context(), status)
q := r.URL.Query()
query := logic.AlertQuery{
Status: q.Get("status"),
Type: q.Get("type"),
From: q.Get("from"),
To: q.Get("to"),
Page: atoiDefault(q.Get("page"), 0),
PageSize: atoiDefault(q.Get("pageSize"), 20),
}
list, total, err := logic.New(svcCtx).ListAlerts(r.Context(), query)
if err != nil {
httpx.Fail(w, 3305, err.Error())
return
}
httpx.Ok(w, data)
if query.Page > 0 {
httpx.Ok(w, map[string]any{"list": list, "total": total, "page": query.Page, "pageSize": query.PageSize})
return
}
httpx.Ok(w, list)
}
}
@@ -102,7 +102,7 @@ func ExportProductTypesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return
}
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("产品型号.xlsx"))
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("物料档案.xlsx"))
_, _ = w.Write(b)
}
}
@@ -174,7 +174,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
{Method: http.MethodGet, Path: "/torque/groups", Handler: production.TorqueGroupsHandler(serverCtx)},
{Method: http.MethodGet, Path: "/torque/stat", Handler: production.TorqueStatHandler(serverCtx)},
{Method: http.MethodPost, Path: "/torque/audit", Handler: production.TorqueAuditHandler(serverCtx)},
{Method: http.MethodGet, Path: "/torque/audit-log", Handler: production.TorqueAuditLogHandler(serverCtx)},
{Method: http.MethodGet, Path: "/torque/audit-log", Handler: production.TorqueAuditLogHandler(serverCtx)},
{Method: http.MethodPost, Path: "/scan/report", Handler: production.ScanReportHandler(serverCtx)},
{Method: http.MethodGet, Path: "/scan/records", Handler: production.ScanRecordsHandler(serverCtx)},
@@ -217,6 +217,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
{Method: http.MethodPost, Path: "/process-flows/upload", Handler: UploadPdfHandler(serverCtx)},
{Method: http.MethodGet, Path: "/stations", Handler: ListStationsHandler(serverCtx)},
{Method: http.MethodPost, Path: "/stations", Handler: SaveStationHandler(serverCtx)},
{Method: http.MethodDelete, Path: "/stations/:id", Handler: DeleteStationHandler(serverCtx)},
{Method: http.MethodGet, Path: "/line/points", Handler: ListPointsHandler(serverCtx)},
{Method: http.MethodPost, Path: "/line/move", Handler: IssueMoveHandler(serverCtx)},
{Method: http.MethodPost, Path: "/line/move/ack", Handler: AckMoveHandler(serverCtx)},
@@ -235,10 +236,6 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
{Method: http.MethodGet, Path: "/quality/unqualified-report", Handler: UnqualifiedReportHandler(serverCtx)},
{Method: http.MethodPost, Path: "/inspections/generate-inter-process", Handler: GenerateInterProcessInspectionHandler(serverCtx)},
// ---------- 数量不符上报(第三批) ----------
{Method: http.MethodPost, Path: "/qty-reports", Handler: CreateQtyReportHandler(serverCtx)},
{Method: http.MethodGet, Path: "/qty-reports", Handler: ListQtyReportsHandler(serverCtx)},
// ---------- 应急呼叫(D3MES 前端亦可发起) ----------
{Method: http.MethodPost, Path: "/alert/emergency", Handler: EmergencyCallHandler(serverCtx)},
@@ -170,36 +170,6 @@ func EmergencyCallHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
// CreateQtyReportHandler POST /api/v1/qty-reports
func CreateQtyReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.QtyReportReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
req.Operator = operator(r, req.Operator)
if err := logic.New(svcCtx).CreateQtyReport(r.Context(), req); err != nil {
httpx.Fail(w, 2402, err.Error())
return
}
httpx.OkMessage(w, "数量不符已上报", nil)
}
}
// ListQtyReportsHandler GET /api/v1/qty-reports?status=
func ListQtyReportsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, err := logic.New(svcCtx).ListQtyReports(r.Context(),
r.URL.Query().Get("status"), r.URL.Query().Get("orderNo"), r.URL.Query().Get("materialCode"))
if err != nil {
httpx.Fail(w, 2404, err.Error())
return
}
httpx.Ok(w, data)
}
}
// GenerateInterProcessInspectionHandler POST /api/v1/inspections/generate-inter-process
// 一键生成《工序间检验记录》:汇总该工单 PROCESS 类巡检记录(按步骤维度)。
func GenerateInterProcessInspectionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+42 -6
View File
@@ -3,6 +3,7 @@ package logic
import (
"context"
"errors"
"time"
"bj_power_mes/ent"
"bj_power_mes/ent/alert"
@@ -48,13 +49,48 @@ func (s *Service) ListAlertRules(ctx context.Context) ([]*ent.AlertRule, error)
Order(ent.Desc(alertrule.FieldCreatedAt), ent.Desc(alertrule.FieldID)).All(ctx)
}
// ListAlerts 查询预警消息(status 空=全部)
func (s *Service) ListAlerts(ctx context.Context, status string) ([]*ent.Alert, error) {
q := s.ctx.EntClient.Alert.Query()
if status != "" {
q = q.Where(alert.Status(status))
// AlertQuery 预警查询条件:status/type 空=不限;from/to 为 YYYY-MM-DD(空=不限);page<=0 表示不分页。
type AlertQuery struct {
Status string
Type string
From string
To string
Page int
PageSize int
}
// ListAlerts 查询预警消息(支持状态/类型/时间范围筛选 + 分页),返回当前页数据与命中总数
func (s *Service) ListAlerts(ctx context.Context, q AlertQuery) ([]*ent.Alert, int, error) {
query := s.ctx.EntClient.Alert.Query()
if q.Status != "" {
query = query.Where(alert.Status(q.Status))
}
return q.Order(ent.Desc(alert.FieldCreatedAt), ent.Desc(alert.FieldID)).All(ctx)
if q.Type != "" {
query = query.Where(alert.Type(q.Type))
}
if q.From != "" {
if f, err := time.Parse("2006-01-02", q.From); err == nil {
query = query.Where(alert.CreatedAtGTE(f))
}
}
if q.To != "" {
if t, err := time.Parse("2006-01-02", q.To); err == nil {
query = query.Where(alert.CreatedAtLT(t.Add(24 * time.Hour)))
}
}
total, err := query.Count(ctx)
if err != nil {
return nil, 0, err
}
if q.Page > 0 {
size := q.PageSize
if size <= 0 {
size = 20
}
query = query.Offset((q.Page - 1) * size).Limit(size)
}
list, err := query.Order(ent.Desc(alert.FieldCreatedAt), ent.Desc(alert.FieldID)).All(ctx)
return list, total, err
}
// UnreadAlertCount 未读预警数
+38 -3
View File
@@ -255,6 +255,8 @@ type StationVO struct {
FlowName string `json:"flowName"`
StationType string `json:"stationType"`
Status string `json:"status"`
IsBuiltin bool `json:"isBuiltin"`
HasDock bool `json:"hasDock"`
}
func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
@@ -269,7 +271,7 @@ func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
}
out := make([]*StationVO, 0, len(stas))
for _, st := range stas {
vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, StationType: st.StationType, Status: st.Status}
vo := &StationVO{Id: st.ID, StationNo: st.StationNo, Name: st.Name, FlowId: st.FlowId, StationType: st.StationType, Status: st.Status, IsBuiltin: st.IsBuiltin, HasDock: st.HasDock}
if f, ok := flowMap[st.FlowId]; ok {
vo.FlowName = f.Name
}
@@ -283,6 +285,9 @@ func (s *Service) ListStations(ctx context.Context) ([]*StationVO, error) {
// - 工位号不存在 → 新增工位;新增的工位号由用户自定义,允许扩展 14、15…(虚拟/物理均可)
//
// 工位数量以数据库为准,前端下拉随 station 表自动增减;虚拟工位(0/13/14...)不连 PLC、仅记录。
// 内置工位(种子/初始化数据,is_builtin=true)需连 PLC 并必须按工位号顺序流转,
// 不允许手动更改工位类型;页面新增的工位一律为非内置,可改类型、可删除。
// 「是否有接驳台」对应实体位置,由内置数据给定,不接收页面入参。
func (s *Service) SaveStation(ctx context.Context, req StationReq, operator string) error {
if req.StationNo < 0 {
return errors.New("工位号非法(需 ≥ 0")
@@ -304,7 +309,10 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
}
}
cr := s.ctx.EntClient.Station.Create().
SetStationNo(req.StationNo).SetName(req.Name).SetStationType(stype).SetStatus("ENABLED")
SetStationNo(req.StationNo).SetName(req.Name).SetStationType(stype).SetStatus("ENABLED").
// 页面「新增工位」创建的是非内置工位(可改类型、可删除);内置工位只能由种子/初始化数据产生。
// 是否有接驳台对应实体位置,由内置数据给定,页面新增时一律为 false 且不可手改。
SetIsBuiltin(false).SetHasDock(false)
if req.FlowId != nil && *req.FlowId > 0 {
flow, fErr := s.ctx.EntClient.ProcessFlow.Get(ctx, *req.FlowId)
if fErr != nil {
@@ -330,7 +338,13 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
if t != "LINE" && t != "OFFLINE" {
return errors.New("工位类型仅支持 LINE/OFFLINE")
}
upd.SetStationType(t)
// 内置工位对应实体产线固定位置(含 PLC 通讯与顺序流转),工位类型不可手动更改。
if st.IsBuiltin && t != st.StationType {
return errors.New("内置工位不允许修改工位类型")
}
if !st.IsBuiltin {
upd.SetStationType(t)
}
}
if req.FlowId != nil {
fid := *req.FlowId
@@ -355,6 +369,27 @@ func (s *Service) SaveStation(ctx context.Context, req StationReq, operator stri
return nil
}
// DeleteStation 删除工位:内置工位不允许删除(对应实体产线位置,需连 PLC 且必须按顺序流转);
// 非内置工位(页面新增的)允许删除。
func (s *Service) DeleteStation(ctx context.Context, id int, operator string) error {
if id <= 0 {
return errors.New("缺少 id")
}
st, err := s.ctx.EntClient.Station.Get(ctx, id)
if err != nil {
return errors.New("工位不存在")
}
if st.IsBuiltin {
return errors.New("内置工位不允许删除")
}
if err := s.ctx.EntClient.Station.DeleteOneID(st.ID).Exec(ctx); err != nil {
return err
}
s.ctx.EventLog.Write(ctx, "station.delete", "", operator, "station", "", "删除工位",
map[string]any{"stationNo": st.StationNo, "name": st.Name})
return nil
}
// SetFlowStatus 工艺流程启用/停用。
// 新语义:绑定关系由「保存流程」显式维护(一个流程可绑多工位),启停只切换流程状态,
// 不再自动解绑/绑定工位;停用流程对已绑定工位不可用(工位取步骤时按 ACTIVE 过滤)。
+4 -4
View File
@@ -56,11 +56,11 @@ func (s *Service) DeleteProductType(ctx context.Context, id int) error {
}
woCnt, _ := s.ctx.EntClient.WorkOrder.Query().Where(workorder.ProductCode(row.Code)).Count(ctx)
if woCnt > 0 {
return fmt.Errorf("该产品型号已被 %d 张工单引用,不可删除;建议停用(下架)", woCnt)
return fmt.Errorf("该物料档案已被 %d 张工单引用,不可删除;建议停用(下架)", woCnt)
}
bomCnt, _ := s.ctx.EntClient.BomItem.Query().Where(bomitem.ProductCode(row.Code)).Count(ctx)
if bomCnt > 0 {
return fmt.Errorf("该产品型号在物料清单中有 %d 条配置,不可删除;建议停用(下架)", bomCnt)
return fmt.Errorf("该物料档案在物料清单中有 %d 条配置,不可删除;建议停用(下架)", bomCnt)
}
if err := s.ctx.Wms.DeleteProductType(ctx, id); err != nil {
return errors.New("WMS 不可达或拒绝删除:" + err.Error())
@@ -112,12 +112,12 @@ func (s *Service) productTypeByID(ctx context.Context, id int) (*wmsclient.Produ
return &list[i], nil
}
}
return nil, errors.New("产品型号不存在")
return nil, errors.New("物料档案不存在")
}
// 降级:本地缓存
pt, err := s.ctx.EntClient.ProductType.Get(ctx, id)
if err != nil {
return nil, errors.New("产品型号不存在(WMS 不可达,无法校验引用)")
return nil, errors.New("物料档案不存在(WMS 不可达,无法校验引用)")
}
return &wmsclient.ProductTypeRow{ID: pt.ID, Code: pt.Code, Name: pt.Name}, nil
}
+45 -13
View File
@@ -28,12 +28,12 @@ func (s *Service) Seed(ctx context.Context) error {
"produce.trace", "produce.wip", "produce.torque", "produce.plc", "produce.product",
"produce.processflow", "produce.station", "produce.performance", "produce.processcard",
// 按钮级权限(块2
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
"produce.qtyreport", "produce.quality", "produce.quality:edit",
"produce.workorder:add", "produce.workorder:edit", "produce.workorder:delete",
"produce.workorder:dailyplan", "produce.bom:edit", "produce.material:generate",
"produce.quality", "produce.quality:edit",
"produce.plc:send", "produce.product:add", "produce.product:edit", "produce.product:delete",
"produce.processflow:add", "produce.processflow:edit", "produce.processflow:delete",
"produce.processflow:upload", "produce.station:edit", "produce.station:add",
"produce.processflow:add", "produce.processflow:edit", "produce.processflow:delete",
"produce.processflow:upload", "produce.station:edit", "produce.station:add", "produce.station:delete",
}
} else if r.code == "INSPECTOR" {
codes = []string{"produce.trace", "produce.wip", "produce.torque", "produce.performance", "sys.eventlog",
@@ -74,13 +74,12 @@ func (s *Service) Seed(ctx context.Context) error {
{"produce.plc", "工位组合下发", "MENU", "/plc-send"},
{"produce.torque", "拧紧查询", "MENU", "/torque"},
{"produce.processflow", "工艺流程", "MENU", "/process-flow"},
{"produce.station", "关联工位", "MENU", "/station"}, {"produce.performance", "绩效报表", "MENU", "/performance"},
{"produce.station", "关联工位", "MENU", "/station"}, {"produce.performance", "绩效报表", "MENU", "/performance"},
{"produce.scan", "手动报工", "MENU", "/scan"},
{"produce.trace", "工件追溯", "MENU", "/trace"},
{"produce.wip", "在制品", "MENU", "/wip"},
{"produce.processcard", "生产流程卡", "MENU", "/process-card"},
{"produce.product", "产品型号", "MENU", "/product-type"},
{"produce.qtyreport", "数量不符上报", "MENU", "/qty-report"},
{"produce.product", "物料档案", "MENU", "/product-type"},
{"produce.quality", "质量检验", "MENU", "/quality"},
{"sys.inspect", "巡检终端", "MENU", "/inspect"},
{"sys.eventlog", "操作日志", "MENU", "/event-log"},
@@ -107,15 +106,16 @@ func (s *Service) Seed(ctx context.Context) error {
{"produce.bom:edit", "物料清单-维护", "BUTTON", "", "produce.bom"},
{"produce.material:generate", "备料单-生成", "BUTTON", "", "produce.material"},
{"produce.plc:send", "工位组合-下发", "BUTTON", "", "produce.plc"},
{"produce.product:add", "产品型号-新增", "BUTTON", "", "produce.product"},
{"produce.product:edit", "产品型号-编辑", "BUTTON", "", "produce.product"},
{"produce.product:delete", "产品型号-删除", "BUTTON", "", "produce.product"},
{"produce.product:add", "物料档案-新增", "BUTTON", "", "produce.product"},
{"produce.product:edit", "物料档案-编辑", "BUTTON", "", "produce.product"},
{"produce.product:delete", "物料档案-删除", "BUTTON", "", "produce.product"},
{"produce.processflow:add", "工艺流程-新增", "BUTTON", "", "produce.processflow"},
{"produce.processflow:edit", "工艺流程-编辑", "BUTTON", "", "produce.processflow"},
{"produce.processflow:delete", "工艺流程-删除", "BUTTON", "", "produce.processflow"},
{"produce.processflow:upload", "工艺流程-上传图纸", "BUTTON", "", "produce.processflow"},
{"produce.station:edit", "工位-绑定与改名", "BUTTON", "", "produce.station"},
{"produce.station:add", "工位-新增", "BUTTON", "", "produce.station"},
{"produce.station:delete", "工位-删除", "BUTTON", "", "produce.station"},
{"sys.account:add", "账号-新增", "BUTTON", "", "sys.account"},
{"sys.account:edit", "账号-编辑", "BUTTON", "", "sys.account"},
{"sys.account:delete", "账号-删除", "BUTTON", "", "sys.account"},
@@ -151,6 +151,10 @@ func (s *Service) Seed(ctx context.Context) error {
// 3.3 工艺路线模块已整块删除(D1):清掉存量角色里的路线权限码与旧菜单/按钮定义行(幂等)
migrateLegacyRoute(ctx, s)
// 3.4 MES「数量不符上报」菜单已下线(改为仅预警,明细由 WMS 处理):
// 清掉存量角色里的 produce.qtyreport 权限码与旧菜单定义行(幂等)
migrateLegacyQtyReport(ctx, s)
// 4. 初始化工位与默认工艺流程(工位绑定流程,流程承载步骤)
seedFlowsAndStations(ctx, s)
return nil
@@ -158,11 +162,14 @@ func (s *Service) Seed(ctx context.Context) error {
func seedFlowsAndStations(ctx context.Context, s *Service) {
// 初始数据:12 个工位(工位数量以数据库为准,这里只做首次初始化;要 13 个就往 station 表插一行)
// 初始化的工位一律为「内置工位」:需连 PLC、必须按工位号顺序流转、不可改类型、不可删除;
// 其中 1~10 号工位对应实体接驳台,11/12 为产线末端无接驳台。
for i := 1; i <= 12; i++ {
st, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(i)).First(ctx)
if err != nil {
st, _ = s.ctx.EntClient.Station.Create().
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").Save(ctx)
SetStationNo(i).SetName("装配工位" + strconv.Itoa(i)).SetStatus("ENABLED").
SetIsBuiltin(true).SetHasDock(i <= 10).Save(ctx)
}
if st == nil || st.FlowId > 0 {
continue
@@ -191,7 +198,8 @@ func seedFlowsAndStations(ctx context.Context, s *Service) {
} {
if _, err := s.ctx.EntClient.Station.Query().Where(station.StationNo(v.no)).First(ctx); err != nil {
_, _ = s.ctx.EntClient.Station.Create().
SetStationNo(v.no).SetName(v.name).SetStationType("OFFLINE").SetStatus("ENABLED").Save(ctx)
SetStationNo(v.no).SetName(v.name).SetStationType("OFFLINE").
SetIsBuiltin(true).SetHasDock(false).SetStatus("ENABLED").Save(ctx)
}
}
}
@@ -279,3 +287,27 @@ func migrateLegacyRoute(ctx context.Context, s *Service) {
Where(permission.Code(code)).Exec(ctx)
}
}
// migrateLegacyQtyReport 清理已下线的「数量不符上报」菜单(幂等,可重复执行):
// MES 侧对数量不符只做预警(预警中心 + 右上角铃铛),不再提供独立菜单页;
// 从存量角色的 permissionCodes 摘除 produce.qtyreport 并删除其权限定义行。
func migrateLegacyQtyReport(ctx context.Context, s *Service) {
roleList, _ := s.ctx.EntClient.Role.Query().All(ctx)
for _, rl := range roleList {
kept := make([]string, 0, len(rl.PermissionCodes))
changed := false
for _, c := range rl.PermissionCodes {
if c == "produce.qtyreport" {
changed = true
continue
}
kept = append(kept, c)
}
if changed {
_ = s.ctx.EntClient.Role.UpdateOneID(rl.ID).
SetPermissionCodes(mergeCodes(kept, nil)).Exec(ctx)
}
}
_, _ = s.ctx.EntClient.Permission.Delete().
Where(permission.Code("produce.qtyreport")).Exec(ctx)
}
+22 -21
View File
@@ -8,7 +8,6 @@ import (
"bj_power_mes/ent"
"bj_power_mes/ent/inspectionrecord"
"bj_power_mes/ent/materialqtyreport"
"bj_power_mes/ent/workorder"
)
@@ -51,8 +50,8 @@ type QtyReportReq struct {
StationNo int `json:"stationNo"`
OrderNo string `json:"orderNo"`
Sn string `json:"sn"`
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
MaterialCode string `json:"materialCode"`
MaterialName string `json:"materialName"`
PlanQty int `json:"planQty"`
ActualQty int `json:"actualQty"`
Cause string `json:"cause"`
@@ -60,6 +59,8 @@ type QtyReportReq struct {
}
// CreateQtyReport 数量不符上报:落 material_qty_report + 预警中心消息(type=qty_diff)。
// MES 侧仅预警,不承载明细处理流程:相关内容融合为一段文字描述写入预警内容,
// 由预警中心与右上角铃铛统一呈现;明细与闭环由 WMS 负责。
func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
if req.StationNo <= 0 || req.MaterialCode == "" {
return errors.New("工位号与物料编码必填")
@@ -81,12 +82,27 @@ func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
if err != nil {
return err
}
content := fmt.Sprintf("工位 %d 数量不符:物料 %s 应发 %d / 实收 %d / 差异 %d。原因:%s",
req.StationNo, req.MaterialCode, req.PlanQty, req.ActualQty, diff, req.Cause)
// 融合成单段文字描述,直接写入预警「内容」(即预警原因)。
cause := req.Cause
if cause == "" {
cause = "未填写"
}
content := fmt.Sprintf("工位%d 数量不符:物料 %s", req.StationNo, req.MaterialCode)
if req.MaterialName != "" {
content += "" + req.MaterialName + ""
}
content += fmt.Sprintf(";应发 %d,实收 %d,差异 %d", req.PlanQty, req.ActualQty, diff)
if req.OrderNo != "" {
content += ";工单 " + req.OrderNo
}
if req.Sn != "" {
content += ";序列号 " + req.Sn
}
content += ";上报原因:" + cause
_, _ = s.ctx.EntClient.Alert.Create().
SetRuleId(0).
SetType("qty_diff").
SetTitle("数量不符上报").
SetTitle("数量不符预警").
SetContent(content).
SetRefType("material_qty_report").
SetRefId(req.MaterialCode).
@@ -99,21 +115,6 @@ func (s *Service) CreateQtyReport(ctx context.Context, req QtyReportReq) error {
return nil
}
// ListQtyReports 数量不符上报列表
func (s *Service) ListQtyReports(ctx context.Context, status, orderNo, materialCode string) ([]*ent.MaterialQtyReport, error) {
q := s.ctx.EntClient.MaterialQtyReport.Query()
if status != "" {
q = q.Where(materialqtyreport.Status(status))
}
if orderNo != "" {
q = q.Where(materialqtyreport.OrderNoContainsFold(orderNo))
}
if materialCode != "" {
q = q.Where(materialqtyreport.MaterialCodeContainsFold(materialCode))
}
return q.Order(ent.Desc(materialqtyreport.FieldCreatedAt), ent.Desc(materialqtyreport.FieldID)).All(ctx)
}
// StationReturnMaterial 工位退料回库房:落事件日志 + 调 WMS 预建待确认退库单。
// 退库单在 WMS 侧由库管确认收货后库存加回(链路封闭,问题记录 L491)。
func (s *Service) StationReturnMaterial(ctx context.Context, orderNo string, stationNo int, sn, materialCode, materialName, spec, reason, operator, unit string, qty int) error {