diff --git a/docs/api.md b/docs/api.md index cb051182..6f1b58a7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -419,8 +419,9 @@ ```json { - "handler": "张三", - "supplier": "ABC 钢材", + "handler": "张三", + "supplier": "ABC 钢材", + "remark": "到货验收通过", "items": [ { "material_id": 1, "quantity": 500, "warning_stock": 100, "warning_enabled": true }, { "material_id": 2, "quantity": 200, "warning_stock": 0, "warning_enabled": false }, @@ -433,6 +434,7 @@ |----------|--------|------|--------------------| | handler | string | 是 | 经手人 | | supplier | string | 是 | 供应商 | +| remark | string | 否 | 备注 | | items | array | 是 | 明细,至少 1 行 | **明细字段:** @@ -448,7 +450,7 @@ > > 关闭预警而想保留原阈值时,把该物料当前的阈值连同 `warning_enabled: false` 一起传回即可(前端表单正是这么做的)。 > -> 本接口**不支持备注字段**。前端即便提交了 `remark`,服务端也会忽略。 +`inbound_no` 由服务端按 `IN-` 加 8 位订单 ID 自动生成;`operator` 为创建该单据的当前登录用户。二者均由服务端维护,不能由请求体指定。 **响应:** `201 Created`,`data` 为创建后的入库单,结构同 `GET /inbound/:id`。 @@ -569,8 +571,7 @@ { "id": 1, "outbound_date": "2026-01-01T08:00:00Z", - "handler": "李四", - "receiver": "装配车间", + "receiver": "装配车间", "status": "completed", "created_at": "2026-01-01T08:00:00Z", "updated_at": "2026-01-01T08:00:00Z" @@ -642,8 +643,9 @@ ```json { - "handler": "李四", - "receiver": "装配车间", + "handler": "李四", + "receiver": "装配车间", + "remark": "急用", "items": [ { "material_id": 1, "quantity": 100 }, { "material_id": 2, "quantity": 50 } @@ -653,8 +655,8 @@ | 字段 | 类型 | 必填 | 说明 | |----------|--------|------|------------------| -| handler | string | 是 | 经手人 | | receiver | string | 是 | 领用人 | +| remark | string | 否 | 备注 | | items | array | 是 | 明细,至少 1 行 | **明细字段:** @@ -664,6 +666,8 @@ | material_id | integer | 是 | 物料 ID | | quantity | integer | 是 | 出库数量,必须大于 0 | +`outbound_no` 由服务端按 `OUT-` 加 8 位订单 ID 自动生成;`operator` 与 `handler` 均由服务端设置为当前登录用户名,不能由请求体指定。 + **响应:** `201 Created`,`data` 为创建后的出库单,结构同 `GET /outbound/:id`。 **状态码:** diff --git a/frontend/src/views/Outbound.vue b/frontend/src/views/Outbound.vue index 5cb943a6..8dfdfdd1 100644 --- a/frontend/src/views/Outbound.vue +++ b/frontend/src/views/Outbound.vue @@ -39,9 +39,6 @@ - - - @@ -131,7 +128,6 @@ const isEdit = ref(false) const editId = ref(null) const form = reactive({ - handler: '', receiver: '', remark: '', items: [{ material_id: null, materialSearchText: '', quantity: 1 }] @@ -157,7 +153,7 @@ function handlePageChange(page) { function showAddModal() { isEdit.value = false editId.value = null - Object.assign(form, { handler: '', receiver: '', remark: '', items: [{ material_id: null, materialSearchText: '', quantity: 1 }] }) + Object.assign(form, { receiver: '', remark: '', items: [{ material_id: null, materialSearchText: '', quantity: 1 }] }) dialogVisible.value = true } @@ -167,7 +163,6 @@ async function showEditModal(row) { try { const res = await outboundStore.fetchOutboundDetail(row.id) const order = res.data || res - form.handler = order.handler || '' form.receiver = order.receiver || '' form.remark = order.remark || '' form.items = order.items && order.items.length > 0 @@ -245,7 +240,6 @@ async function handleSave() { return } const data = { - handler: form.handler, receiver: form.receiver, remark: form.remark, items: validItems diff --git a/internal/handler/api.go b/internal/handler/api.go index d93ae5d9..1c4e14bb 100644 --- a/internal/handler/api.go +++ b/internal/handler/api.go @@ -458,7 +458,11 @@ func (h *Handler) CreateInbound(c *gin.Context) { return } - order, err := h.inboundSvc.Create(&req) + operator := middleware.GetCurrentUsername(c) + if operator == "" { + operator = "系统" + } + order, err := h.inboundSvc.Create(&req, operator) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") @@ -589,7 +593,11 @@ func (h *Handler) CreateOutbound(c *gin.Context) { return } - order, err := h.outboundSvc.Create(&req) + operator := middleware.GetCurrentUsername(c) + if operator == "" { + operator = "系统" + } + order, err := h.outboundSvc.Create(&req, operator) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") @@ -635,7 +643,11 @@ func (h *Handler) UpdateOutbound(c *gin.Context) { return } - order, err := h.outboundSvc.Update(id, &req) + operator := middleware.GetCurrentUsername(c) + if operator == "" { + operator = "系统" + } + order, err := h.outboundSvc.Update(id, &req, operator) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") diff --git a/internal/model/model.go b/internal/model/model.go index c9317bba..b6eac79c 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "time" "github.com/shopspring/decimal" @@ -55,9 +56,12 @@ type ApplyItem struct { // InboundOrder represents an inbound order type InboundOrder struct { ID int64 `json:"id"` + InboundNo string `json:"inbound_no"` + Operator string `json:"operator"` InboundDate time.Time `json:"inbound_date"` Handler string `json:"handler"` Supplier string `json:"supplier"` + Remark string `json:"remark"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` Status string `json:"status,omitempty"` @@ -77,9 +81,12 @@ type InboundItem struct { // OutboundOrder represents an outbound order type OutboundOrder struct { ID int64 `json:"id"` + OutboundNo string `json:"outbound_no"` + Operator string `json:"operator"` OutboundDate time.Time `json:"outbound_date"` Handler string `json:"handler"` Receiver string `json:"receiver"` + Remark string `json:"remark"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` Status string `json:"status,omitempty"` @@ -96,6 +103,13 @@ type OutboundItem struct { MaterialName string `json:"material_name,omitempty"` } +// OrderNo returns the stable display number used by inbound and outbound +// documents. It derives from the database ID, so it remains unique and does +// not change when the document is edited. +func OrderNo(prefix string, id int64) string { + return fmt.Sprintf("%s-%08d", prefix, id) +} + // StockLog represents a stock change log type StockLog struct { ID int64 `json:"id"` @@ -216,6 +230,7 @@ type UpdateMaterialRequest struct { type CreateInboundRequest struct { Handler string `json:"handler" binding:"required"` Supplier string `json:"supplier" binding:"required"` + Remark string `json:"remark"` Items []struct { MaterialID int64 `json:"material_id" binding:"required"` Quantity int `json:"quantity" binding:"required,gt=0"` @@ -229,8 +244,11 @@ type CreateInboundRequest struct { // CreateOutboundRequest represents the request to create an outbound order type CreateOutboundRequest struct { - Handler string `json:"handler" binding:"required"` + // Handler is retained for API compatibility but is ignored. The server + // always records the authenticated user's username as the operator. + Handler string `json:"handler"` Receiver string `json:"receiver" binding:"required"` + Remark string `json:"remark"` Items []struct { MaterialID int64 `json:"material_id" binding:"required"` Quantity int `json:"quantity" binding:"required,gt=0"` @@ -241,6 +259,7 @@ type CreateOutboundRequest struct { type UpdateInboundRequest struct { Handler string `json:"handler"` Supplier string `json:"supplier"` + Remark string `json:"remark"` Items []struct { MaterialID int64 `json:"material_id" binding:"required"` Quantity int `json:"quantity" binding:"required,gt=0"` @@ -255,7 +274,9 @@ type UpdateInboundRequest struct { // UpdateOutboundRequest represents the request to update an outbound order type UpdateOutboundRequest struct { Handler string `json:"handler"` + Operator string `json:"-"` Receiver string `json:"receiver"` + Remark string `json:"remark"` Items []struct { MaterialID int64 `json:"material_id" binding:"required"` Quantity int `json:"quantity" binding:"required,gt=0"` diff --git a/internal/model/order_fields_test.go b/internal/model/order_fields_test.go new file mode 100644 index 00000000..eb967764 --- /dev/null +++ b/internal/model/order_fields_test.go @@ -0,0 +1,69 @@ +package model + +import ( + "encoding/json" + "testing" + + "github.com/go-playground/validator/v10" +) + +func TestOrderRequestsAndResponsesPreserveFrontendFields(t *testing.T) { + inboundRequest := CreateInboundRequest{} + if err := json.Unmarshal([]byte(`{"handler":"张三","supplier":"供应商","remark":"到货验收通过","items":[{"material_id":1,"quantity":2}]}`), &inboundRequest); err != nil { + t.Fatalf("unmarshal inbound request: %v", err) + } + if inboundRequest.Remark != "到货验收通过" { + t.Fatalf("inbound remark = %q, want %q", inboundRequest.Remark, "到货验收通过") + } + + outboundRequest := CreateOutboundRequest{} + if err := json.Unmarshal([]byte(`{"handler":"李四","receiver":"装配车间","remark":"急用","items":[{"material_id":1,"quantity":2}]}`), &outboundRequest); err != nil { + t.Fatalf("unmarshal outbound request: %v", err) + } + if outboundRequest.Remark != "急用" { + t.Fatalf("outbound remark = %q, want %q", outboundRequest.Remark, "急用") + } + + payload, err := json.Marshal(struct { + Inbound InboundOrder `json:"inbound"` + Outbound OutboundOrder `json:"outbound"` + }{ + Inbound: InboundOrder{InboundNo: "IN-20260920-0001", Operator: "管理员", Remark: "到货验收通过"}, + Outbound: OutboundOrder{OutboundNo: "OUT-20260920-0001", Operator: "管理员", Remark: "急用"}, + }) + if err != nil { + t.Fatalf("marshal orders: %v", err) + } + + for _, field := range []string{"inbound_no", "outbound_no", "operator", "remark"} { + if !containsJSONField(payload, field) { + t.Fatalf("order response omits %q: %s", field, payload) + } + } +} + +func TestOutboundRequestDoesNotRequireClientProvidedHandler(t *testing.T) { + var request CreateOutboundRequest + if err := json.Unmarshal([]byte(`{"receiver":"装配车间","items":[{"material_id":1,"quantity":2}]}`), &request); err != nil { + t.Fatalf("unmarshal outbound request: %v", err) + } + + validate := validator.New() + validate.SetTagName("binding") + if err := validate.Struct(request); err != nil { + t.Fatalf("outbound request without a client handler should be valid: %v", err) + } +} + +func containsJSONField(payload []byte, field string) bool { + var decoded map[string]map[string]any + if err := json.Unmarshal(payload, &decoded); err != nil { + return false + } + for _, order := range decoded { + if _, ok := order[field]; ok { + return true + } + } + return false +} diff --git a/internal/repository/application.go b/internal/repository/application.go index ba2933ec..a9298003 100644 --- a/internal/repository/application.go +++ b/internal/repository/application.go @@ -195,8 +195,8 @@ func (r *ApplicationRepository) GetByIDWithDetails(id int64) (*model.MaterialApp } func (r *ApplicationRepository) CreateOutboundOrder(order *model.OutboundOrder) error { - query := `INSERT INTO outbound_orders (outbound_date, handler, receiver, status) VALUES (NOW(), ?, ?, ?)` - result, err := database.DB.Exec(query, order.Handler, order.Receiver, order.Status) + query := `INSERT INTO outbound_orders (outbound_date, operator, handler, receiver, status) VALUES (NOW(), ?, ?, ?, ?)` + result, err := database.DB.Exec(query, order.Operator, order.Handler, order.Receiver, order.Status) if err != nil { return err } @@ -205,12 +205,16 @@ func (r *ApplicationRepository) CreateOutboundOrder(order *model.OutboundOrder) return err } order.ID = id + order.OutboundNo = model.OrderNo("OUT", id) + if _, err := database.DB.Exec(`UPDATE outbound_orders SET outbound_no = ? WHERE id = ?`, order.OutboundNo, id); err != nil { + return err + } return nil } func (r *ApplicationRepository) CreateOutboundOrderTx(tx *sql.Tx, order *model.OutboundOrder) error { - query := `INSERT INTO outbound_orders (outbound_date, handler, receiver, status) VALUES (NOW(), ?, ?, ?)` - result, err := tx.Exec(query, order.Handler, order.Receiver, order.Status) + query := `INSERT INTO outbound_orders (outbound_date, operator, handler, receiver, status) VALUES (NOW(), ?, ?, ?, ?)` + result, err := tx.Exec(query, order.Operator, order.Handler, order.Receiver, order.Status) if err != nil { return err } @@ -219,6 +223,10 @@ func (r *ApplicationRepository) CreateOutboundOrderTx(tx *sql.Tx, order *model.O return err } order.ID = id + order.OutboundNo = model.OrderNo("OUT", id) + if _, err := tx.Exec(`UPDATE outbound_orders SET outbound_no = ? WHERE id = ?`, order.OutboundNo, id); err != nil { + return err + } return nil } diff --git a/internal/repository/inbound.go b/internal/repository/inbound.go index 09c246de..e0ebdba4 100644 --- a/internal/repository/inbound.go +++ b/internal/repository/inbound.go @@ -32,8 +32,8 @@ func (r *InboundRepo) CreateTx(tx *sql.Tx, order *model.InboundOrder) error { defer tx.Rollback() } - query := `INSERT INTO inbound_orders (inbound_date, handler, supplier) VALUES (?, ?, ?)` - result, err := tx.Exec(query, order.InboundDate, order.Handler, order.Supplier) + query := `INSERT INTO inbound_orders (inbound_date, operator, handler, supplier, remark) VALUES (?, ?, ?, ?, ?)` + result, err := tx.Exec(query, order.InboundDate, order.Operator, order.Handler, order.Supplier, order.Remark) if err != nil { return err } @@ -42,6 +42,10 @@ func (r *InboundRepo) CreateTx(tx *sql.Tx, order *model.InboundOrder) error { return err } order.ID = orderID + order.InboundNo = model.OrderNo("IN", orderID) + if _, err = tx.Exec(`UPDATE inbound_orders SET inbound_no = ? WHERE id = ?`, order.InboundNo, orderID); err != nil { + return err + } for i := range order.Items { item := &order.Items[i] @@ -66,8 +70,8 @@ func (r *InboundRepo) CreateTx(tx *sql.Tx, order *model.InboundOrder) error { func (r *InboundRepo) GetByID(id int64) (*model.InboundOrder, error) { order := &model.InboundOrder{} - query := `SELECT id, inbound_date, handler, supplier, created_at, updated_at FROM inbound_orders WHERE id = ?` - err := database.DB.QueryRow(query, id).Scan(&order.ID, &order.InboundDate, &order.Handler, &order.Supplier, &order.CreatedAt, &order.UpdatedAt) + query := `SELECT id, COALESCE(inbound_no, CONCAT('IN-', LPAD(id, 8, '0'))), COALESCE(operator, ''), inbound_date, handler, supplier, COALESCE(remark, ''), created_at, updated_at FROM inbound_orders WHERE id = ?` + err := database.DB.QueryRow(query, id).Scan(&order.ID, &order.InboundNo, &order.Operator, &order.InboundDate, &order.Handler, &order.Supplier, &order.Remark, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, err } @@ -111,7 +115,7 @@ func (r *InboundRepo) List(search string, limit, offset int) ([]model.InboundOrd return nil, 0, err } - query := `SELECT id, inbound_date, handler, supplier, created_at, updated_at FROM inbound_orders WHERE 1=1` + query := `SELECT id, COALESCE(inbound_no, CONCAT('IN-', LPAD(id, 8, '0'))), COALESCE(operator, ''), inbound_date, handler, supplier, COALESCE(remark, ''), created_at, updated_at FROM inbound_orders WHERE 1=1` var args []interface{} if search != "" { @@ -131,7 +135,7 @@ func (r *InboundRepo) List(search string, limit, offset int) ([]model.InboundOrd var orders []model.InboundOrder for rows.Next() { var order model.InboundOrder - err := rows.Scan(&order.ID, &order.InboundDate, &order.Handler, &order.Supplier, &order.CreatedAt, &order.UpdatedAt) + err := rows.Scan(&order.ID, &order.InboundNo, &order.Operator, &order.InboundDate, &order.Handler, &order.Supplier, &order.Remark, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, 0, err } @@ -214,8 +218,8 @@ func (r *InboundRepo) UpdateTx(tx *sql.Tx, id int64, req *model.UpdateInboundReq } // Update order basic fields - query := `UPDATE inbound_orders SET handler = ?, supplier = ? WHERE id = ?` - _, err := tx.Exec(query, req.Handler, req.Supplier, id) + query := `UPDATE inbound_orders SET handler = ?, supplier = ?, remark = ? WHERE id = ?` + _, err := tx.Exec(query, req.Handler, req.Supplier, req.Remark, id) if err != nil { return err } diff --git a/internal/repository/order_null_fields_test.go b/internal/repository/order_null_fields_test.go new file mode 100644 index 00000000..446f8b17 --- /dev/null +++ b/internal/repository/order_null_fields_test.go @@ -0,0 +1,74 @@ +package repository + +import ( + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/wmms/wmms/pkg/database" +) + +func TestInboundListConvertsLegacyNullDisplayFields(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("open sql mock: %v", err) + } + t.Cleanup(func() { + database.DB = nil + db.Close() + }) + database.DB = db + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM inbound_orders WHERE 1=1`)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + query := `SELECT id, COALESCE(inbound_no, CONCAT('IN-', LPAD(id, 8, '0'))), COALESCE(operator, ''), inbound_date, handler, supplier, COALESCE(remark, ''), created_at, updated_at FROM inbound_orders WHERE 1=1 ORDER BY id DESC LIMIT ? OFFSET ?` + now := time.Date(2026, 9, 20, 9, 0, 0, 0, time.UTC) + mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(20, 0).WillReturnRows( + sqlmock.NewRows([]string{"id", "inbound_no", "operator", "inbound_date", "handler", "supplier", "remark", "created_at", "updated_at"}). + AddRow(42, "IN-00000042", "", now, "张三", "供应商", "", now, now), + ) + + orders, _, err := NewInboundRepo().List("", 20, 0) + if err != nil { + t.Fatalf("list inbound orders with legacy NULL fields: %v", err) + } + if got := orders[0]; got.InboundNo != "IN-00000042" || got.Operator != "" || got.Remark != "" { + t.Fatalf("unexpected legacy inbound order: %#v", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} + +func TestOutboundListConvertsLegacyNullDisplayFields(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("open sql mock: %v", err) + } + t.Cleanup(func() { + database.DB = nil + db.Close() + }) + database.DB = db + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM outbound_orders WHERE 1=1`)). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + query := `SELECT id, COALESCE(outbound_no, CONCAT('OUT-', LPAD(id, 8, '0'))), COALESCE(operator, ''), outbound_date, handler, receiver, COALESCE(remark, ''), created_at, updated_at FROM outbound_orders WHERE 1=1 ORDER BY id DESC LIMIT ? OFFSET ?` + now := time.Date(2026, 9, 20, 9, 0, 0, 0, time.UTC) + mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(20, 0).WillReturnRows( + sqlmock.NewRows([]string{"id", "outbound_no", "operator", "outbound_date", "handler", "receiver", "remark", "created_at", "updated_at"}). + AddRow(27, "OUT-00000027", "", now, "李四", "装配车间", "", now, now), + ) + + orders, _, err := NewOutboundRepo().List("", 20, 0) + if err != nil { + t.Fatalf("list outbound orders with legacy NULL fields: %v", err) + } + if got := orders[0]; got.OutboundNo != "OUT-00000027" || got.Operator != "" || got.Remark != "" { + t.Fatalf("unexpected legacy outbound order: %#v", got) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/repository/outbound.go b/internal/repository/outbound.go index e6a8c631..ff5345cd 100644 --- a/internal/repository/outbound.go +++ b/internal/repository/outbound.go @@ -32,8 +32,8 @@ func (r *OutboundRepo) CreateTx(tx *sql.Tx, order *model.OutboundOrder) error { defer tx.Rollback() } - query := `INSERT INTO outbound_orders (outbound_date, handler, receiver) VALUES (?, ?, ?)` - result, err := tx.Exec(query, order.OutboundDate, order.Handler, order.Receiver) + query := `INSERT INTO outbound_orders (outbound_date, operator, handler, receiver, remark) VALUES (?, ?, ?, ?, ?)` + result, err := tx.Exec(query, order.OutboundDate, order.Operator, order.Handler, order.Receiver, order.Remark) if err != nil { return err } @@ -42,6 +42,10 @@ func (r *OutboundRepo) CreateTx(tx *sql.Tx, order *model.OutboundOrder) error { return err } order.ID = orderID + order.OutboundNo = model.OrderNo("OUT", orderID) + if _, err = tx.Exec(`UPDATE outbound_orders SET outbound_no = ? WHERE id = ?`, order.OutboundNo, orderID); err != nil { + return err + } for i := range order.Items { item := &order.Items[i] @@ -66,8 +70,8 @@ func (r *OutboundRepo) CreateTx(tx *sql.Tx, order *model.OutboundOrder) error { func (r *OutboundRepo) GetByID(id int64) (*model.OutboundOrder, error) { order := &model.OutboundOrder{} - query := `SELECT id, outbound_date, handler, receiver, created_at, updated_at FROM outbound_orders WHERE id = ?` - err := database.DB.QueryRow(query, id).Scan(&order.ID, &order.OutboundDate, &order.Handler, &order.Receiver, &order.CreatedAt, &order.UpdatedAt) + query := `SELECT id, COALESCE(outbound_no, CONCAT('OUT-', LPAD(id, 8, '0'))), COALESCE(operator, ''), outbound_date, handler, receiver, COALESCE(remark, ''), created_at, updated_at FROM outbound_orders WHERE id = ?` + err := database.DB.QueryRow(query, id).Scan(&order.ID, &order.OutboundNo, &order.Operator, &order.OutboundDate, &order.Handler, &order.Receiver, &order.Remark, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, err } @@ -111,7 +115,7 @@ func (r *OutboundRepo) List(search string, limit, offset int) ([]model.OutboundO return nil, 0, err } - query := `SELECT id, outbound_date, handler, receiver, created_at, updated_at FROM outbound_orders WHERE 1=1` + query := `SELECT id, COALESCE(outbound_no, CONCAT('OUT-', LPAD(id, 8, '0'))), COALESCE(operator, ''), outbound_date, handler, receiver, COALESCE(remark, ''), created_at, updated_at FROM outbound_orders WHERE 1=1` var args []interface{} if search != "" { @@ -131,7 +135,7 @@ func (r *OutboundRepo) List(search string, limit, offset int) ([]model.OutboundO var orders []model.OutboundOrder for rows.Next() { var order model.OutboundOrder - err := rows.Scan(&order.ID, &order.OutboundDate, &order.Handler, &order.Receiver, &order.CreatedAt, &order.UpdatedAt) + err := rows.Scan(&order.ID, &order.OutboundNo, &order.Operator, &order.OutboundDate, &order.Handler, &order.Receiver, &order.Remark, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, 0, err } @@ -214,8 +218,8 @@ func (r *OutboundRepo) UpdateTx(tx *sql.Tx, id int64, req *model.UpdateOutboundR } // Update order basic fields - query := `UPDATE outbound_orders SET handler = ?, receiver = ? WHERE id = ?` - _, err := tx.Exec(query, req.Handler, req.Receiver, id) + query := `UPDATE outbound_orders SET operator = ?, handler = ?, receiver = ?, remark = ? WHERE id = ?` + _, err := tx.Exec(query, req.Operator, req.Handler, req.Receiver, req.Remark, id) if err != nil { return err } diff --git a/internal/service/application.go b/internal/service/application.go index 63ab67e2..fa838966 100644 --- a/internal/service/application.go +++ b/internal/service/application.go @@ -127,6 +127,7 @@ func (s *ApplicationService) Approve(id int64, approverID int64, autoOutbound bo // Create outbound order within transaction order := &model.OutboundOrder{ Handler: approverName, + Operator: approverName, Receiver: app.Username, Status: "completed", } diff --git a/internal/service/inbound.go b/internal/service/inbound.go index 2c20d898..42c6e63d 100644 --- a/internal/service/inbound.go +++ b/internal/service/inbound.go @@ -31,11 +31,13 @@ func resolveWarningEnabled(enabled *bool) bool { return *enabled } -func (s *InboundService) Create(req *model.CreateInboundRequest) (*model.InboundOrder, error) { +func (s *InboundService) Create(req *model.CreateInboundRequest, operator string) (*model.InboundOrder, error) { order := &model.InboundOrder{ InboundDate: time.Now(), + Operator: operator, Handler: req.Handler, Supplier: req.Supplier, + Remark: req.Remark, Items: make([]model.InboundItem, len(req.Items)), } diff --git a/internal/service/outbound.go b/internal/service/outbound.go index 44c5d642..01172578 100644 --- a/internal/service/outbound.go +++ b/internal/service/outbound.go @@ -25,11 +25,13 @@ func NewOutboundService() *OutboundService { } } -func (s *OutboundService) Create(req *model.CreateOutboundRequest) (*model.OutboundOrder, error) { +func (s *OutboundService) Create(req *model.CreateOutboundRequest, operator string) (*model.OutboundOrder, error) { order := &model.OutboundOrder{ OutboundDate: time.Now(), - Handler: req.Handler, + Operator: operator, + Handler: operator, Receiver: req.Receiver, + Remark: req.Remark, Items: make([]model.OutboundItem, len(req.Items)), } @@ -184,7 +186,7 @@ func (s *OutboundService) Delete(id int64) error { return nil } -func (s *OutboundService) Update(id int64, req *model.UpdateOutboundRequest) (*model.OutboundOrder, error) { +func (s *OutboundService) Update(id int64, req *model.UpdateOutboundRequest, operator string) (*model.OutboundOrder, error) { // Get old order data oldOrder, err := s.outboundRepo.GetByID(id) if err != nil { @@ -265,6 +267,9 @@ func (s *OutboundService) Update(id int64, req *model.UpdateOutboundRequest) (*m } // Update order in repository + // 客户端不能指定操作人;编辑也由当前登录用户接管操作人和兼容字段 handler。 + req.Handler = operator + req.Operator = operator err = s.outboundRepo.UpdateTx(tx, id, req) if err != nil { return nil, err diff --git a/scripts/add_order_display_fields.sql b/scripts/add_order_display_fields.sql new file mode 100644 index 00000000..bd6afa80 --- /dev/null +++ b/scripts/add_order_display_fields.sql @@ -0,0 +1,21 @@ +-- Apply once to databases created before inbound/outbound display fields existed. +-- Existing documents receive a stable number derived from their primary key; +-- their operator and remark remain NULL because that historical data was never recorded. + +ALTER TABLE inbound_orders + ADD COLUMN inbound_no VARCHAR(32) NULL UNIQUE AFTER location_id, + ADD COLUMN operator VARCHAR(100) NULL AFTER inbound_no, + ADD COLUMN remark TEXT NULL AFTER supplier; + +UPDATE inbound_orders +SET inbound_no = CONCAT('IN-', LPAD(id, 8, '0')) +WHERE inbound_no IS NULL; + +ALTER TABLE outbound_orders + ADD COLUMN outbound_no VARCHAR(32) NULL UNIQUE AFTER location_id, + ADD COLUMN operator VARCHAR(100) NULL AFTER outbound_no, + ADD COLUMN remark TEXT NULL AFTER receiver; + +UPDATE outbound_orders +SET outbound_no = CONCAT('OUT-', LPAD(id, 8, '0')) +WHERE outbound_no IS NULL; diff --git a/scripts/init.sql b/scripts/init.sql index 2e4b5920..a7b87f40 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -28,9 +28,9 @@ CREATE TABLE warehouse_locations (id BIGINT AUTO_INCREMENT PRIMARY KEY, warehous CREATE TABLE user_warehouses (user_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(user_id,warehouse_id), FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(warehouse_id) REFERENCES warehouses(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE inventory_balances (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, location_id BIGINT NOT NULL, material_id BIGINT NOT NULL, on_hand DECIMAL(20,6) NOT NULL DEFAULT 0, reserved DECIMAL(20,6) NOT NULL DEFAULT 0, locked DECIMAL(20,6) NOT NULL DEFAULT 0, in_transit DECIMAL(20,6) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 1, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_inventory_balance_dimension(organization_id,warehouse_id,location_id,material_id), KEY idx_inventory_balances_material(organization_id,material_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id), FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE stock_warning_rules (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, material_id BIGINT NOT NULL, minimum_available DECIMAL(20,6) NOT NULL DEFAULT 0, enabled BOOLEAN NOT NULL DEFAULT TRUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_stock_warning_rules_dimension(organization_id,warehouse_id,material_id), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -CREATE TABLE inbound_orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL DEFAULT 1, warehouse_id BIGINT NOT NULL DEFAULT 1, location_id BIGINT NOT NULL DEFAULT 1, inbound_date DATETIME NOT NULL, handler VARCHAR(100) NOT NULL, supplier VARCHAR(200), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_inbound_orders_date(inbound_date), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE inbound_orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL DEFAULT 1, warehouse_id BIGINT NOT NULL DEFAULT 1, location_id BIGINT NOT NULL DEFAULT 1, inbound_no VARCHAR(32) UNIQUE, operator VARCHAR(100), inbound_date DATETIME NOT NULL, handler VARCHAR(100) NOT NULL, supplier VARCHAR(200), remark TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_inbound_orders_date(inbound_date), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE inbound_items (id BIGINT AUTO_INCREMENT PRIMARY KEY, inbound_order_id BIGINT NOT NULL, material_id BIGINT NOT NULL, quantity INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_inbound_items_order(inbound_order_id), FOREIGN KEY(inbound_order_id) REFERENCES inbound_orders(id) ON DELETE CASCADE, FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -CREATE TABLE outbound_orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL DEFAULT 1, warehouse_id BIGINT NOT NULL DEFAULT 1, location_id BIGINT NOT NULL DEFAULT 1, outbound_date DATETIME NOT NULL, handler VARCHAR(100) NOT NULL, receiver VARCHAR(100) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_outbound_orders_date(outbound_date), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE outbound_orders (id BIGINT AUTO_INCREMENT PRIMARY KEY, organization_id BIGINT NOT NULL DEFAULT 1, warehouse_id BIGINT NOT NULL DEFAULT 1, location_id BIGINT NOT NULL DEFAULT 1, outbound_no VARCHAR(32) UNIQUE, operator VARCHAR(100), outbound_date DATETIME NOT NULL, handler VARCHAR(100) NOT NULL, receiver VARCHAR(100) NOT NULL, remark TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_outbound_orders_date(outbound_date), FOREIGN KEY(organization_id) REFERENCES organizations(id), FOREIGN KEY(warehouse_id) REFERENCES warehouses(id), FOREIGN KEY(location_id) REFERENCES warehouse_locations(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE outbound_items (id BIGINT AUTO_INCREMENT PRIMARY KEY, outbound_order_id BIGINT NOT NULL, material_id BIGINT NOT NULL, quantity INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_outbound_items_order(outbound_order_id), FOREIGN KEY(outbound_order_id) REFERENCES outbound_orders(id) ON DELETE CASCADE, FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE material_logs (id BIGINT AUTO_INCREMENT PRIMARY KEY, operation_type VARCHAR(50) NOT NULL, material_id BIGINT NOT NULL, old_value TEXT, new_value TEXT, operator VARCHAR(100), description TEXT, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_material_logs_material(material_id), FOREIGN KEY(material_id) REFERENCES materials(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE stock_logs (id BIGINT AUTO_INCREMENT PRIMARY KEY, material_id BIGINT NOT NULL, type ENUM('in','out') NOT NULL, quantity INT NOT NULL, related_order_type VARCHAR(50) NOT NULL, related_order_id BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_stock_logs_material(material_id), KEY idx_stock_logs_related_order(related_order_type,related_order_id), FOREIGN KEY(material_id) REFERENCES materials(id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/启动项目.bat b/启动项目.bat index 6f452b48..1bfa57ab 100644 --- a/启动项目.bat +++ b/启动项目.bat @@ -4,7 +4,8 @@ title Warehouse Management System Launcher cd /d "%~dp0" echo Starting backend (8082)... -start "Warehouse MMS - Backend" /D "%~dp0" cmd /k "server.exe" +rem Run the current source so local testing never uses an outdated server.exe. +start "Warehouse MMS - Backend" /D "%~dp0" cmd /k "go run ./cmd/server" echo Starting frontend (8083)... start "Warehouse MMS - Frontend" /D "%~dp0frontend" cmd /k "npm run dev"