feat: 完成MES工位终端系统全量功能迭代

本次提交包含以下核心变更:
1. 新增按钮级权限控制框架与前端权限校验
2. 新增工位管理、工艺流程、巡检终端等核心业务模块
3. 完善用户体系,支持工位终端专属登录权限
4. 新增文件上传下载、报表查询等配套功能
5. 修复多系统兼容性与交互体验问题
6. 完成所有文档与配置项的规范化更新
This commit is contained in:
SunYF
2026-08-31 08:06:55 +08:00
parent dab03ac0cf
commit 4ec6784629
99 changed files with 17436 additions and 479 deletions
+19 -36
View File
@@ -25,13 +25,15 @@ func healthHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
}
// loginHandler 本地 SQLite + bcrypt 校验登录,返回 {token, expireAt, user}
// 账号自动记录为操作人(前端通过 localStorage 的 user 保存操作人)
// loginHandler 登录代理 MES /api/internal/station/login(块1:用户体系来源于 MES)
// 前端提交 {username, password, stationNo}MES 校验工位终端密码 + 工位权限并签发同密钥 JWT
// 响应体原样透传({code,message,data:{accessToken,expireAt,user,stations,stationNo}})。
func loginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
Username string `json:"username"`
Password string `json:"password"`
StationNo int `json:"stationNo"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
@@ -42,46 +44,27 @@ func loginHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return
}
u, err := ctx.Store.GetUserByUsername(strings.TrimSpace(req.Username))
if err != nil {
fail(w, http.StatusUnauthorized, "用户名或密码错误")
return
}
if bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(req.Password)) != nil {
fail(w, http.StatusUnauthorized, "用户名或密码错误")
return
}
token, expireAt, err := auth.SignToken(
ctx.Config.Auth.AccessSecret,
ctx.Config.Auth.AccessExpire,
auth.TokenInfo{UserID: u.ID, Username: u.Username, RealName: u.RealName, Role: u.Role},
)
if err != nil {
fail(w, http.StatusInternalServerError, "签发 token 失败")
return
}
ctx.Store.AddEventLog("", u.RealName, "login", "账号登录")
ok(w, map[string]any{
"token": token,
"expireAt": expireAt,
"user": map[string]any{
"id": u.ID,
"username": u.Username,
"realName": u.RealName,
"role": u.Role,
},
body, _, err := ctx.Mes.Post("/api/internal/station/login", map[string]any{
"username": strings.TrimSpace(req.Username),
"password": req.Password,
"stationNo": req.StationNo,
})
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
}
// configHandler GET /api/config 返回工位配置:
// 固定工位号(Station.Code 为空则前端可自选)、本工位需拧紧的螺丝颗数。
// 固定工位号 stationNoStation.Code 为空则前端可自选 1~12)、本工位需拧紧的螺丝颗数。
func configHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ok(w, map[string]any{
"station": ctx.Config.Station.Code,
"stationNo": atoi(ctx.Config.Station.Code, 0),
"screwCount": ctx.Config.Station.ScrewCount,
})
}
@@ -23,6 +23,10 @@ func atoi(s string, def int) int {
return n
}
func itoa(n int) string {
return strconv.Itoa(n)
}
// errString 压缩错误信息长度便于前端展示。
func errString(err error) string {
s := err.Error()
+33 -5
View File
@@ -13,17 +13,45 @@ import (
"bj_power_workstation/internal/svc"
)
// taskCurrentHandler 代理 MES GET /api/internal/station/task?dockCode=。
// taskCurrentHandler 代理 MES GET /api/internal/station/task?stationNo=。
// 响应体原样透传(MES 已按 {code,message,data} 封装,不再二次包装)。
func taskCurrentHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dockCode := strings.TrimSpace(r.URL.Query().Get("dockCode"))
if dockCode == "" {
fail(w, http.StatusBadRequest, "缺少 dockCode 参数")
stationNo := atoi(strings.TrimSpace(r.URL.Query().Get("stationNo")), 0)
if stationNo <= 0 {
fail(w, http.StatusBadRequest, "缺少 stationNo 参数")
return
}
body, _, err := ctx.Mes.Get("/api/internal/station/task", url.Values{"dockCode": {dockCode}})
body, _, err := ctx.Mes.Get("/api/internal/station/task", url.Values{"stationNo": {strconv.Itoa(stationNo)}})
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
}
// workloadHandler 代理 MES GET /api/internal/station/workload?operator=&from=&to=(块6)。
// 工位终端「我的工作量」仅查询当前登录人数据;MES 响应体原样透传。
func workloadHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
operator := strings.TrimSpace(r.URL.Query().Get("operator"))
from := strings.TrimSpace(r.URL.Query().Get("from"))
to := strings.TrimSpace(r.URL.Query().Get("to"))
params := url.Values{}
if operator != "" {
params.Set("operator", operator)
}
if from != "" {
params.Set("from", from)
}
if to != "" {
params.Set("to", to)
}
body, _, err := ctx.Mes.Get("/api/internal/station/workload", params)
if err != nil {
fail(w, http.StatusBadGateway, "MES 服务不可达:"+errString(err))
return
+35 -27
View File
@@ -10,24 +10,30 @@ import (
)
// processDoneHandler POST /api/report/process-done
// {dockCode, orderNo, sn, processCode, operator}
// 流程:先冲刷一轮积压 → 实时报 MES /api/internal/station/done
// {sn, processCode, stationNo, steps:[{stepId,name,value,text}]}
// 流程:先冲刷一轮积压 → 实时报 MES /api/internal/station/report
// MES 不可达则写入 report_queue(kind=process_done) 并返回 data.queued=true(离线缓存稍后自动重传)。
func processDoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
DockCode string `json:"dockCode"`
OrderNo string `json:"orderNo"`
Sn string `json:"sn"`
ProcessCode string `json:"processCode"`
Operator string `json:"operator"`
OrderNo string `json:"orderNo"`
ProcessCode int `json:"processCode"`
StationNo int `json:"stationNo"`
Steps []struct {
StepId int `json:"stepId"`
Name string `json:"name"`
Value float64 `json:"value"`
Text string `json:"text"`
} `json:"steps"`
Operator string `json:"operator"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.DockCode == "" {
fail(w, http.StatusBadRequest, "缺少 dockCode")
if req.Sn == "" || req.ProcessCode <= 0 || req.StationNo <= 0 {
fail(w, http.StatusBadRequest, "sn/processCode/stationNo 必填")
return
}
@@ -35,18 +41,19 @@ func processDoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
ctx.Syncer.RunOnce(r.Context())
body := map[string]any{
"dockCode": req.DockCode,
"orderNo": req.OrderNo,
"sn": req.Sn,
"orderNo": req.OrderNo,
"processCode": req.ProcessCode,
"stationNo": req.StationNo,
"steps": req.Steps,
"operator": req.Operator,
}
payloadBytes, _ := json.Marshal(body)
ctx.Store.AddEventLog(req.OrderNo, req.Operator, "process_done",
"工序完成上报 dockCode="+req.DockCode+" sn="+req.Sn+" processCode="+req.ProcessCode)
"工序完成上报 sn="+req.Sn+" stationNo="+itoa(req.StationNo)+" processCode="+itoa(req.ProcessCode))
if _, _, err := ctx.Mes.Post("/api/internal/station/done", json.RawMessage(payloadBytes)); err != nil {
if _, _, err := ctx.Mes.Post("/api/internal/station/report", json.RawMessage(payloadBytes)); err != nil {
if qerr := ctx.Store.InsertQueueItem("process_done", string(payloadBytes)); qerr != nil {
fail(w, http.StatusInternalServerError, "上报失败且离线队列写入失败:"+qerr.Error())
return
@@ -61,22 +68,23 @@ func processDoneHandler(ctx *svc.ServiceContext) http.HandlerFunc {
}
// tempStoreHandler POST /api/report/temp-store
// {dockCode, orderNo, sn, operator} → 调 MES /api/internal/station/checkin
// body 附带 type=temp_store不可达时以同样 payload 写入 report_queue(kind=temp_store)。
// {sn, orderNo, stationNo, processCode, operator} → 调 MES /api/internal/station/checkin(半成品入库)。
// 不可达时以同样 payload 写入 report_queue(kind=temp_store)。
func tempStoreHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
DockCode string `json:"dockCode"`
OrderNo string `json:"orderNo"`
Sn string `json:"sn"`
Operator string `json:"operator"`
Sn string `json:"sn"`
OrderNo string `json:"orderNo"`
StationNo int `json:"stationNo"`
ProcessCode int `json:"processCode"`
Operator string `json:"operator"`
}
if err := parseJSON(r, &req); err != nil {
fail(w, http.StatusBadRequest, "参数错误")
return
}
if req.DockCode == "" {
fail(w, http.StatusBadRequest, "缺少 dockCode")
if req.Sn == "" {
fail(w, http.StatusBadRequest, "缺少 sn")
return
}
@@ -84,16 +92,16 @@ func tempStoreHandler(ctx *svc.ServiceContext) http.HandlerFunc {
ctx.Syncer.RunOnce(r.Context())
body := map[string]any{
"type": "temp_store", // checkin 类型标识:暂存退库
"dockCode": req.DockCode,
"orderNo": req.OrderNo,
"sn": req.Sn,
"operator": req.Operator,
"sn": req.Sn,
"orderNo": req.OrderNo,
"stationNo": req.StationNo,
"processCode": req.ProcessCode,
"operator": req.Operator,
}
payloadBytes, _ := json.Marshal(body)
ctx.Store.AddEventLog(req.OrderNo, req.Operator, "temp_store",
"暂存/退回库房 dockCode="+req.DockCode+" sn="+req.Sn)
"暂存/退回库房 sn="+req.Sn+" stationNo="+itoa(req.StationNo))
if _, _, err := ctx.Mes.Post("/api/internal/station/checkin", json.RawMessage(payloadBytes)); err != nil {
if qerr := ctx.Store.InsertQueueItem("temp_store", string(payloadBytes)); qerr != nil {
@@ -107,4 +115,4 @@ func tempStoreHandler(ctx *svc.ServiceContext) http.HandlerFunc {
ok(w, map[string]any{"queued": false})
}
}
}
@@ -29,6 +29,7 @@ func RegisterHandlers(server *rest.Server, ctx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{Method: http.MethodGet, Path: "/api/task/current", Handler: taskCurrentHandler(ctx)},
{Method: http.MethodGet, Path: "/api/workload", Handler: workloadHandler(ctx)},
{Method: http.MethodGet, Path: "/api/pdf", Handler: pdfQueryHandler(ctx)},
{Method: http.MethodGet, Path: "/pdfopen/:name", Handler: pdfOpenHandler(ctx)},
{Method: http.MethodGet, Path: "/api/tightening/list", Handler: tighteningListHandler(ctx)},