fix: rebuild MES as compilable go-zero+ent backend (renamed bj_power_mes), restore 3 workstation projects from pristine original, align naming; all Go projects go build clean

This commit is contained in:
SunYF
2026-08-27 10:56:41 +08:00
parent 380715f8a9
commit 371b494c54
523 changed files with 67923 additions and 24758 deletions
+195
View File
@@ -0,0 +1,195 @@
package upload
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"path/filepath"
"time"
"bj_power_mes/constants"
"bj_power_mes/ent/equipmentslot"
"bj_power_mes/ent/inspectionimage"
"bj_power_mes/ent/job"
"bj_power_mes/internal/svc"
"github.com/dromara/carbon/v2"
"github.com/google/uuid"
)
// HandleInspectionUpload 内窥镜检测图片上传,自动绑定当前检测工件
func HandleInspectionUpload(svcCtx *svc.ServiceContext, w http.ResponseWriter, r *http.Request) {
if r.ContentLength == 0 {
slog.Info("内窥镜URL测试")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"code": 200,
"msg": "success",
})
return
}
deviceId := r.FormValue("deviceId")
file, handler, err := r.FormFile("file")
if err != nil {
slog.Error("读取文件失败", "error", err)
http.Error(w, fmt.Sprintf("读取文件失败:%s", err.Error()), http.StatusBadRequest)
return
}
defer file.Close()
// 先查当前检测工件,避免无工件时产生孤儿文件
var jobId int
var workpieceNo string
var workOrderId int
var workOrderNo string
var palletCode string
slot, err := svcCtx.EntClient.EquipmentSlot.Query().
Where(
equipmentslot.EquipmentIdEQ(6),
equipmentslot.StatusEQ(constants.SlotStatus_Occupied),
).
Only(r.Context())
if err == nil && slot.CurrentJobId != nil && *slot.CurrentJobId > 0 {
j, err := svcCtx.EntClient.Job.Query().
Where(job.IDEQ(*slot.CurrentJobId)).
WithWorkOrder().
WithPallet().
Only(r.Context())
if err == nil {
jobId = j.ID
workpieceNo = j.WorkpieceNo
if j.Edges.WorkOrder != nil {
workOrderId = j.Edges.WorkOrder.ID
workOrderNo = j.Edges.WorkOrder.WorkOrderNo
}
if j.Edges.Pallet != nil {
palletCode = j.Edges.Pallet.PalletCode
}
}
}
if jobId <= 0 {
slog.Error("当前没有检测工件,无法绑定检测图片")
http.Error(w, "当前没有检测工件,无法绑定检测图片", http.StatusBadRequest)
return
}
now := carbon.Now()
dir := filepath.Join(svcCtx.Config.Upload.Dir, "inspection",
fmt.Sprintf("%04d", now.Year()),
fmt.Sprintf("%02d", now.Month()),
fmt.Sprintf("%02d", now.Day()))
if err := os.MkdirAll(dir, 0755); err != nil {
slog.Error("创建上传目录失败", "dir", dir, "error", err)
http.Error(w, "创建目录失败", http.StatusInternalServerError)
return
}
ext := filepath.Ext(handler.Filename)
if ext == "" {
ext = ".jpg"
}
fileName := uuid.New().String() + ext
filePath := filepath.Join(dir, fileName)
relPath := filepath.Join("inspection",
fmt.Sprintf("%04d", now.Year()),
fmt.Sprintf("%02d", now.Month()),
fmt.Sprintf("%02d", now.Day()),
fileName)
dst, err := os.Create(filePath)
if err != nil {
slog.Error("创建文件失败", "path", filePath, "error", err)
http.Error(w, "保存文件失败", http.StatusInternalServerError)
return
}
defer dst.Close()
written, err := io.Copy(dst, file)
if err != nil {
slog.Error("写入文件失败", "path", filePath, "error", err)
http.Error(w, "保存文件失败", http.StatusInternalServerError)
return
}
_, err = svcCtx.EntClient.InspectionImage.Create().
SetJobId(jobId).
SetWorkpieceNo(workpieceNo).
SetWorkOrderId(workOrderId).
SetWorkOrderNo(workOrderNo).
SetPalletCode(palletCode).
SetDeviceId(deviceId).
SetFileName(handler.Filename).
SetFilePath(filepath.ToSlash(relPath)).
SetFileSize(written).
Save(r.Context())
if err != nil {
slog.Error("创建检测图片记录失败", "jobId", jobId, "error", err)
w.Header().Set("Content-Type", "application/json")
http.Error(w, "创建检测图片记录失败", http.StatusBadRequest)
return
}
slog.Info("检测图片上传", "file", handler.Filename, "size", written, "jobId", jobId, "deviceId", deviceId)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
"code": 200,
"data": map[string]any{ // 必须返回data字段(内容无所谓),否则内窥镜会卡死
"createTime": now.Format("2006-01-02 15:04:05"),
"deviceId": deviceId,
"name": fileName,
"dirStatus": 0,
"dmCode": "",
"docPath": filePath,
//"id": 619,
"level": 2,
"parentDir": dir,
"remark": "",
"size": written,
"type": ext,
},
"msg": "success",
})
}
// StartCleanup 定期清理过期检测图片(每天一次),RetentionDays=-1 跳过
func StartCleanup(svcCtx *svc.ServiceContext) {
if svcCtx.Config.Upload.RetentionDays == -1 {
return
}
retention := svcCtx.Config.Upload.RetentionDays
if retention <= 0 {
retention = 30
}
go func() {
ticker := time.NewTicker(24 * time.Hour)
defer ticker.Stop()
for range ticker.C {
cutoff := time.Now().AddDate(0, 0, -retention)
images, err := svcCtx.EntClient.InspectionImage.Query().
Where(inspectionimage.CreatedAtLT(cutoff)).
All(context.Background())
if err != nil {
slog.Error("inspection cleanup: query failed", "error", err)
continue
}
for _, img := range images {
absPath := filepath.Join(svcCtx.Config.Upload.Dir, filepath.FromSlash(img.FilePath))
os.Remove(absPath)
_ = svcCtx.EntClient.InspectionImage.DeleteOneID(img.ID).Exec(context.Background())
}
if len(images) > 0 {
slog.Info("inspection cleanup: removed expired images", "count", len(images), "before", cutoff)
}
}
}()
}
@@ -0,0 +1,132 @@
package upload
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"testing"
"bj_power_mes/ent"
"bj_power_mes/internal/config"
"bj_power_mes/internal/svc"
entsql "entgo.io/ent/dialect/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"resty.dev/v3"
)
func TestHandleInspectionUpload(t *testing.T) {
// 创建临时上传目录
//tmpDir := t.TempDir()
svcCtx := newTestServiceContext(t, "F:\\Workspace\\Hardman\\back_cover\\uploads")
t.Run("上传成功", func(t *testing.T) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
_ = writer.WriteField("deviceId", "test")
part, _ := writer.CreateFormFile("file", "test.jpg")
data, _ := os.ReadFile("F:\\Workspace\\Hardman\\back_cover\\internal\\upload\\rog.png")
part.Write(data)
writer.Close()
req := httptest.NewRequest(http.MethodPost, "/document/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
HandleInspectionUpload(svcCtx, w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]any
json.NewDecoder(w.Body).Decode(&resp)
if resp["code"] != 200 {
t.Errorf("expected code 200, got %v", resp["code"])
}
})
t.Run("缺少文件返回400", func(t *testing.T) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
_ = writer.WriteField("deviceId", "6")
writer.Close()
req := httptest.NewRequest(http.MethodPost, "/document/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
HandleInspectionUpload(svcCtx, w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
})
t.Run("超大文件被拒绝", func(t *testing.T) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("file", "big.jpg")
// 写入超过 5MB 的数据
chunk := make([]byte, 1024)
for i := 0; i < 6*1024; i++ {
part.Write(chunk)
}
writer.Close()
req := httptest.NewRequest(http.MethodPost, "/document/upload", body)
req.Header.Set("Content-Type", writer.FormDataContentType())
w := httptest.NewRecorder()
HandleInspectionUpload(svcCtx, w, req)
if w.Code == http.StatusOK {
t.Error("超大文件应被拒绝")
}
})
t.Run("内窥镜URL测试", func(t *testing.T) {
client := resty.New()
res, err := client.R().
SetMultipartFormData(map[string]string{
"userId": "0",
"deviceId": "test",
}).
SetFile("file", "F:\\Workspace\\Hardman\\back_cover\\internal\\upload\\rog.png").
//Post("http://47.115.54.30:8097/document/upload")
Post("http://localhost:8888/document/upload")
fmt.Println(err, res)
})
}
// newTestServiceContext 创建测试用的 ServiceContext,指向临时目录
func newTestServiceContext(t *testing.T, uploadDir string) *svc.ServiceContext {
t.Helper()
db, err := sql.Open("pgx", "postgresql://postgres:postgres@127.0.0.1:5432/back_cover?sslmode=disable")
if err != nil {
t.Skipf("跳过:无法连接数据库 (%v)", err)
}
drv := entsql.OpenDB("postgres", db)
entClient := ent.NewClient(ent.Driver(drv))
return &svc.ServiceContext{
Config: config.Config{
Upload: struct {
Dir string `json:",default=./uploads"`
RetentionDays int `json:",default=30"`
}{
Dir: uploadDir,
RetentionDays: -1,
},
},
EntClient: entClient,
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB