package handler import ( "net/http" "errors" "net/url" "strconv" "time" "bj_power_mes/common/httpx" "bj_power_mes/internal/logic" "bj_power_mes/internal/svc" "github.com/xuri/excelize/v2" ) // atoiDefault 解析整型字符串,非法返回默认值 func atoiDefault(s string, def int) int { if s == "" { return def } if n, err := strconv.Atoi(s); err == nil { return n } return def } // operator 从 JWT 上下文取操作人,取不到回退 fallback func operator(r *http.Request, fallback string) string { if v, ok := r.Context().Value("name").(string); ok && v != "" { return v } if v, ok := r.Context().Value("username").(string); ok && v != "" { return v } if fallback != "" { return fallback } return "system" } // StationLoginInternalHandler 工位终端登录(内部接口,X-API-TOKEN 保护) // 校验 用户名 + 工位终端密码 + 工位权限,签发与 MES 同密钥 JWT func StationLoginInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req logic.StationLoginReq if err := httpx.ParseJSON(r, &req); err != nil { httpx.BadRequest(w, "请求体解析失败") return } data, err := logic.New(svcCtx).StationLogin(r.Context(), req) if err != nil { httpx.Fail(w, 2101, err.Error()) return } // 操作日志:工位终端登录(写失败不阻断登录) svcCtx.EventLog.Write(r.Context(), "auth.station_login", "", req.Username, "auth", req.Username, "工位终端登录(工位"+strconv.Itoa(req.StationNo)+")", map[string]any{"ip": clientIP(r), "stationNo": req.StationNo}) httpx.Ok(w, data) } } // StationLogoutInternalHandler 工位终端退出(内部接口,X-API-TOKEN 保护) func StationLogoutInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { Username string `json:"username"` StationNo int `json:"stationNo"` } if err := httpx.ParseJSON(r, &req); err != nil { httpx.BadRequest(w, "请求体解析失败") return } if req.Username != "" { svcCtx.EventLog.Write(r.Context(), "auth.station_logout", "", req.Username, "auth", req.Username, "工位终端退出(工位"+strconv.Itoa(req.StationNo)+")", map[string]any{"ip": clientIP(r), "stationNo": req.StationNo}) } httpx.OkMessage(w, "已退出", nil) } } // StationChangePasswordInternalHandler 工位终端修改自己的工位终端密码 func StationChangePasswordInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { UserId int `json:"userId"` OldPassword string `json:"oldPassword"` NewPassword string `json:"newPassword"` } if err := httpx.ParseJSON(r, &req); err != nil { httpx.BadRequest(w, "请求体解析失败") return } if err := logic.New(svcCtx).StationChangePassword(r.Context(), req.UserId, req.OldPassword, req.NewPassword); err != nil { httpx.Fail(w, 2102, err.Error()) return } httpx.OkMessage(w, "工位终端密码修改成功", nil) } } // StationTaskInternalHandler 工位终端拉取当前任务(工位/流程/步骤/活跃工单) func StationTaskInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { stationNo := atoiDefault(r.URL.Query().Get("stationNo"), 0) if stationNo <= 0 { httpx.BadRequest(w, "缺少 stationNo 参数") return } data, err := logic.New(svcCtx).StationTask(r.Context(), stationNo) if err != nil { httpx.Fail(w, 2103, err.Error()) return } httpx.Ok(w, data) } } // StationReportInternalHandler 工位终端工序报工(写工序实绩 + 步骤考核) func StationReportInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req logic.ReportProcessReq if err := httpx.ParseJSON(r, &req); err != nil { httpx.BadRequest(w, "请求体解析失败") return } if err := logic.New(svcCtx).ReportProcess(r.Context(), req, operator(r, "")); err != nil { httpx.Fail(w, 2104, err.Error()) return } httpx.OkMessage(w, "报工成功", nil) } } // WorkloadInternalHandler 工作量/绩效报表(工位终端"我的工作量" + MES 绩效报表共用) // operator=操作人、stationNo=工位号(空=全部工位)、from/to=日期范围 func WorkloadInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() data, err := logic.New(svcCtx).Workload(r.Context(), q.Get("operator"), q.Get("stationNo"), q.Get("from"), q.Get("to"), atoiDefault(q.Get("opPage"), 1), atoiDefault(q.Get("opPageSize"), 20), atoiDefault(q.Get("stPage"), 1), atoiDefault(q.Get("stPageSize"), 20), atoiDefault(q.Get("detPage"), 1), atoiDefault(q.Get("detPageSize"), 20)) if err != nil { httpx.Fail(w, 2105, err.Error()) return } httpx.Ok(w, data) } } // FileDownloadInternalHandler 内部文件下载(工艺图纸 PDF,供工位终端代理) func FileDownloadInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { serveUploadFile(w, r, svcCtx.Config.Upload.Dir, r.URL.Query().Get("name")) } } // StationCheckinInternalHandler 工位终端暂存退库(半成品入库,块4/5) // 记录已完成工序后按半成品 INBOUND 流转,并通知 WMS 入库。 func StationCheckinInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var req struct { Sn string `json:"sn"` OrderNo string `json:"orderNo"` StationNo int `json:"stationNo"` ProcessCode int `json:"processCode"` Operator string `json:"operator"` } if err := httpx.ParseJSON(r, &req); err != nil { httpx.BadRequest(w, "请求体解析失败") return } if req.Sn == "" { httpx.BadRequest(w, "缺少 sn") return } doneCodes := []int{} if req.ProcessCode > 0 { doneCodes = append(doneCodes, req.ProcessCode) } op := req.Operator if op == "" { op = operator(r, "") } if err := logic.New(svcCtx).SemiFlow(r.Context(), logic.SemiReq{ Sn: req.Sn, OrderNo: req.OrderNo, DoneProcessCodes: doneCodes, Action: "INBOUND", }, op); err != nil { httpx.Fail(w, 2106, err.Error()) return } httpx.OkMessage(w, "暂存退库已登记", nil) } } // PerformanceExportHandler GET /performance/export 绩效报表导出(一个 xlsx 三个 sheet:按人/按工位/明细) // 执行全项目统一导出时间硬规则:起止必填(默认近3个月)、间隔≤1年 func PerformanceExportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() startDate, endDate, rerr := normalizeExportRange(q.Get("startDate"), q.Get("endDate")) if rerr != nil { httpx.Fail(w, 2106, rerr.Error()) return } data, err := logic.New(svcCtx).Workload(r.Context(), q.Get("operator"), q.Get("stationNo"), startDate, endDate, 1, 1<<30, 1, 1<<30, 1, 1<<30) if err != nil { httpx.Fail(w, 2105, err.Error()) return } f := excelize.NewFile() type view struct { name string key string headers []string cols func(m map[string]any) []any } rowOf := func(v any) map[string]any { m, _ := v.(map[string]any); return m } views := []view{ {"按人", "byOperator", []string{"操作人", "完成数", "合格", "不合格"}, func(m map[string]any) []any { return []any{m["operator"], m["doneCount"], m["okCount"], m["ngCount"]} }}, {"按工位", "byStation", []string{"工位", "完成数", "合格", "不合格"}, func(m map[string]any) []any { return []any{m["stationNo"], m["doneCount"], m["okCount"], m["ngCount"]} }}, {"明细", "detail", []string{"操作人", "工位", "日期", "工序", "工序名", "完成数", "合格", "不合格"}, func(m map[string]any) []any { return []any{m["operator"], m["stationNo"], m["date"], m["processCode"], m["processName"], m["doneCount"], m["okCount"], m["ngCount"]} }}, } for i, vw := range views { sheet := vw.name if i == 0 { f.SetSheetName("Sheet1", sheet) } else { f.NewSheet(sheet) } for c, h := range vw.headers { cell, _ := excelize.CoordinatesToCellName(c+1, 1) _ = f.SetCellValue(sheet, cell, h) } vm, _ := data[vw.key].(map[string]any) list, _ := vm["list"].([]any) for rIdx, it := range list { m := rowOf(it) for c := range vw.headers { cell, _ := excelize.CoordinatesToCellName(c+1, rIdx+2) _ = f.SetCellValue(sheet, cell, vw.cols(m)[c]) } } } w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("绩效报表.xlsx")) _, _ = f.WriteTo(w) } } // normalizeExportRange MES 侧导出时间硬规则(与 WMS 同一套): // ① 起止都空 → 默认近 3 个月;② 只传一个 → 报错;③ 间隔 > 1 年 → 报错;④ 结束早于起始 → 报错。 func normalizeExportRange(startDate, endDate string) (string, string, error) { if startDate == "" && endDate == "" { now := time.Now() return now.AddDate(0, -3, 0).Format("2006-01-02"), now.Format("2006-01-02"), nil } if startDate == "" || endDate == "" { return "", "", errors.New("导出必须同时提供起始时间与结束时间") } st, err1 := time.ParseInLocation("2006-01-02", startDate, time.Local) et, err2 := time.ParseInLocation("2006-01-02", endDate, time.Local) if err1 != nil || err2 != nil { return "", "", errors.New("时间格式应为 YYYY-MM-DD") } if et.Before(st) { return "", "", errors.New("结束时间不能早于起始时间") } if et.Sub(st) > 366*24*time.Hour { return "", "", errors.New("导出时间间隔不能超过 1 年") } return startDate, endDate, nil }