feat&refactor: 完成多模块功能迭代与配置优化

本次提交覆盖多个业务模块的功能完善与体验优化:
1.  **鉴权与配置调整**:
    - 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
    - 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
    - 工位配置放开1~12限制,改为仅校验大于0
2.  **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3.  **功能新增**:
    - 新增工位类型、工艺路线与产线点位台账模块
    - 添加工艺PDF预览面板、工位终端代理转发接口
    - 新增操作日志按操作人列表筛选、工位登出日志记录
    - 新增PLC移料指令与产线点位状态管理
4.  **业务流程优化**:
    - 调整BOM物料删除校验逻辑,优化工单备料计算
    - 补充物料图号、检测单号等追溯字段
    - 完善工艺流程图与工位绑定关系说明
    - 优化前端页面文案与交互细节
5.  **代码规范与维护**:
    - 新增通用工具函数与前端静态资源
    - 整理路由权限与中间件逻辑
    - 修复部分接口与配置的不兼容问题
This commit is contained in:
SunYF
2026-09-10 16:59:15 +08:00
parent 6d8bb75696
commit 0ab21b1234
202 changed files with 21397 additions and 3172 deletions
+37 -10
View File
@@ -78,23 +78,50 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
httpx.Fail(w, 1001, err.Error())
return
}
// 操作日志:登录成功(写失败不阻断登录)
svcCtx.EventLog.Write(r.Context(), "auth.login", "", req.Username, "auth", req.Username,
"登录成功", map[string]any{"ip": clientIP(r)})
httpx.Ok(w, data)
}
}
func RefreshHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
// clientIP 取客户端 IP(优先代理头,兜底 RemoteAddr)
func clientIP(r *http.Request) string {
if v := r.Header.Get("X-Real-IP"); v != "" {
return v
}
if v := r.Header.Get("X-Forwarded-For"); v != "" {
for i := 0; i < len(v); i++ {
if v[i] == ',' {
return strings.TrimSpace(v[:i])
}
}
return strings.TrimSpace(v)
}
if host := r.RemoteAddr; host != "" {
if i := strings.LastIndex(host, ":"); i > 0 {
return host[:i]
}
return host
}
return ""
}
// LogoutHandler POST /api/v1/logout 退出登录(需登录 JWT
// 记录退出日志;登录态清除由前端完成(无状态 JWT 无服务端会话)。
func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.RefreshReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
username, _ := r.Context().Value("username").(string)
if username == "" {
if u, err := svcCtx.EntClient.User.Get(r.Context(), uidFromRequest(r, svcCtx)); err == nil {
username = u.Username
}
}
data, err := logic.New(svcCtx).Refresh(r.Context(), req)
if err != nil {
httpx.Fail(w, 1002, err.Error())
return
if username != "" {
svcCtx.EventLog.Write(r.Context(), "auth.logout", "", username, "auth", username,
"退出登录", map[string]any{"ip": clientIP(r)})
}
httpx.Ok(w, data)
httpx.OkMessage(w, "已退出", nil)
}
}
+156
View File
@@ -0,0 +1,156 @@
package handler
import (
"net/http"
"strings"
"bj_power_mes/common/httpx"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/svc"
)
// OccupyPointHandler POST /line/occupy(点位人工记账:IN 上料占位 / OUT 下料释放)
// 供工位终端在无 PLC 联调时、或上料位(IN)/末端下料检测位(11/12) 人工操作时维护点位台账(唯一事实源)。
func OccupyPointHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
PointNo string `json:"pointNo"`
Sn string `json:"sn"`
OrderNo string `json:"orderNo"`
Operator string `json:"operator"`
Action string `json:"action"` // IN=上料 / OUT=下料
}
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
action := strings.ToUpper(strings.TrimSpace(req.Action))
if action == "" {
action = "IN"
}
if action != "IN" && action != "OUT" {
httpx.BadRequest(w, "action 仅支持 IN(上料) / OUT(下料)")
return
}
op := req.Operator
if op == "" {
op = operator(r, "")
}
if err := logic.New(svcCtx).OccupyPoint(r.Context(), req.PointNo, req.Sn, req.OrderNo, op, action); err != nil {
httpx.Fail(w, 2407, err.Error())
return
}
msg := "上料已记账"
if action == "OUT" {
msg = "下料已释放"
}
httpx.OkMessage(w, msg, nil)
}
}
// ListPointsHandler GET /line/points(产线点位台账:上料位/工位/缓存位/接驳台/末端 当前工件)
func ListPointsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_ = logic.New(svcCtx).InitLinePoints(r.Context())
data, err := logic.New(svcCtx).ListPoints(r.Context())
if err != nil {
httpx.Fail(w, 2401, err.Error())
return
}
httpx.Ok(w, data)
}
}
// IssueMoveHandler POST /line/move(上位机下发移料指令:起点位 → 目标点位)
func IssueMoveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Sn string `json:"sn"`
FromPoint string `json:"fromPoint"`
ToPoint string `json:"toPoint"`
OrderNo string `json:"orderNo"`
}
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
data, err := logic.New(svcCtx).IssueMove(r.Context(), req.Sn, req.FromPoint, req.ToPoint, req.OrderNo, operator(r, ""))
if err != nil {
httpx.Fail(w, 2402, err.Error())
return
}
httpx.OkMessage(w, "移料指令已下发", data)
}
}
// AckMoveHandler POST /line/move/ackPLC 应答)
func AckMoveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
CmdNo string `json:"cmdNo"`
}
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).AckMove(r.Context(), req.CmdNo); err != nil {
httpx.Fail(w, 2403, err.Error())
return
}
httpx.OkMessage(w, "已应答", nil)
}
}
// ArriveMoveHandler POST /line/move/arrive(工件到位:回写台账并闭环指令)
func ArriveMoveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
CmdNo string `json:"cmdNo"`
}
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).ArriveMove(r.Context(), req.CmdNo); err != nil {
httpx.Fail(w, 2404, err.Error())
return
}
httpx.OkMessage(w, "到位已记账", nil)
}
}
// ListMoveCmdsHandler GET /line/moves?status=&sn=&limit=
func ListMoveCmdsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data, err := logic.New(svcCtx).ListMoveCmds(r.Context(), q.Get("status"), q.Get("sn"),
atoiDefault(q.Get("limit"), 50))
if err != nil {
httpx.Fail(w, 2405, err.Error())
return
}
httpx.Ok(w, data)
}
}
// CallMaterialHandler POST /line/call-material(工位终端叫料:选接驳台)
func CallMaterialHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
StationNo int `json:"stationNo"`
Dock string `json:"dock"`
OrderNo string `json:"orderNo"`
Items []logic.CallMaterialItem `json:"items"`
}
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
n, err := logic.New(svcCtx).CallMaterial(r.Context(), req.StationNo, req.Dock, req.OrderNo, operator(r, ""), req.Items)
if err != nil {
httpx.Fail(w, 2406, err.Error())
return
}
httpx.OkMessage(w, "叫料已提交", map[string]any{"count": n})
}
}
+2
View File
@@ -29,6 +29,8 @@ 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",
"POST:/api/v1/process-routes": "produce.route:add",
"DELETE:/api/v1/process-routes/*": "produce.route:delete",
// 装机绑定(报工前扫料/撤销,属报工操作域)
"POST:/api/v1/binds": "produce.scan",
"POST:/api/v1/binds/remove": "produce.scan",
+1 -1
View File
@@ -190,7 +190,7 @@ func buildCard(ctx context.Context, svcCtx *svc.ServiceContext, sn string) (*car
missing = append(missing, "拧紧数据")
}
// 关联物料(批次/精密件SN
// 关联物料(批次/电气件SN
assoc, _ := client.AssociationTrace.Query().
Where(associationtrace.FinishedSn(sn)).First(ctx)
if assoc != nil {
@@ -0,0 +1,62 @@
package handler
import (
"net/http"
"bj_power_mes/common/httpx"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/svc"
)
// ListRoutesHandler GET /process-routes?name=&status=&page=&pageSize=
func ListRoutesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data, err := logic.New(svcCtx).ListRoutes(r.Context(), q.Get("name"), q.Get("status"),
atoiDefault(q.Get("page"), 0), atoiDefault(q.Get("pageSize"), 20))
if err != nil {
httpx.Fail(w, 2301, err.Error())
return
}
httpx.Ok(w, data)
}
}
// SaveRouteHandler POST /process-routes(新建或更新工艺路线,含段)
func SaveRouteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.RouteReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).SaveRoute(r.Context(), req, operator(r, "")); err != nil {
httpx.Fail(w, 2302, err.Error())
return
}
httpx.OkMessage(w, "工艺路线已保存", nil)
}
}
// GetRouteHandler GET /process-routes/:id
func GetRouteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, err := logic.New(svcCtx).GetRoute(r.Context(), pathId(r))
if err != nil {
httpx.Fail(w, 2303, err.Error())
return
}
httpx.Ok(w, data)
}
}
// DeleteRouteHandler DELETE /process-routes/:id
func DeleteRouteHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := logic.New(svcCtx).DeleteRoute(r.Context(), pathId(r), operator(r, "")); err != nil {
httpx.Fail(w, 2304, err.Error())
return
}
httpx.OkMessage(w, "工艺路线已删除", nil)
}
}
@@ -1,10 +1,15 @@
package production
import (
"context"
"net/http"
"strconv"
"strings"
"bj_power_mes/common/httpx"
tokenx "bj_power_mes/common/token"
"bj_power_mes/ent"
"bj_power_mes/ent/user"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/svc"
)
@@ -110,6 +115,9 @@ func ListSemiFlowsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
// ---------- 操作日志 ----------
// ListEventLogsHandler GET /api/v1/event-logs 操作日志分页查询
// 可见性:adminSUPER_ADMIN)可看全部并可按操作人筛选;普通用户强制只看自己的日志。
// operatorName:操作人中文名(users.name 批量映射,历史数据同样生效)。
func ListEventLogsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
@@ -117,12 +125,109 @@ func ListEventLogsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
pageSize := atoiDefault(q.Get("pageSize"), 20)
fromMs, _ := strconv.ParseInt(q.Get("from"), 10, 64)
toMs, _ := strconv.ParseInt(q.Get("to"), 10, 64)
// 当前身份:uid → user(历史数据 operator 中文名/用户名混杂,过滤需同时带两者)
uid := currentUid(r, svcCtx)
me, _ := svcCtx.EntClient.User.Get(context.Background(), uid)
meUsername, _ := r.Context().Value("username").(string)
if me == nil && meUsername != "" {
me, _ = svcCtx.EntClient.User.Query().Where(user.UsernameEQ(meUsername)).First(context.Background())
}
var candidates []string
isAdmin := false
if me != nil {
candidates = []string{me.Username, me.Name}
if rl, err := svcCtx.EntClient.Role.Get(context.Background(), me.RoleId); err == nil && rl.Code == "SUPER_ADMIN" {
isAdmin = true
}
}
explicitFilter := q.Get("operator")
if !isAdmin {
// 普通用户仅能查看自己的操作日志(用户名 + 中文名任一命中),忽略显式筛选
explicitFilter = ""
}
list, total, err := logic.New(svcCtx).ListEventLogs(r.Context(),
q.Get("orderNo"), q.Get("operator"), q.Get("eventType"), fromMs, toMs, page, pageSize)
q.Get("orderNo"), operatorCandidates(explicitFilter, candidates), q.Get("eventType"), fromMs, toMs, page, pageSize)
if err != nil {
httpx.Fail(w, 3308, err.Error())
return
}
httpx.Ok(w, map[string]any{"list": list, "total": total})
// 批量映射操作人中文名,缺档显示原用户名
nameMap := realNameMapOf(svcCtx, list)
rows := make([]map[string]any, 0, len(list))
for _, e := range list {
name := nameMap[e.Operator]
if name == "" {
name = e.Operator
}
rows = append(rows, map[string]any{
"id": e.ID,
"eventType": e.EventType,
"workOrderNo": e.WorkOrderNo,
"description": e.Description,
"entityType": e.EntityType,
"entityId": e.EntityId,
"operator": e.Operator,
"operatorName": name,
"payload": e.Payload,
"createdAt": e.CreatedAt.UnixMilli(),
})
}
httpx.Ok(w, map[string]any{"list": rows, "total": total, "isAdmin": isAdmin})
}
}
// currentUid 取当前登录用户ID:优先 JWT 上下文,缺失时回退解析 Bearer token
func currentUid(r *http.Request, svcCtx *svc.ServiceContext) int {
switch v := r.Context().Value("userId").(type) {
case float64:
return int(v)
case int:
return v
case int64:
return int(v)
}
auth := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if auth != "" && strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
if claims, err := tokenx.Parse(svcCtx.Config.Auth.AccessSecret, auth); err == nil {
return claims.UserId
}
}
return 0
}
// operatorCandidates 计算操作人过滤候选:admin 显式筛选时精确匹配输入值;否则取当前用户身份候选
func operatorCandidates(filter string, self []string) []string {
if filter != "" {
return []string{filter}
}
return self
}
// realNameMapOf 批量取操作人中文名(users.name)
func realNameMapOf(svcCtx *svc.ServiceContext, logs []*ent.EventLog) map[string]string {
set := make([]string, 0, 8)
seen := map[string]bool{}
for _, e := range logs {
if e.Operator != "" && !seen[e.Operator] {
seen[e.Operator] = true
set = append(set, e.Operator)
}
}
out := map[string]string{}
if len(set) == 0 {
return out
}
us, err := svcCtx.EntClient.User.Query().Where(user.UsernameIn(set...)).All(context.Background())
if err != nil {
return out
}
for _, u := range us {
if u.Name != "" {
out[u.Username] = u.Name
}
}
return out
}
+37
View File
@@ -0,0 +1,37 @@
package handler
import (
"net/http"
"strings"
"time"
tokenx "bj_power_mes/common/token"
"bj_power_mes/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
// RenewalMiddleware 滑动续签中间件(对齐 WMS 的 X-Renewed-Token 模式):
// 每个携带有效 Bearer token 的请求都重签一个同负载的新 token,
// 会话有效期 = 距最后一次请求 sessionExpire 秒(默认 1 小时),闲置超时由 jwtAuth 返回 401。
// 无 Bearer 头(内部 X-API-TOKEN、登录接口、静态资源)直接放行不做任何处理。
func RenewalMiddleware(svcCtx *svc.ServiceContext) rest.Middleware {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
tokenStr := strings.TrimPrefix(authHeader, "Bearer ")
if claims, err := tokenx.Parse(svcCtx.Config.Auth.AccessSecret, tokenStr); err == nil && time.Until(claims.ExpiresAt.Time) > 0 {
expire := svcCtx.Config.Auth.AccessExpire
if expire <= 0 {
expire = 3600
}
if newToken, err := tokenx.Issue(svcCtx.Config.Auth.AccessSecret, time.Duration(expire)*time.Second, *claims); err == nil {
w.Header().Set("X-Renewed-Token", newToken)
}
}
}
next(w, r)
}
}
}
+1 -1
View File
@@ -13,7 +13,6 @@ func RegisterRoutes(server *rest.Server, serverCtx *svc.ServiceContext) {
// 免鉴权
server.AddRoutes([]rest.Route{
{Method: http.MethodPost, Path: "/login", Handler: LoginHandler(serverCtx)},
{Method: http.MethodPost, Path: "/refresh", Handler: RefreshHandler(serverCtx)},
}, rest.WithPrefix("/api/v1"), rest.WithMaxBytes(serverCtx.Config.Upload.MaxMB<<20))
// JWT 保护:用户信息 + 用户/角色/权限 管理
@@ -21,6 +20,7 @@ func RegisterRoutes(server *rest.Server, serverCtx *svc.ServiceContext) {
{Method: http.MethodGet, Path: "/userinfo", Handler: UserInfoHandler(serverCtx)},
{Method: http.MethodPost, Path: "/seed", Handler: SeedHandler(serverCtx)},
{Method: http.MethodPost, Path: "/user/change-password", Handler: ChangePasswordHandler(serverCtx)},
{Method: http.MethodPost, Path: "/logout", Handler: LogoutHandler(serverCtx)},
{Method: http.MethodGet, Path: "/users", Handler: ListUsersHandler(serverCtx)},
{Method: http.MethodPost, Path: "/users", Handler: CreateUserHandler(serverCtx)},
@@ -41,6 +41,9 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/station/login", Handler: internal(StationLoginInternalHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/station/logout", Handler: internal(StationLogoutInternalHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/station/change-password", Handler: internal(StationChangePasswordInternalHandler(serverCtx)),
})
@@ -81,6 +84,29 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
Method: http.MethodPost, Path: "/api/internal/dock/pallet/set", Handler: internal(production.SetDockPalletInternalHandler(serverCtx)),
})
// ---------- 产线点位/移料(供工位终端调用,X-API-TOKEN 保护)----------
server.AddRoute(rest.Route{
Method: http.MethodGet, Path: "/api/internal/line/points", Handler: internal(ListPointsHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/line/move", Handler: internal(IssueMoveHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/line/move/ack", Handler: internal(AckMoveHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/line/move/arrive", Handler: internal(ArriveMoveHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodGet, Path: "/api/internal/line/moves", Handler: internal(ListMoveCmdsHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/line/call-material", Handler: internal(CallMaterialHandler(serverCtx)),
})
server.AddRoute(rest.Route{
Method: http.MethodPost, Path: "/api/internal/line/occupy", Handler: internal(OccupyPointHandler(serverCtx)),
})
// ---------- JWT 业务 API ----------
jwtRoutes := []rest.Route{
{Method: http.MethodGet, Path: "/product-types", Handler: production.ListProductTypesHandler(serverCtx)},
@@ -143,6 +169,16 @@ 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.MethodGet, Path: "/process-routes", Handler: ListRoutesHandler(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)},
{Method: http.MethodPost, Path: "/line/move/arrive", Handler: ArriveMoveHandler(serverCtx)},
{Method: http.MethodGet, Path: "/line/moves", Handler: ListMoveCmdsHandler(serverCtx)},
{Method: http.MethodPost, Path: "/line/call-material", Handler: CallMaterialHandler(serverCtx)},
{Method: http.MethodPost, Path: "/process-routes", Handler: SaveRouteHandler(serverCtx)},
{Method: http.MethodGet, Path: "/process-routes/:id", Handler: GetRouteHandler(serverCtx)},
{Method: http.MethodDelete, Path: "/process-routes/:id", Handler: DeleteRouteHandler(serverCtx)},
// ---------- PAD 巡检终端(块8 ----------
{Method: http.MethodPost, Path: "/inspections", Handler: CreateInspectionHandler(serverCtx)},
+24
View File
@@ -53,10 +53,34 @@ func StationLoginInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
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) {