本次提交覆盖多个业务模块的功能完善与体验优化:
1. **鉴权与配置调整**:
- 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
- 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
- 工位配置放开1~12限制,改为仅校验大于0
2. **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3. **功能新增**:
- 新增工位类型、工艺路线与产线点位台账模块
- 添加工艺PDF预览面板、工位终端代理转发接口
- 新增操作日志按操作人列表筛选、工位登出日志记录
- 新增PLC移料指令与产线点位状态管理
4. **业务流程优化**:
- 调整BOM物料删除校验逻辑,优化工单备料计算
- 补充物料图号、检测单号等追溯字段
- 完善工艺流程图与工位绑定关系说明
- 优化前端页面文案与交互细节
5. **代码规范与维护**:
- 新增通用工具函数与前端静态资源
- 整理路由权限与中间件逻辑
- 修复部分接口与配置的不兼容问题
288 lines
9.9 KiB
Go
288 lines
9.9 KiB
Go
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
|
|
}
|