package repository import ( "database/sql" "time" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/pkg/database" ) type OutboundRepo struct{} func NewOutboundRepo() *OutboundRepo { return &OutboundRepo{} } func (r *OutboundRepo) Create(order *model.OutboundOrder) error { return r.CreateTx(nil, order) } func (r *OutboundRepo) CreateTx(tx *sql.Tx, order *model.OutboundOrder) error { shouldCommit := false if tx == nil { var err error tx, err = database.DB.Begin() if err != nil { return err } shouldCommit = true } if shouldCommit { defer tx.Rollback() } // 三个库存维度由服务端解析后显式落库;表上这三列 NOT NULL 无默认值,缺一即报错, // 不会静默写进默认公司。 query := `INSERT INTO outbound_orders (organization_id, warehouse_id, location_id, outbound_date, operator, handler, receiver, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` result, err := tx.Exec(query, order.OrganizationID, order.WarehouseID, order.LocationID, order.OutboundDate, order.Operator, order.Handler, order.Receiver, order.Remark) if err != nil { return err } orderID, err := result.LastInsertId() if err != nil { 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] item.OutboundOrderID = orderID query := `INSERT INTO outbound_items (outbound_order_id, material_id, quantity) VALUES (?, ?, ?)` result, err := tx.Exec(query, orderID, item.MaterialID, item.Quantity) if err != nil { return err } itemID, err := result.LastInsertId() if err != nil { return err } item.ID = itemID } if shouldCommit { return tx.Commit() } return nil } // loadOutboundItems 读取单据明细,并 JOIN 头表与物料表把明细限制在同一组织内: // 头表已按组织过滤,明细物料也必须属于同一公司,避免读到跨公司的物料行。 func loadOutboundItems(q orderQuerier, orderID int64) ([]model.OutboundItem, error) { itemsQuery := `SELECT oi.id, oi.outbound_order_id, oi.material_id, oi.quantity, m.material_code, m.name, m.status FROM outbound_items oi JOIN outbound_orders oo ON oo.id = oi.outbound_order_id JOIN materials m ON m.id = oi.material_id AND m.organization_id = oo.organization_id WHERE oi.outbound_order_id = ?` rows, err := q.Query(itemsQuery, orderID) if err != nil { return nil, err } defer rows.Close() var items []model.OutboundItem for rows.Next() { var item model.OutboundItem if err := rows.Scan(&item.ID, &item.OutboundOrderID, &item.MaterialID, &item.Quantity, &item.MaterialCode, &item.MaterialName, &item.MaterialStatus); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } // getByID 按公司读取单据头表加明细。lock=true 时用 FOR UPDATE 锁定当前公司的原单据, // 供更新/删除事务在同一把锁下冲销原库存。别的公司的单据在这里命中 sql.ErrNoRows。 func (r *OutboundRepo) getByID(q orderQuerier, organizationID, id int64, lock bool) (*model.OutboundOrder, error) { order := &model.OutboundOrder{} query := `SELECT id, COALESCE(outbound_no, CONCAT('OUT-', LPAD(id, 8, '0'))), COALESCE(operator, ''), outbound_date, handler, receiver, COALESCE(remark, ''), organization_id, warehouse_id, location_id, created_at, updated_at FROM outbound_orders WHERE id = ? AND organization_id = ?` if lock { query += ` FOR UPDATE` } err := q.QueryRow(query, id, organizationID).Scan(&order.ID, &order.OutboundNo, &order.Operator, &order.OutboundDate, &order.Handler, &order.Receiver, &order.Remark, &order.OrganizationID, &order.WarehouseID, &order.LocationID, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, err } order.Items, err = loadOutboundItems(q, id) if err != nil { return nil, err } // 状态不落库:outbound_orders 没有 status 列,统一在这里给出。引入状态机时改为真实读写。 order.Status = "completed" return order, nil } func (r *OutboundRepo) GetByID(organizationID, id int64) (*model.OutboundOrder, error) { return r.getByID(database.DB, organizationID, id, false) } // GetByIDForUpdateTx 在事务内锁定当前公司的原单据,别的公司的单据命中 ErrNoRows。 func (r *OutboundRepo) GetByIDForUpdateTx(tx *sql.Tx, organizationID, id int64) (*model.OutboundOrder, error) { return r.getByID(tx, organizationID, id, true) } func (r *OutboundRepo) List(organizationID int64, search string, limit, offset int) ([]model.OutboundOrder, int64, error) { // COUNT 和主查询是两条独立 SQL,组织条件与筛选条件必须成对出现,否则分页总数会和 // 实际行数对不上。 countQuery := `SELECT COUNT(*) FROM outbound_orders WHERE organization_id = ?` countArgs := []interface{}{organizationID} if search != "" { countQuery += ` AND (handler LIKE ? OR receiver LIKE ?)` searchPattern := "%" + search + "%" countArgs = append(countArgs, searchPattern, searchPattern) } var total int64 err := database.DB.QueryRow(countQuery, countArgs...).Scan(&total) if err != nil { return nil, 0, err } query := `SELECT id, COALESCE(outbound_no, CONCAT('OUT-', LPAD(id, 8, '0'))), COALESCE(operator, ''), outbound_date, handler, receiver, COALESCE(remark, ''), organization_id, warehouse_id, location_id, created_at, updated_at FROM outbound_orders WHERE organization_id = ?` args := []interface{}{organizationID} if search != "" { query += ` AND (handler LIKE ? OR receiver LIKE ?)` searchPattern := "%" + search + "%" args = append(args, searchPattern, searchPattern) } query += ` ORDER BY id DESC LIMIT ? OFFSET ?` args = append(args, limit, offset) rows, err := database.DB.Query(query, args...) if err != nil { return nil, 0, err } defer rows.Close() var orders []model.OutboundOrder for rows.Next() { var order model.OutboundOrder err := rows.Scan(&order.ID, &order.OutboundNo, &order.Operator, &order.OutboundDate, &order.Handler, &order.Receiver, &order.Remark, &order.OrganizationID, &order.WarehouseID, &order.LocationID, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, 0, err } // 同上:状态在查询层给出,列表里一并补上。 order.Status = "completed" orders = append(orders, order) } return orders, total, nil } // GetByMaterialID 返回某物料在本公司的出库历史。头表按组织过滤,明细 JOIN 的物料也 // 必须属于同一公司,公司 A 不能凭物料 ID 读到公司 B 的出库记录。 func (r *OutboundRepo) GetByMaterialID(organizationID, materialID int64) ([]model.OutboundRecord, error) { query := `SELECT oi.id, oi.outbound_order_id, oo.handler, oo.receiver, oi.quantity, oi.created_at FROM outbound_items oi JOIN outbound_orders oo ON oi.outbound_order_id = oo.id JOIN materials m ON oi.material_id = m.id AND m.organization_id = oo.organization_id WHERE oi.material_id = ? AND oo.organization_id = ? ORDER BY oi.id DESC` rows, err := database.DB.Query(query, materialID, organizationID) if err != nil { return nil, err } defer rows.Close() var records []model.OutboundRecord for rows.Next() { var rec model.OutboundRecord err := rows.Scan(&rec.ID, &rec.OutboundID, &rec.Handler, &rec.Receiver, &rec.Quantity, &rec.CreatedAt) if err != nil { return nil, err } records = append(records, rec) } return records, nil } func (r *OutboundRepo) GetOutboundDate(organizationID, id int64) (time.Time, error) { var outboundDate time.Time query := `SELECT outbound_date FROM outbound_orders WHERE id = ? AND organization_id = ?` err := database.DB.QueryRow(query, id, organizationID).Scan(&outboundDate) return outboundDate, err } func (r *OutboundRepo) Delete(organizationID, id int64) error { return r.DeleteTx(nil, organizationID, id) } func (r *OutboundRepo) DeleteTx(tx *sql.Tx, organizationID, id int64) error { if tx == nil { var err error tx, err = database.DB.Begin() if err != nil { return err } defer tx.Rollback() _, err = tx.Exec(`DELETE FROM outbound_orders WHERE organization_id = ? AND id = ?`, organizationID, id) if err != nil { return err } return tx.Commit() } _, err := tx.Exec(`DELETE FROM outbound_orders WHERE organization_id = ? AND id = ?`, organizationID, id) return err } func (r *OutboundRepo) Update(organizationID, id int64, req *model.UpdateOutboundRequest) error { return r.UpdateTx(nil, organizationID, id, req) } func (r *OutboundRepo) UpdateTx(tx *sql.Tx, organizationID, id int64, req *model.UpdateOutboundRequest) error { shouldCommit := false if tx == nil { var err error tx, err = database.DB.Begin() if err != nil { return err } shouldCommit = true } if shouldCommit { defer tx.Rollback() } // 先确认目标单据属于本公司:跨公司的单据在这里直接返回 ErrNoRows,后面的明细 // 删除/插入绝不会碰到别家公司的单据(明细表没有组织列,只能靠头表归属收口)。 var existsID int64 err := tx.QueryRow(`SELECT id FROM outbound_orders WHERE organization_id = ? AND id = ?`, organizationID, id).Scan(&existsID) if err == sql.ErrNoRows { return sql.ErrNoRows } if err != nil { return err } // Update order basic fields,写入带组织条件,跨公司的单据不会在这里被改动。 query := `UPDATE outbound_orders SET operator = ?, handler = ?, receiver = ?, remark = ? WHERE organization_id = ? AND id = ?` _, err = tx.Exec(query, req.Operator, req.Handler, req.Receiver, req.Remark, organizationID, id) if err != nil { return err } // Delete old items _, err = tx.Exec(`DELETE FROM outbound_items WHERE outbound_order_id = ?`, id) if err != nil { return err } // Insert new items for i := range req.Items { item := &req.Items[i] query := `INSERT INTO outbound_items (outbound_order_id, material_id, quantity) VALUES (?, ?, ?)` _, err = tx.Exec(query, id, item.MaterialID, item.Quantity) if err != nil { return err } } if shouldCommit { return tx.Commit() } return nil }