material_manage_system/internal/repository/outbound.go
2026-10-09 12:21:53 +08:00

293 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}