package repository import ( "database/sql" "time" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/pkg/database" ) // orderQuerier 同时被 *sql.DB 与 *sql.Tx 满足,让单据头表加明细的读取逻辑能在 // 事务内外复用:事务内用 FOR UPDATE 锁定当前公司的原单据,事务外只做普通读取。 type orderQuerier interface { Query(query string, args ...interface{}) (*sql.Rows, error) QueryRow(query string, args ...interface{}) *sql.Row } type InboundRepo struct{} func NewInboundRepo() *InboundRepo { return &InboundRepo{} } func (r *InboundRepo) Create(order *model.InboundOrder) error { return r.CreateTx(nil, order) } func (r *InboundRepo) CreateTx(tx *sql.Tx, order *model.InboundOrder) 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 inbound_orders (organization_id, warehouse_id, location_id, inbound_date, operator, handler, supplier, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` result, err := tx.Exec(query, order.OrganizationID, order.WarehouseID, order.LocationID, order.InboundDate, order.Operator, order.Handler, order.Supplier, order.Remark) if err != nil { return err } orderID, err := result.LastInsertId() if err != nil { 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] item.InboundOrderID = orderID query := `INSERT INTO inbound_items (inbound_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 } // loadInboundItems 读取单据明细,并 JOIN 头表与物料表把明细限制在同一组织内: // 头表已按组织过滤,明细物料也必须属于同一公司,避免读到跨公司的物料行。 func loadInboundItems(q orderQuerier, orderID int64) ([]model.InboundItem, error) { itemsQuery := `SELECT ii.id, ii.inbound_order_id, ii.material_id, ii.quantity, m.material_code, m.name, m.status FROM inbound_items ii JOIN inbound_orders io ON io.id = ii.inbound_order_id JOIN materials m ON m.id = ii.material_id AND m.organization_id = io.organization_id WHERE ii.inbound_order_id = ?` rows, err := q.Query(itemsQuery, orderID) if err != nil { return nil, err } defer rows.Close() var items []model.InboundItem for rows.Next() { var item model.InboundItem if err := rows.Scan(&item.ID, &item.InboundOrderID, &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 *InboundRepo) getByID(q orderQuerier, organizationID, id int64, lock bool) (*model.InboundOrder, error) { order := &model.InboundOrder{} query := `SELECT id, COALESCE(inbound_no, CONCAT('IN-', LPAD(id, 8, '0'))), COALESCE(operator, ''), inbound_date, handler, supplier, COALESCE(remark, ''), organization_id, warehouse_id, location_id, created_at, updated_at FROM inbound_orders WHERE id = ? AND organization_id = ?` if lock { query += ` FOR UPDATE` } err := q.QueryRow(query, id, organizationID).Scan(&order.ID, &order.InboundNo, &order.Operator, &order.InboundDate, &order.Handler, &order.Supplier, &order.Remark, &order.OrganizationID, &order.WarehouseID, &order.LocationID, &order.CreatedAt, &order.UpdatedAt) if err != nil { return nil, err } order.Items, err = loadInboundItems(q, id) if err != nil { return nil, err } // 状态不落库:inbound_orders 没有 status 列,统一在这里给出。引入状态机时改为真实读写。 order.Status = "completed" return order, nil } func (r *InboundRepo) GetByID(organizationID, id int64) (*model.InboundOrder, error) { return r.getByID(database.DB, organizationID, id, false) } // GetByIDForUpdateTx 在事务内锁定当前公司的原单据,别的公司的单据命中 ErrNoRows。 func (r *InboundRepo) GetByIDForUpdateTx(tx *sql.Tx, organizationID, id int64) (*model.InboundOrder, error) { return r.getByID(tx, organizationID, id, true) } func (r *InboundRepo) List(organizationID int64, search string, limit, offset int) ([]model.InboundOrder, int64, error) { // COUNT 和主查询是两条独立 SQL,组织条件与筛选条件必须成对出现,否则分页总数会和 // 实际行数对不上。 countQuery := `SELECT COUNT(*) FROM inbound_orders WHERE organization_id = ?` countArgs := []interface{}{organizationID} if search != "" { countQuery += ` AND (handler LIKE ? OR supplier 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(inbound_no, CONCAT('IN-', LPAD(id, 8, '0'))), COALESCE(operator, ''), inbound_date, handler, supplier, COALESCE(remark, ''), organization_id, warehouse_id, location_id, created_at, updated_at FROM inbound_orders WHERE organization_id = ?` args := []interface{}{organizationID} if search != "" { query += ` AND (handler LIKE ? OR supplier 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.InboundOrder for rows.Next() { var order model.InboundOrder err := rows.Scan(&order.ID, &order.InboundNo, &order.Operator, &order.InboundDate, &order.Handler, &order.Supplier, &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 *InboundRepo) GetByMaterialID(organizationID, materialID int64) ([]model.InboundRecord, error) { query := `SELECT ii.id, ii.inbound_order_id, io.handler, io.supplier, ii.quantity, ii.created_at FROM inbound_items ii JOIN inbound_orders io ON ii.inbound_order_id = io.id JOIN materials m ON ii.material_id = m.id AND m.organization_id = io.organization_id WHERE ii.material_id = ? AND io.organization_id = ? ORDER BY ii.id DESC` rows, err := database.DB.Query(query, materialID, organizationID) if err != nil { return nil, err } defer rows.Close() var records []model.InboundRecord for rows.Next() { var rec model.InboundRecord err := rows.Scan(&rec.ID, &rec.InboundID, &rec.Handler, &rec.Supplier, &rec.Quantity, &rec.CreatedAt) if err != nil { return nil, err } records = append(records, rec) } return records, nil } func (r *InboundRepo) GetInboundDate(organizationID, id int64) (time.Time, error) { var inboundDate time.Time query := `SELECT inbound_date FROM inbound_orders WHERE id = ? AND organization_id = ?` err := database.DB.QueryRow(query, id, organizationID).Scan(&inboundDate) return inboundDate, err } func (r *InboundRepo) Delete(organizationID, id int64) error { return r.DeleteTx(nil, organizationID, id) } func (r *InboundRepo) 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 inbound_orders WHERE organization_id = ? AND id = ?`, organizationID, id) if err != nil { return err } return tx.Commit() } _, err := tx.Exec(`DELETE FROM inbound_orders WHERE organization_id = ? AND id = ?`, organizationID, id) return err } func (r *InboundRepo) Update(organizationID, id int64, req *model.UpdateInboundRequest) error { return r.UpdateTx(nil, organizationID, id, req) } func (r *InboundRepo) UpdateTx(tx *sql.Tx, organizationID, id int64, req *model.UpdateInboundRequest) 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 inbound_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 inbound_orders SET handler = ?, supplier = ?, remark = ? WHERE organization_id = ? AND id = ?` _, err = tx.Exec(query, req.Handler, req.Supplier, req.Remark, organizationID, id) if err != nil { return err } // Delete old items _, err = tx.Exec(`DELETE FROM inbound_items WHERE inbound_order_id = ?`, id) if err != nil { return err } // Insert new items for i := range req.Items { item := &req.Items[i] query := `INSERT INTO inbound_items (inbound_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 }