feat(mes): 添加定时同步可生产数量到WMS及BOM项相关标准字段

- 在main.go中添加定时任务每10分钟同步可生产数量到WMS系统
- 为BomItem实体添加relatedStandard字段及相关CRUD方法
- 为InspectionRecord实体添加reportNo、materialCode、materialName等字段
- 更新ent schema确保新字段的验证和默认值设置
- 添加必要的数据库迁移和字段映射逻辑
This commit is contained in:
SunYF
2026-09-17 12:49:07 +08:00
parent b4b274301d
commit 1cc93795ce
191 changed files with 24504 additions and 1250 deletions
@@ -2,14 +2,132 @@ package production
import (
"net/http"
"strconv"
"bj_power_mes/common/httpx"
"bj_power_mes/internal/logic"
"bj_power_mes/internal/svc"
"github.com/xuri/excelize/v2"
)
// ---------- BOM ----------
// ImportBomHandler POST /bom/import 产品物料清单(BOM) Excel 导入。
// 模板列:产品编号|物料清单名称|物料编码|名称|规格|单位|单台用量|损耗率|装配工位|相关标准|备注
func ImportBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(svcCtx.Config.Upload.MaxMB << 20); err != nil {
httpx.BadRequest(w, "上传文件过大或格式错误")
return
}
f, _, err := r.FormFile("file")
if err != nil {
httpx.BadRequest(w, "请选择 Excel 文件")
return
}
defer f.Close()
xlsx, err := excelize.OpenReader(f)
if err != nil {
httpx.Fail(w, 3109, "Excel 解析失败:"+err.Error())
return
}
sheet := xlsx.GetSheetName(0)
rows, err := xlsx.GetRows(sheet)
if err != nil {
httpx.Fail(w, 3110, "读取 Excel 失败:"+err.Error())
return
}
// 按 (产品编号, 物料清单名称) 分组,每组一次 SaveBom(覆盖式 upsert
type key struct{ pc, bn string }
groups := map[key][]logic.BomItemReq{}
order := []key{}
count := 0
for i, row := range rows {
if i == 0 {
continue // 跳过表头
}
if len(row) < 3 {
continue
}
productCode := trimCell(row, 0)
bomName := trimCell(row, 1)
materialCode := trimCell(row, 2)
if productCode == "" || materialCode == "" {
continue
}
if bomName == "" {
bomName = "默认"
}
pc := 0
if v := trimCell(row, 8); v != "" {
if n, e := strconv.Atoi(v); e == nil {
pc = n
}
}
item := logic.BomItemReq{
ProductCode: productCode,
BomName: bomName,
MaterialCode: materialCode,
MaterialName: trimCell(row, 3),
Spec: trimCell(row, 4),
Unit: trimCell(row, 5),
ManageMode: "2",
ProcessCode: &pc,
UnitQty: parseFloat(trimCell(row, 6)),
LossRate: parseFloat(trimCell(row, 7)),
RelatedStandard: trimCell(row, 9),
}
k := key{productCode, bomName}
if _, ok := groups[k]; !ok {
order = append(order, k)
}
groups[k] = append(groups[k], item)
count++
}
if count == 0 {
httpx.Fail(w, 3111, "未解析到有效 BOM 行(至少需产品编号与物料编码)")
return
}
for _, k := range order {
if err := logic.New(svcCtx).SaveBom(r.Context(), k.pc, k.bn, groups[k], operator(r, "")); err != nil {
httpx.Fail(w, 3112, "导入失败:"+err.Error())
return
}
}
httpx.OkMessage(w, "导入成功", map[string]any{"count": count})
}
}
func trimCell(row []string, i int) string {
if i >= len(row) {
return ""
}
return strconvTrimSpace(row[i])
}
func strconvTrimSpace(s string) string {
out := []rune{}
for _, c := range s {
if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
continue
}
out = append(out, c)
}
return string(out)
}
func parseFloat(s string) float64 {
if s == "" {
return 0
}
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return v
}
func SaveBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
@@ -2,6 +2,7 @@ package production
import (
"net/http"
"strings"
"time"
"bj_power_mes/common/httpx"
@@ -41,6 +42,28 @@ func PlcSendLogsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
// ---------- 拧紧 ----------
// parseTorqueTime 兼容 RFC3339 与纯日期(YYYY-MM-DD)两种查询参数;
// endOfDay=true 时把纯日期补到当日 23:59:59,保证「截止日期」含当天。
func parseTorqueTime(v string, endOfDay bool) time.Time {
v = strings.TrimSpace(v)
if v == "" {
return time.Time{}
}
var t time.Time
if err := t.UnmarshalText([]byte(v)); err == nil {
return t
}
for _, layout := range []string{"2006-01-02T15:04:05", "2006-01-02 15:04:05", "2006-01-02"} {
if p, err := time.ParseInLocation(layout, v, time.Local); err == nil {
if layout == "2006-01-02" && endOfDay {
p = p.Add(24*time.Hour - time.Second)
}
return p
}
}
return time.Time{}
}
func TorqueReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.TorqueReq
@@ -59,14 +82,9 @@ func TorqueReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
func TorqueRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
var from, to time.Time
if v := q.Get("from"); v != "" {
_ = from.UnmarshalText([]byte(v))
}
if v := q.Get("to"); v != "" {
_ = to.UnmarshalText([]byte(v))
}
data, err := logic.New(svcCtx).ListTorqueRecords(r.Context(), q.Get("sn"), q.Get("workOrderNo"), from, to)
from := parseTorqueTime(q.Get("from"), false)
to := parseTorqueTime(q.Get("to"), true)
data, err := logic.New(svcCtx).ListTorqueRecords(r.Context(), q.Get("sn"), q.Get("workOrderNo"), q.Get("stationNo"), from, to)
if err != nil {
httpx.Fail(w, 3204, err.Error())
return
@@ -75,6 +93,39 @@ func TorqueRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
// TorqueGroupsHandler GET /torque/groups 拧紧一级汇总(工单号/SN/日期/工位 四条件复合 + 应拧 vs 实拧满足判定)
func TorqueGroupsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
from := parseTorqueTime(q.Get("from"), false)
to := parseTorqueTime(q.Get("to"), true)
data, err := logic.New(svcCtx).ListTorqueGroups(r.Context(), q.Get("sn"), q.Get("workOrderNo"), q.Get("stationNo"),
from, to, atoiDefault(q.Get("page"), 1), atoiDefault(q.Get("pageSize"), 20))
if err != nil {
httpx.Fail(w, 3210, err.Error())
return
}
httpx.Ok(w, data)
}
}
// TorqueStatHandler GET /torque/stat 单工件单工位应拧 vs 实拧统计
func TorqueStatHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if q.Get("sn") == "" {
httpx.Fail(w, 3211, "缺少 sn 参数")
return
}
data, err := logic.New(svcCtx).TorqueScrewStat(r.Context(), q.Get("sn"), q.Get("stationNo"))
if err != nil {
httpx.Fail(w, 3211, err.Error())
return
}
httpx.Ok(w, data)
}
}
// ---------- 扫码报工 ----------
func ScanReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
@@ -124,32 +175,6 @@ func ProcessStepsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
// TorqueManualAddHandler POST /torque/manual-add 拧紧数据补录(设备漏传/手工修正)
func TorqueManualAddHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req struct {
Sn string `json:"sn"`
WorkOrderNo string `json:"workOrderNo"`
ScrewNo string `json:"screwNo"`
StationNo string `json:"stationNo"`
Strain float64 `json:"strain"`
Angle float64 `json:"angle"`
Result string `json:"result"`
Reason string `json:"reason"`
}
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).AddTorqueRecord(r.Context(), req.Sn, req.WorkOrderNo, req.ScrewNo,
req.StationNo, req.Strain, req.Angle, req.Result, req.Reason, operator(r, "系统管理员")); err != nil {
httpx.Fail(w, 3205, err.Error())
return
}
httpx.OkMessage(w, "补录成功", nil)
}
}
// TorqueAuditHandler POST /torque/audit 拧紧记录审核签字
func TorqueAuditHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
@@ -94,7 +94,7 @@ func WorkpieceTraceInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc
func OrderQueryInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orderNo := r.URL.Query().Get("orderNo")
list, err := logic.New(svcCtx).ListWorkOrders(r.Context(), orderNo, "", "", "")
list, err := logic.New(svcCtx).ListWorkOrders(r.Context(), orderNo, "", "", "", "", "", "", "")
if err != nil {
httpx.Fail(w, 2004, err.Error())
return
@@ -103,6 +103,69 @@ func OrderQueryInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
// ---------- 工位终端 0/13 虚拟工位内部接口(第五步 5.2/5.4 ----------
// InProcessInternalHandler 在制品列表(工位终端「在制品默认展示」/上下线面板)。
// GET ?stationNo=<=0 全部;0=上线位, 13=下线位, 其余为物理工位),返回 {list}。
func InProcessInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stationNo := atoiDefault(r.URL.Query().Get("stationNo"), 0)
data, err := logic.New(svcCtx).WorkpiecesInProcess(r.Context(), stationNo)
if err != nil {
httpx.Fail(w, 2006, err.Error())
return
}
httpx.Ok(w, map[string]any{"list": data})
}
}
// OnlineWorkpieceInternalHandler 工件进线建档(工位终端上线位 StationNo=0)。
// POST OnlineReq{sn,orderNo,workOrderId,processSeq}operator 从 query 兜底(内部接口无 JWT)。
func OnlineWorkpieceInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.OnlineReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).OnlineWorkpiece(r.Context(), req, operator(r, r.URL.Query().Get("operator"))); err != nil {
httpx.Fail(w, 2007, err.Error())
return
}
httpx.OkMessage(w, "进线登记成功", nil)
}
}
// DoneWorkpieceInternalHandler 工件完工入库(工位终端下线位 StationNo=13)。
// POST DoneReq{sn,batchItems,serialItems};完工内部自动回流 WMS 成品库存。
func DoneWorkpieceInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.DoneReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).DoneWorkpiece(r.Context(), req, operator(r, r.URL.Query().Get("operator"))); err != nil {
httpx.Fail(w, 2008, err.Error())
return
}
httpx.OkMessage(w, "完工成功", nil)
}
}
// BomInternalHandler BOM 物料清单(工位终端上线位「领料确认」按物料清单展示应领料)。
// GET ?productCode=&bomName=,返回 []*ent.BomItem。
func BomInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, err := logic.New(svcCtx).ListBom(r.Context(), r.URL.Query().Get("productCode"), r.URL.Query().Get("bomName"))
if err != nil {
httpx.Fail(w, 2009, err.Error())
return
}
httpx.Ok(w, data)
}
}
// ---------- 看板内部接口 ----------
// DashboardSnapshotInternalHandler 看板全量快照。
@@ -113,7 +113,8 @@ func ListWorkOrdersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
data, err := logic.New(svcCtx).ListWorkOrders(r.Context(),
q.Get("orderNo"), q.Get("status"), q.Get("productCode"), q.Get("productName"))
q.Get("orderNo"), q.Get("status"), q.Get("productCode"), q.Get("productName"),
q.Get("contractNo"), q.Get("projectNo"), q.Get("dueFrom"), q.Get("dueTo"))
if err != nil {
httpx.Fail(w, 3004, err.Error())
return
@@ -194,6 +195,22 @@ func DeleteWorkOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
}
}
// AutoScheduleHandler POST /work-orders/auto-schedule 引导式自动排产(生成多条 PENDING 排产单)
func AutoScheduleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req logic.AutoScheduleReq
if err := httpx.ParseJSON(r, &req); err != nil {
httpx.BadRequest(w, "请求体解析失败")
return
}
if err := logic.New(svcCtx).AutoSchedule(r.Context(), req, operator(r, "")); err != nil {
httpx.Fail(w, 3011, err.Error())
return
}
httpx.OkMessage(w, "自动排产已生成(待人工启用)", nil)
}
}
// ---------- 日排产 ----------
func SaveDailyPlanHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
@@ -251,3 +268,17 @@ func SuggestedDockCodesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
httpx.Ok(w, map[string]any{"dockCodes": docks})
}
}
// InProcessHandler GET /workpieces/in-process 跨天在制品视图(停在各工位未完工工件)
// stationNo 可选:0=上线位, 13=下线位, 其余为物理工位;不传或<=0 返回全部。
func InProcessHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
stationNo, _ := strconv.Atoi(r.URL.Query().Get("stationNo"))
data, err := logic.New(svcCtx).WorkpiecesInProcess(r.Context(), stationNo)
if err != nil {
httpx.Fail(w, 3001, err.Error())
return
}
httpx.Ok(w, map[string]any{"list": data})
}
}