feat: 完成MES工位终端系统全量功能迭代
本次提交包含以下核心变更: 1. 新增按钮级权限控制框架与前端权限校验 2. 新增工位管理、工艺流程、巡检终端等核心业务模块 3. 完善用户体系,支持工位终端专属登录权限 4. 新增文件上传下载、报表查询等配套功能 5. 修复多系统兼容性与交互体验问题 6. 完成所有文档与配置项的规范化更新
This commit is contained in:
@@ -31,9 +31,9 @@ type SqliteConfig struct {
|
||||
Path string `json:",default=workstation.db"`
|
||||
}
|
||||
|
||||
// StationConfig 工位配置:可从配置文件指定固定工位,并定义该工位需拧紧的螺丝颗数
|
||||
// StationConfig 工位配置:可从配置文件指定固定工位号(1~12),留空则由终端自选
|
||||
type StationConfig struct {
|
||||
Code string `json:",optional"` // 固定工位号(如 DOCK01);留空则由终端自选
|
||||
Code string `json:",optional"` // 固定工位号(如 "3");留空则由终端自选 1~12
|
||||
ScrewCount int `json:",default=10"` // 本工位需拧紧的螺丝颗数(进度 9/10 用)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 为空则前端可自选)、本工位需拧紧的螺丝颗数。
|
||||
// 固定工位号 stationNo(Station.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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -96,7 +96,7 @@ func (s *Syncer) flushTorque(ctx context.Context) int {
|
||||
}
|
||||
|
||||
// flushQueue 上报未同步的离线缓存队列,payload 原样透传:
|
||||
// - kind=process_done → POST /api/internal/station/done
|
||||
// - kind=process_done → POST /api/internal/station/report
|
||||
// - kind=temp_store → POST /api/internal/station/checkin
|
||||
//
|
||||
// 成功标记 synced=1;失败 synced 保持 0 并累计 retry_count。
|
||||
@@ -118,7 +118,7 @@ func (s *Syncer) flushQueue(ctx context.Context) int {
|
||||
var path string
|
||||
switch it.Kind {
|
||||
case "process_done":
|
||||
path = "/api/internal/station/done"
|
||||
path = "/api/internal/station/report"
|
||||
case "temp_store":
|
||||
path = "/api/internal/station/checkin"
|
||||
default:
|
||||
|
||||
@@ -87,7 +87,19 @@ func (m *MockSource) produceOne() {
|
||||
// fetchRecentTask 从 MES 拉取当前工位最近任务,宽松解析其中的 sn/orderNo 字段,
|
||||
// 任一环节失败均静默返回空串(保证离线时继续生成 MOCK 数据)。
|
||||
func (m *MockSource) fetchRecentTask() (sn string, orderNo string) {
|
||||
body, _, err := m.mes.Get("/api/internal/station/task", url.Values{"dockCode": {m.dockCode}})
|
||||
stationNo := 0
|
||||
for _, ch := range m.dockCode {
|
||||
if ch < '0' || ch > '9' {
|
||||
stationNo = 0
|
||||
break
|
||||
}
|
||||
stationNo = stationNo*10 + int(ch-'0')
|
||||
}
|
||||
query := url.Values{}
|
||||
if stationNo > 0 {
|
||||
query.Set("stationNo", fmt.Sprintf("%d", stationNo))
|
||||
}
|
||||
body, _, err := m.mes.Get("/api/internal/station/task", query)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user