184 lines
6.6 KiB
Go
Executable File
184 lines
6.6 KiB
Go
Executable File
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"paygateway/internal/model"
|
|
)
|
|
|
|
// ========== 人工异常处理池 ==========
|
|
|
|
const exceptionCols = `id, out_trade_no, pid, upstream_name, channel, amount_minor, order_status,
|
|
channel_status, channel_tx_id, paid_amount, payer, reason, phase, resolved_action,
|
|
evidence, suggestion, handler, handler_note, resolved_at, updated_at, created_at`
|
|
|
|
func scanException(row *sql.Row) (*model.ExceptionOrder, error) {
|
|
var e model.ExceptionOrder
|
|
var updatedAt, createdAt string
|
|
err := row.Scan(&e.ID, &e.OutTradeNo, &e.Pid, &e.UpstreamName, &e.Channel, &e.AmountMinor,
|
|
&e.OrderStatus, &e.ChannelStatus, &e.ChannelTxID, &e.PaidAmount, &e.Payer, &e.Reason,
|
|
&e.Phase, &e.ResolvedAction, &e.Evidence, &e.Suggestion, &e.Handler, &e.HandlerNote,
|
|
&e.ResolvedAt, &updatedAt, &createdAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.UpdatedAt = updatedAt
|
|
e.CreatedAt = createdAt
|
|
return &e, nil
|
|
}
|
|
|
|
// CreateException 创建异常单(幂等:同 out_trade_no 已存在则返回已有)
|
|
func (s *SQLiteStore) CreateException(ctx context.Context, e *model.ExceptionOrder) error {
|
|
now := time.Now().Format(time.RFC3339)
|
|
if e.PaidAmount == 0 {
|
|
e.PaidAmount = e.AmountMinor
|
|
}
|
|
if e.Phase == "" {
|
|
e.Phase = model.ExPhasePending
|
|
}
|
|
if e.ChannelStatus == "" {
|
|
e.ChannelStatus = "UNKNOWN"
|
|
}
|
|
_, err := s.db.ExecContext(ctx, `INSERT INTO exception_orders
|
|
(out_trade_no, pid, upstream_name, channel, amount_minor, order_status, channel_status,
|
|
channel_tx_id, paid_amount, payer, reason, phase, resolved_action, evidence, suggestion,
|
|
handler, handler_note, resolved_at, updated_at, created_at)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
ON CONFLICT(out_trade_no) DO NOTHING`,
|
|
e.OutTradeNo, e.Pid, e.UpstreamName, e.Channel, e.AmountMinor, e.OrderStatus, e.ChannelStatus,
|
|
e.ChannelTxID, e.PaidAmount, e.Payer, e.Reason, e.Phase, e.ResolvedAction, e.Evidence, e.Suggestion,
|
|
e.Handler, e.HandlerNote, e.ResolvedAt, now, now)
|
|
if err != nil {
|
|
return fmt.Errorf("创建异常单失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SQLiteStore) GetException(ctx context.Context, id int64) (*model.ExceptionOrder, error) {
|
|
row := s.db.QueryRowContext(ctx, `SELECT `+exceptionCols+` FROM exception_orders WHERE id = ?`, id)
|
|
return scanException(row)
|
|
}
|
|
|
|
func (s *SQLiteStore) GetExceptionByOutTradeNo(ctx context.Context, outTradeNo string) (*model.ExceptionOrder, error) {
|
|
row := s.db.QueryRowContext(ctx, `SELECT `+exceptionCols+` FROM exception_orders WHERE out_trade_no = ?`, outTradeNo)
|
|
return scanException(row)
|
|
}
|
|
|
|
func (s *SQLiteStore) ListExceptions(ctx context.Context, phase, reason string, limit, offset int) ([]*model.ExceptionOrder, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
q := `SELECT ` + exceptionCols + ` FROM exception_orders WHERE 1=1`
|
|
args := []any{}
|
|
if phase != "" {
|
|
q += ` AND phase = ?`
|
|
args = append(args, phase)
|
|
}
|
|
if reason != "" {
|
|
q += ` AND reason = ?`
|
|
args = append(args, reason)
|
|
}
|
|
q += ` ORDER BY id DESC LIMIT ? OFFSET ?`
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := s.db.QueryContext(ctx, q, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*model.ExceptionOrder
|
|
for rows.Next() {
|
|
var e model.ExceptionOrder
|
|
var updatedAt, createdAt string
|
|
if err := rows.Scan(&e.ID, &e.OutTradeNo, &e.Pid, &e.UpstreamName, &e.Channel, &e.AmountMinor,
|
|
&e.OrderStatus, &e.ChannelStatus, &e.ChannelTxID, &e.PaidAmount, &e.Payer, &e.Reason,
|
|
&e.Phase, &e.ResolvedAction, &e.Evidence, &e.Suggestion, &e.Handler, &e.HandlerNote,
|
|
&e.ResolvedAt, &updatedAt, &createdAt); err != nil {
|
|
return nil, err
|
|
}
|
|
e.UpdatedAt = updatedAt
|
|
e.CreatedAt = createdAt
|
|
out = append(out, &e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *SQLiteStore) CountExceptions(ctx context.Context, phase string) (int64, error) {
|
|
q := `SELECT COUNT(*) FROM exception_orders`
|
|
args := []any{}
|
|
if phase != "" {
|
|
q += ` WHERE phase = ?`
|
|
args = append(args, phase)
|
|
}
|
|
var n int64
|
|
err := s.db.QueryRowContext(ctx, q, args...).Scan(&n)
|
|
return n, err
|
|
}
|
|
|
|
// UpdateExceptionPhase 条件更新阶段(防并发处理:pending -> processing 锁)
|
|
func (s *SQLiteStore) UpdateExceptionPhase(ctx context.Context, id int64, from, to string) (bool, error) {
|
|
res, err := s.db.ExecContext(ctx, `UPDATE exception_orders SET phase = ?, updated_at = ? WHERE id = ? AND phase = ?`,
|
|
to, time.Now().Format(time.RFC3339), id, from)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
return n > 0, nil
|
|
}
|
|
|
|
// UpdateException 更新异常单(set 修改字段,updated_at 自动刷新)
|
|
func (s *SQLiteStore) UpdateException(ctx context.Context, id int64, set func(e *model.ExceptionOrder)) error {
|
|
e, err := s.GetException(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
set(e)
|
|
_, err = s.db.ExecContext(ctx, `UPDATE exception_orders SET
|
|
channel_status=?, channel_tx_id=?, paid_amount=?, payer=?, reason=?, phase=?,
|
|
resolved_action=?, evidence=?, suggestion=?, handler=?, handler_note=?, resolved_at=?, updated_at=?
|
|
WHERE id=?`,
|
|
e.ChannelStatus, e.ChannelTxID, e.PaidAmount, e.Payer, e.Reason, e.Phase,
|
|
e.ResolvedAction, e.Evidence, e.Suggestion, e.Handler, e.HandlerNote, e.ResolvedAt,
|
|
time.Now().Format(time.RFC3339), id)
|
|
return err
|
|
}
|
|
|
|
func (s *SQLiteStore) InsertExceptionLog(ctx context.Context, exceptionID int64, action, operator, detail string) error {
|
|
_, err := s.db.ExecContext(ctx, `INSERT INTO exception_logs (exception_id, action, operator, detail, created_at)
|
|
VALUES (?,?,?,?,?)`, exceptionID, action, operator, detail, time.Now().Format(time.RFC3339))
|
|
return err
|
|
}
|
|
|
|
func (s *SQLiteStore) ListExceptionLogs(ctx context.Context, exceptionID int64, limit int) ([]*model.ExceptionLog, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, exception_id, action, operator, detail, created_at
|
|
FROM exception_logs WHERE exception_id = ? ORDER BY id DESC LIMIT ?`, exceptionID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*model.ExceptionLog
|
|
for rows.Next() {
|
|
var l model.ExceptionLog
|
|
if err := rows.Scan(&l.ID, &l.ExceptionID, &l.Action, &l.Operator, &l.Detail, &l.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, &l)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// UpdateOrderHandled 订单人工处理标记(人工池处理后永久留痕)
|
|
func (s *SQLiteStore) UpdateOrderHandled(ctx context.Context, outTradeNo string, exceptionID int64, action, handler, note string) error {
|
|
_, err := s.db.ExecContext(ctx, `UPDATE orders SET
|
|
exception_id = ?, handled_by = ?, handled_action = ?, handled_note = ?, handled_at = ?
|
|
WHERE out_trade_no = ?`,
|
|
exceptionID, handler, action, note, time.Now().Format(time.RFC3339), outTradeNo)
|
|
return err
|
|
}
|