chore: 批量完成项目多模块迭代优化
1. 废弃半成品库存表并统一库存主表 2. 修复列表排序与搜索大小写不敏感问题 3. 新增用户最近登录时间、工单BOM名称字段 4. 完善角色管理、工位选择器功能 5. 增加拧紧数据补录、成品自动回流WMS能力 6. 统一导出时间范围校验规则 7. 丰富物料/备料单筛选条件 8. 调整菜单与权限命名适配产品型号业务
This commit is contained in:
@@ -36,17 +36,27 @@ func CreateInspectionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ListInspectionsHandler GET /inspections?category=&operator=&from=&to=
|
||||
// ListInspectionsHandler GET /inspections?category=&operator=&from=&to=&page=&pageSize=
|
||||
// 返回 {list, total},后端真分页(created_at desc / id desc,最新置顶)
|
||||
func ListInspectionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data, err := logic.New(svcCtx).ListInspections(r.Context(),
|
||||
q.Get("category"), q.Get("operator"), q.Get("from"), q.Get("to"))
|
||||
page := atoiDefault(q.Get("page"), 1)
|
||||
pageSize := atoiDefault(q.Get("pageSize"), 50)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 1000 {
|
||||
pageSize = 50
|
||||
}
|
||||
data, total, err := logic.New(svcCtx).ListInspectionsPaged(r.Context(),
|
||||
q.Get("category"), q.Get("operator"), q.Get("from"), q.Get("to"),
|
||||
q.Get("orderNo"), q.Get("stationNo"), page, pageSize)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2302, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
httpx.Ok(w, map[string]any{"list": data, "total": total})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ var pathPermMap = map[string]string{
|
||||
"DELETE:/api/v1/work-orders/*": "produce.workorder:delete",
|
||||
"POST:/api/v1/daily-plans": "produce.workorder:dailyplan",
|
||||
"PUT:/api/v1/bom": "produce.bom:edit",
|
||||
"POST:/api/v1/bom/item/delete": "produce.bom:edit",
|
||||
"POST:/api/v1/material-requests/generate": "produce.material:generate",
|
||||
"POST:/api/v1/plc/send-process": "produce.plc:send",
|
||||
"POST:/api/v1/process-flows": "produce.processflow:add",
|
||||
|
||||
@@ -6,13 +6,17 @@ import (
|
||||
"bj_power_mes/common/httpx"
|
||||
"bj_power_mes/internal/logic"
|
||||
"bj_power_mes/internal/svc"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/pathvar"
|
||||
)
|
||||
|
||||
// ListProcessFlowsHandler GET /process-flows?stationNo=
|
||||
func ListProcessFlowsHandler(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).ListFlows(r.Context(), stationNo)
|
||||
page := atoiDefault(r.URL.Query().Get("page"), 0)
|
||||
pageSize := atoiDefault(r.URL.Query().Get("pageSize"), 20)
|
||||
data, err := logic.New(svcCtx).ListFlows(r.Context(), stationNo, page, pageSize)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 2201, err.Error())
|
||||
return
|
||||
@@ -71,6 +75,15 @@ func FileDownloadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// FilePathHandler GET /files/:date/:name —— 路径式文件 URL(巡检拍照上传返回的
|
||||
// url 形如 /api/v1/files/2026-09-07/xxx.png,此前无此路由导致照片 404)
|
||||
func FilePathHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := pathvar.Vars(r)
|
||||
serveUploadFile(w, r, svcCtx.Config.Upload.Dir, vars["date"]+"/"+vars["name"])
|
||||
}
|
||||
}
|
||||
|
||||
// ListStationsHandler GET /stations
|
||||
func ListStationsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -14,6 +14,7 @@ func SaveBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
ProductCode string `json:"productCode"`
|
||||
BomName string `json:"bomName"` // 同型号多份 BOM 并存时的名称(空=「默认」)
|
||||
Items []logic.BomItemReq `json:"items"`
|
||||
}
|
||||
if err := httpx.ParseJSON(r, &req); err != nil {
|
||||
@@ -24,7 +25,7 @@ func SaveBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
httpx.BadRequest(w, "请选择产品编码")
|
||||
return
|
||||
}
|
||||
if err := logic.New(svcCtx).SaveBom(r.Context(), req.ProductCode, req.Items, operator(r, "")); err != nil {
|
||||
if err := logic.New(svcCtx).SaveBom(r.Context(), req.ProductCode, req.BomName, req.Items, operator(r, "")); err != nil {
|
||||
httpx.Fail(w, 3101, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -34,7 +35,7 @@ func SaveBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
func ListBomHandler(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"))
|
||||
data, err := logic.New(svcCtx).ListBom(r.Context(), r.URL.Query().Get("productCode"), r.URL.Query().Get("bomName"))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3102, err.Error())
|
||||
return
|
||||
@@ -43,6 +44,36 @@ func ListBomHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ListBomNamesHandler GET /bom/names?productCode= — 型号下并存的 BOM 名称列表(工单建单选 BOM 用)
|
||||
func ListBomNamesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).ListBomNames(r.Context(), r.URL.Query().Get("productCode"))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3107, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteBomItemHandler POST /bom/item/delete {id} — 移除 BOM 中一条物料(按行主键)
|
||||
func DeleteBomItemHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Id int `json:"id"`
|
||||
}
|
||||
if err := httpx.ParseJSON(r, &req); err != nil || req.Id <= 0 {
|
||||
httpx.BadRequest(w, "缺少 id")
|
||||
return
|
||||
}
|
||||
if err := logic.New(svcCtx).DeleteBomItem(r.Context(), req.Id, operator(r, "")); err != nil {
|
||||
httpx.Fail(w, 3108, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.OkMessage(w, "已移除", nil)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 备料单 ----------
|
||||
|
||||
// GenerateMaterialHandler 按日排产生成备料单(生成前校验 BOM 物料在 WMS 档案存在)
|
||||
@@ -72,7 +103,9 @@ func GenerateMaterialHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
func ListMaterialRequestsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data, err := logic.New(svcCtx).ListMaterialRequests(r.Context(), q.Get("orderNo"), q.Get("planDate"), q.Get("status"))
|
||||
data, err := logic.New(svcCtx).ListMaterialRequestsFiltered(r.Context(),
|
||||
q.Get("orderNo"), q.Get("planDate"), q.Get("status"),
|
||||
q.Get("materialCode"), q.Get("materialName"), q.Get("targetDock"))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3104, err.Error())
|
||||
return
|
||||
|
||||
@@ -95,12 +95,13 @@ func ScanReportHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
func ScanRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
data, err := logic.New(svcCtx).ListScanRecords(r.Context(), q.Get("sn"), q.Get("orderNo"))
|
||||
data, total, err := logic.New(svcCtx).ListScanRecords(r.Context(), q.Get("sn"), q.Get("orderNo"),
|
||||
atoiDefault(q.Get("page"), 1), atoiDefault(q.Get("pageSize"), 20))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3206, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, data)
|
||||
httpx.Ok(w, map[string]any{"list": data, "total": total})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,3 +122,30 @@ func ProcessStepsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
httpx.Ok(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package production
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -18,11 +19,11 @@ func pathID(r *http.Request) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// ---------- 产品类型 ----------
|
||||
// ---------- 产品型号 ----------
|
||||
|
||||
func ListProductTypesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := logic.New(svcCtx).ListProductTypes(r.Context())
|
||||
data, _, err := logic.New(svcCtx).ListProductTypes(r.Context())
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3001, err.Error())
|
||||
return
|
||||
@@ -31,6 +32,25 @@ func ListProductTypesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ListProductTypesPageHandler GET /product-types/page 产品型号管理页真分页+搜索
|
||||
// 响应 {total,list,page,pageSize,wmsOnline}
|
||||
func ListProductTypesPageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page := atoiDefault(q.Get("page"), 1)
|
||||
pageSize := atoiDefault(q.Get("pageSize"), 20)
|
||||
total, list, wmsOnline, err := logic.New(svcCtx).ListProductTypesPage(r.Context(),
|
||||
q.Get("keyword"), q.Get("isActive"), page, pageSize)
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3001, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.Ok(w, map[string]any{
|
||||
"total": total, "list": list, "page": page, "pageSize": pageSize, "wmsOnline": wmsOnline,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func CreateProductTypeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req logic.ProductTypeReq
|
||||
@@ -71,6 +91,22 @@ func DeleteProductTypeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// ExportProductTypesHandler GET /product-types/export 转发 WMS xlsx 导出
|
||||
func ExportProductTypesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
b, err := svcCtx.Wms.ExportProductTypes(r.Context(),
|
||||
q.Get("keyword"), q.Get("isActive"), q.Get("startDate"), q.Get("endDate"))
|
||||
if err != nil {
|
||||
httpx.Fail(w, 3002, "导出失败(WMS 不可达):"+err.Error())
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("产品型号.xlsx"))
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 工单 ----------
|
||||
|
||||
func ListWorkOrdersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
|
||||
@@ -84,6 +84,8 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
// ---------- JWT 业务 API ----------
|
||||
jwtRoutes := []rest.Route{
|
||||
{Method: http.MethodGet, Path: "/product-types", Handler: production.ListProductTypesHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/product-types/page", Handler: production.ListProductTypesPageHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/product-types/export", Handler: production.ExportProductTypesHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/product-types", Handler: production.CreateProductTypeHandler(serverCtx)},
|
||||
{Method: http.MethodPut, Path: "/product-types", Handler: production.UpdateProductTypeHandler(serverCtx)},
|
||||
{Method: http.MethodDelete, Path: "/product-types/:id", Handler: production.DeleteProductTypeHandler(serverCtx)},
|
||||
@@ -102,6 +104,8 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
|
||||
{Method: http.MethodPut, Path: "/bom", Handler: production.SaveBomHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/bom", Handler: production.ListBomHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/bom/names", Handler: production.ListBomNamesHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/bom/item/delete", Handler: production.DeleteBomItemHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodPost, Path: "/material-requests/generate", Handler: production.GenerateMaterialHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/material-requests", Handler: production.ListMaterialRequestsHandler(serverCtx)},
|
||||
@@ -111,6 +115,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
|
||||
{Method: http.MethodPost, Path: "/torque/report", Handler: production.TorqueReportHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/torque/records", Handler: production.TorqueRecordsHandler(serverCtx)},
|
||||
{Method: http.MethodPost, Path: "/torque/manual-add", Handler: production.TorqueManualAddHandler(serverCtx)},
|
||||
|
||||
{Method: http.MethodPost, Path: "/scan/report", Handler: production.ScanReportHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/scan/records", Handler: production.ScanRecordsHandler(serverCtx)},
|
||||
@@ -138,7 +143,6 @@ 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: "/files", Handler: FileDownloadHandler(serverCtx)},
|
||||
|
||||
// ---------- PAD 巡检终端(块8) ----------
|
||||
{Method: http.MethodPost, Path: "/inspections", Handler: CreateInspectionHandler(serverCtx)},
|
||||
@@ -147,6 +151,7 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
|
||||
// ---------- 工作量/绩效报表(块6) ----------
|
||||
{Method: http.MethodGet, Path: "/performance", Handler: WorkloadInternalHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/performance/export", Handler: PerformanceExportHandler(serverCtx)},
|
||||
|
||||
// ---------- 流程卡打印(块7) ----------
|
||||
{Method: http.MethodGet, Path: "/process-card", Handler: ProcessCardHandler(serverCtx)},
|
||||
@@ -158,4 +163,17 @@ func RegisterProductionRoutes(server *rest.Server, serverCtx *svc.ServiceContext
|
||||
// 请求体上限跟随 Upload.MaxMB(默认20MB),否则 go-zero 默认 1MB 会 413(PDF/照片上传在此分组)
|
||||
rest.WithMaxBytes(serverCtx.Config.Upload.MaxMB<<20),
|
||||
)
|
||||
|
||||
// ---------- 公开文件访问(免 JWT) ----------
|
||||
// <img>/el-image 发起的图片请求无法携带 Authorization 头,走 JWT 组会 401
|
||||
// 导致"上传成功但页面图片加载失败"(2026-09-08 修复)。文件名为系统生成的
|
||||
// 日期目录+随机名,不可枚举,免鉴权风险可控。
|
||||
fileRoutes := []rest.Route{
|
||||
{Method: http.MethodGet, Path: "/files", Handler: FileDownloadHandler(serverCtx)},
|
||||
{Method: http.MethodGet, Path: "/files/:date/:name", Handler: FilePathHandler(serverCtx)},
|
||||
}
|
||||
server.AddRoutes(
|
||||
fileRoutes,
|
||||
rest.WithPrefix("/api/v1"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,16 @@ 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 解析整型字符串,非法返回默认值
|
||||
@@ -110,7 +115,10 @@ func StationReportInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
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"))
|
||||
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
|
||||
@@ -165,3 +173,91 @@ func StationCheckinInternalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user