678 lines
17 KiB
Go
678 lines
17 KiB
Go
package service
|
||
|
||
import (
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/freefire/jiang13-bbs/model"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
var (
|
||
ErrInsufficientPoints = errors.New("积分不足")
|
||
ErrInvalidPoints = errors.New("积分数量无效")
|
||
)
|
||
|
||
// PointsService 用户积分账户
|
||
type PointsService struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
func NewPointsService(db *gorm.DB) *PointsService {
|
||
return &PointsService{db: db}
|
||
}
|
||
|
||
// Balance 查询可用积分
|
||
func (s *PointsService) Balance(userID uint) (int, error) {
|
||
var u model.User
|
||
if err := s.db.Select("id", "points").First(&u, userID).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return u.Points, nil
|
||
}
|
||
|
||
// CreditTx 在事务内入账
|
||
func CreditTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
|
||
if delta <= 0 {
|
||
return 0, ErrInvalidPoints
|
||
}
|
||
var u model.User
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Select("id", "points").First(&u, userID).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
bal := u.Points + delta
|
||
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("points", bal).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
if err := tx.Create(&model.PointLedger{
|
||
UserID: userID,
|
||
Delta: delta,
|
||
Balance: bal,
|
||
Reason: reason,
|
||
RefType: refType,
|
||
RefID: refID,
|
||
Note: note,
|
||
}).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return bal, nil
|
||
}
|
||
|
||
// DebitTx 在事务内扣款
|
||
func DebitTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
|
||
if delta <= 0 {
|
||
return 0, ErrInvalidPoints
|
||
}
|
||
var u model.User
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||
Select("id", "points").First(&u, userID).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
if u.Points < delta {
|
||
return 0, ErrInsufficientPoints
|
||
}
|
||
bal := u.Points - delta
|
||
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("points", bal).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
if err := tx.Create(&model.PointLedger{
|
||
UserID: userID,
|
||
Delta: -delta,
|
||
Balance: bal,
|
||
Reason: reason,
|
||
RefType: refType,
|
||
RefID: refID,
|
||
Note: note,
|
||
}).Error; err != nil {
|
||
return 0, err
|
||
}
|
||
return bal, nil
|
||
}
|
||
|
||
// Credit 入账
|
||
func (s *PointsService) Credit(userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
|
||
var bal int
|
||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||
var e error
|
||
bal, e = CreditTx(tx, userID, delta, reason, refType, refID, note)
|
||
return e
|
||
})
|
||
return bal, err
|
||
}
|
||
|
||
// Debit 扣款
|
||
func (s *PointsService) Debit(userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
|
||
var bal int
|
||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||
var e error
|
||
bal, e = DebitTx(tx, userID, delta, reason, refType, refID, note)
|
||
return e
|
||
})
|
||
return bal, err
|
||
}
|
||
|
||
// LedgerItem 流水展示项
|
||
type LedgerItem struct {
|
||
ID uint `json:"id"`
|
||
Delta int `json:"delta"`
|
||
Balance int `json:"balance"`
|
||
Reason string `json:"reason"`
|
||
RefType string `json:"ref_type"`
|
||
RefID uint `json:"ref_id"`
|
||
Note string `json:"note"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
}
|
||
|
||
// LedgerParty 管理端流水相关用户摘要
|
||
type LedgerParty struct {
|
||
ID uint `json:"id"`
|
||
Username string `json:"username"`
|
||
Nickname string `json:"nickname"`
|
||
Avatar string `json:"avatar"`
|
||
}
|
||
|
||
// AdminLedgerItem 管理端最近流水:单边入账带 user;用户互转合并为 from → to
|
||
type AdminLedgerItem struct {
|
||
ID uint `json:"id"`
|
||
Delta int `json:"delta"`
|
||
Balance int `json:"balance"`
|
||
Reason string `json:"reason"`
|
||
RefType string `json:"ref_type"`
|
||
RefID uint `json:"ref_id"`
|
||
Note string `json:"note"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
User *LedgerParty `json:"user,omitempty"`
|
||
From *LedgerParty `json:"from,omitempty"`
|
||
To *LedgerParty `json:"to,omitempty"`
|
||
}
|
||
|
||
const (
|
||
adminRecentLedgerLimit = 15
|
||
adminRecentLedgerFetch = 32
|
||
ledgerPairMaxDist = 8
|
||
)
|
||
|
||
func absInt(n int) int {
|
||
if n < 0 {
|
||
return -n
|
||
}
|
||
return n
|
||
}
|
||
|
||
func ledgerPartyOf(u model.User) LedgerParty {
|
||
return LedgerParty{ID: u.ID, Username: u.Username, Nickname: u.Nickname, Avatar: u.Avatar}
|
||
}
|
||
|
||
func copyLedgerParty(p LedgerParty) *LedgerParty {
|
||
if p.ID == 0 {
|
||
return nil
|
||
}
|
||
cp := p
|
||
return &cp
|
||
}
|
||
|
||
// complementaryLedgerRef 用户互转对向 ref_type;非互转返回空
|
||
func complementaryLedgerRef(reason, refType string) string {
|
||
switch reason {
|
||
case model.PointReasonUnlockPost:
|
||
if refType == "post" {
|
||
return "post_earn"
|
||
}
|
||
if refType == "post_earn" {
|
||
return "post"
|
||
}
|
||
case model.PointReasonDownloadFile:
|
||
if refType == "attachment" {
|
||
return "attachment_earn"
|
||
}
|
||
if refType == "attachment_earn" {
|
||
return "attachment"
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func ledgersComplement(a, b model.PointLedger) bool {
|
||
comp := complementaryLedgerRef(a.Reason, a.RefType)
|
||
if comp == "" || b.Reason != a.Reason || b.RefID != a.RefID || b.RefType != comp {
|
||
return false
|
||
}
|
||
if absInt(a.Delta) != absInt(b.Delta) || absInt(a.Delta) == 0 {
|
||
return false
|
||
}
|
||
return (a.Delta > 0) != (b.Delta > 0)
|
||
}
|
||
|
||
func idDist(a, b uint) int {
|
||
if a > b {
|
||
return int(a - b)
|
||
}
|
||
return int(b - a)
|
||
}
|
||
|
||
func findComplementIndex(rows []model.PointLedger, i int, used []bool) int {
|
||
r := rows[i]
|
||
if complementaryLedgerRef(r.Reason, r.RefType) == "" {
|
||
return -1
|
||
}
|
||
best := -1
|
||
bestDist := ledgerPairMaxDist + 1
|
||
for j := range rows {
|
||
if j == i || used[j] || !ledgersComplement(r, rows[j]) {
|
||
continue
|
||
}
|
||
d := idDist(r.ID, rows[j].ID)
|
||
if d == 0 || d > ledgerPairMaxDist || d >= bestDist {
|
||
continue
|
||
}
|
||
bestDist = d
|
||
best = j
|
||
}
|
||
return best
|
||
}
|
||
|
||
// pairLedgerIndices 将用户互转的支出/收益配成一对;返回 [本条, 对向](无对向则为 -1)
|
||
func pairLedgerIndices(rows []model.PointLedger) [][2]int {
|
||
used := make([]bool, len(rows))
|
||
out := make([][2]int, 0, len(rows))
|
||
for i := range rows {
|
||
if used[i] {
|
||
continue
|
||
}
|
||
j := findComplementIndex(rows, i, used)
|
||
if j >= 0 {
|
||
used[i] = true
|
||
used[j] = true
|
||
out = append(out, [2]int{i, j})
|
||
continue
|
||
}
|
||
used[i] = true
|
||
out = append(out, [2]int{i, -1})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func mergePairedLedger(a, b model.PointLedger) model.PointLedger {
|
||
newer, older := a, b
|
||
if b.ID > a.ID {
|
||
newer, older = b, a
|
||
}
|
||
out := newer
|
||
out.Delta = absInt(newer.Delta)
|
||
if newer.Delta < 0 {
|
||
out.Delta = absInt(older.Delta)
|
||
}
|
||
debit, credit := a, b
|
||
if a.Delta > 0 {
|
||
debit, credit = b, a
|
||
}
|
||
if debit.Note != "" {
|
||
out.Note = debit.Note
|
||
} else {
|
||
out.Note = credit.Note
|
||
}
|
||
return out
|
||
}
|
||
|
||
func transferEnds(a, b model.PointLedger) (fromID, toID uint) {
|
||
if a.Delta < 0 {
|
||
return a.UserID, b.UserID
|
||
}
|
||
if b.Delta < 0 {
|
||
return b.UserID, a.UserID
|
||
}
|
||
if a.Delta > 0 {
|
||
return b.UserID, a.UserID
|
||
}
|
||
return a.UserID, b.UserID
|
||
}
|
||
|
||
// Ledger 分页查询本人积分流水
|
||
func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64, error) {
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if size < 1 || size > 50 {
|
||
size = 20
|
||
}
|
||
var total int64
|
||
q := s.db.Model(&model.PointLedger{}).Where("user_id = ?", userID)
|
||
if err := q.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
var rows []model.PointLedger
|
||
if err := q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
out := make([]LedgerItem, 0, len(rows))
|
||
for _, r := range rows {
|
||
out = append(out, LedgerItem{
|
||
ID: r.ID, Delta: r.Delta, Balance: r.Balance, Reason: r.Reason,
|
||
RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
|
||
})
|
||
}
|
||
return out, total, nil
|
||
}
|
||
|
||
// DeltaDay 单日积分发放与消耗
|
||
type DeltaDay struct {
|
||
Date string `json:"date"`
|
||
Granted int64 `json:"granted"`
|
||
Spent int64 `json:"spent"`
|
||
}
|
||
|
||
// PointsEconomyStats 全站积分与类型帖概览(管理端)
|
||
type PointsEconomyStats struct {
|
||
TotalBalance int64 `json:"total_balance"`
|
||
LedgerCount int64 `json:"ledger_count"`
|
||
CheckinToday int64 `json:"checkin_today"`
|
||
OpenBounties int64 `json:"open_bounties"`
|
||
EscrowedPoints int64 `json:"escrowed_points"`
|
||
PostsByType map[string]int64 `json:"posts_by_type"`
|
||
RecentLedger []AdminLedgerItem `json:"recent_ledger"`
|
||
CheckinSeries []DayCount `json:"checkin_series"`
|
||
LedgerByReason []NamedCount `json:"ledger_by_reason"`
|
||
DeltaSeries []DeltaDay `json:"delta_series"`
|
||
NetDelta7d int64 `json:"net_delta_7d"`
|
||
}
|
||
|
||
// AdminEconomyStats 管理端经济看板
|
||
func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
|
||
st := &PointsEconomyStats{
|
||
PostsByType: map[string]int64{},
|
||
CheckinSeries: make([]DayCount, 0),
|
||
LedgerByReason: make([]NamedCount, 0),
|
||
DeltaSeries: make([]DeltaDay, 0),
|
||
RecentLedger: make([]AdminLedgerItem, 0),
|
||
}
|
||
if err := s.db.Model(&model.User{}).Where("deleted_at IS NULL").
|
||
Select("COALESCE(SUM(points),0)").Scan(&st.TotalBalance).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if err := s.db.Model(&model.PointLedger{}).Count(&st.LedgerCount).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if err := s.db.Model(&model.Checkin{}).
|
||
Where("checkin_date::date = CURRENT_DATE").
|
||
Count(&st.CheckinToday).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
type row struct {
|
||
PostType string
|
||
Cnt int64
|
||
}
|
||
var rows []row
|
||
if err := s.db.Model(&model.Post{}).
|
||
Select("post_type, count(*) as cnt").
|
||
Where("deleted_at IS NULL").
|
||
Group("post_type").Scan(&rows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, r := range rows {
|
||
pt := model.NormalizePostType(r.PostType)
|
||
st.PostsByType[pt] += r.Cnt
|
||
}
|
||
|
||
var bountyPosts []model.Post
|
||
if err := s.db.Select("id, type_meta").
|
||
Where("post_type = ? AND deleted_at IS NULL", model.PostTypeBounty).
|
||
Find(&bountyPosts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, p := range bountyPosts {
|
||
m, err := parseBountyMeta(p.TypeMeta)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if m.Escrowed && m.AcceptedCommentID == 0 && !m.Refunded && !m.Expired {
|
||
st.OpenBounties++
|
||
st.EscrowedPoints += int64(m.Points)
|
||
}
|
||
}
|
||
|
||
recent, err := s.buildAdminRecentLedger()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
st.RecentLedger = recent
|
||
|
||
now := time.Now()
|
||
day0 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
start14 := day0.AddDate(0, 0, -13)
|
||
start7 := day0.AddDate(0, 0, -6)
|
||
|
||
type dayCnt struct {
|
||
Day string
|
||
Cnt int64
|
||
}
|
||
var checkinRows []dayCnt
|
||
if err := s.db.Raw(`
|
||
SELECT to_char(checkin_date, 'YYYY-MM-DD') AS day, count(*) AS cnt
|
||
FROM checkins
|
||
WHERE checkin_date >= ? AND checkin_date <= ?
|
||
GROUP BY day ORDER BY day`, start14, day0).Scan(&checkinRows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
checkinMap := map[string]int64{}
|
||
for _, r := range checkinRows {
|
||
checkinMap[r.Day] = r.Cnt
|
||
}
|
||
st.CheckinSeries = fillDaySeries(start14, day0, checkinMap)
|
||
|
||
type reasonRow struct {
|
||
Reason string
|
||
Cnt int64
|
||
}
|
||
var reasons []reasonRow
|
||
if err := s.db.Model(&model.PointLedger{}).
|
||
Select("reason, count(*) as cnt").
|
||
Group("reason").
|
||
Order("cnt DESC").
|
||
Limit(12).Scan(&reasons).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, r := range reasons {
|
||
st.LedgerByReason = append(st.LedgerByReason, NamedCount{
|
||
Key: r.Reason, Label: r.Reason, Count: r.Cnt,
|
||
})
|
||
}
|
||
|
||
type deltaRow struct {
|
||
Day string
|
||
Granted int64
|
||
Spent int64
|
||
}
|
||
var deltas []deltaRow
|
||
if err := s.db.Raw(`
|
||
SELECT to_char(created_at, 'YYYY-MM-DD') AS day,
|
||
COALESCE(SUM(CASE WHEN delta > 0 THEN delta ELSE 0 END), 0) AS granted,
|
||
COALESCE(SUM(CASE WHEN delta < 0 THEN -delta ELSE 0 END), 0) AS spent
|
||
FROM point_ledgers
|
||
WHERE created_at >= ? AND created_at < ?
|
||
GROUP BY day ORDER BY day`,
|
||
start14, day0.AddDate(0, 0, 1)).Scan(&deltas).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
deltaMap := map[string]DeltaDay{}
|
||
for _, r := range deltas {
|
||
deltaMap[r.Day] = DeltaDay{Date: r.Day, Granted: r.Granted, Spent: r.Spent}
|
||
}
|
||
for d := start14; !d.After(day0); d = d.AddDate(0, 0, 1) {
|
||
k := dateKey(d)
|
||
dd := deltaMap[k]
|
||
dd.Date = k
|
||
st.DeltaSeries = append(st.DeltaSeries, dd)
|
||
if !d.Before(start7) {
|
||
st.NetDelta7d += dd.Granted - dd.Spent
|
||
}
|
||
}
|
||
|
||
return st, nil
|
||
}
|
||
|
||
func (s *PointsService) lookupLedgerMate(r model.PointLedger) (*model.PointLedger, error) {
|
||
comp := complementaryLedgerRef(r.Reason, r.RefType)
|
||
if comp == "" || r.RefID == 0 {
|
||
return nil, nil
|
||
}
|
||
amt := absInt(r.Delta)
|
||
if amt == 0 {
|
||
return nil, nil
|
||
}
|
||
lo, hi := uint(1), r.ID+uint(ledgerPairMaxDist)
|
||
if r.ID > uint(ledgerPairMaxDist) {
|
||
lo = r.ID - uint(ledgerPairMaxDist)
|
||
}
|
||
var mates []model.PointLedger
|
||
q := s.db.Where("reason = ? AND ref_id = ? AND ref_type = ? AND id BETWEEN ? AND ? AND id <> ?",
|
||
r.Reason, r.RefID, comp, lo, hi, r.ID)
|
||
if r.Delta > 0 {
|
||
q = q.Where("delta = ?", -amt)
|
||
} else {
|
||
q = q.Where("delta = ?", amt)
|
||
}
|
||
if err := q.Find(&mates).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
best := -1
|
||
bestDist := ledgerPairMaxDist + 1
|
||
for i := range mates {
|
||
d := idDist(r.ID, mates[i].ID)
|
||
if d == 0 || d > ledgerPairMaxDist || d >= bestDist {
|
||
continue
|
||
}
|
||
bestDist = d
|
||
best = i
|
||
}
|
||
if best < 0 {
|
||
return nil, nil
|
||
}
|
||
return &mates[best], nil
|
||
}
|
||
|
||
func (s *PointsService) loadLedgerParties(ids []uint) (map[uint]LedgerParty, error) {
|
||
out := map[uint]LedgerParty{}
|
||
if len(ids) == 0 {
|
||
return out, nil
|
||
}
|
||
seen := map[uint]struct{}{}
|
||
uniq := make([]uint, 0, len(ids))
|
||
for _, id := range ids {
|
||
if id == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[id]; ok {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
uniq = append(uniq, id)
|
||
}
|
||
if len(uniq) == 0 {
|
||
return out, nil
|
||
}
|
||
var users []model.User
|
||
if err := s.db.Unscoped().Select("id", "username", "nickname", "avatar").
|
||
Where("id IN ?", uniq).Find(&users).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, u := range users {
|
||
out[u.ID] = ledgerPartyOf(u)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *PointsService) bountyPayers(rows []model.PointLedger) (map[uint]uint, error) {
|
||
ids := make([]uint, 0)
|
||
seen := map[uint]struct{}{}
|
||
for _, r := range rows {
|
||
if r.Reason != model.PointReasonBountyAward || r.RefType != "post" || r.RefID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[r.RefID]; ok {
|
||
continue
|
||
}
|
||
seen[r.RefID] = struct{}{}
|
||
ids = append(ids, r.RefID)
|
||
}
|
||
out := map[uint]uint{}
|
||
if len(ids) == 0 {
|
||
return out, nil
|
||
}
|
||
var posts []model.Post
|
||
if err := s.db.Unscoped().Select("id", "user_id").Where("id IN ?", ids).Find(&posts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
for _, p := range posts {
|
||
out[p.ID] = p.UserID
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *PointsService) buildAdminRecentLedger() ([]AdminLedgerItem, error) {
|
||
out := make([]AdminLedgerItem, 0, adminRecentLedgerLimit)
|
||
var recent []model.PointLedger
|
||
if err := s.db.Order("id DESC").Limit(adminRecentLedgerFetch).Find(&recent).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
if len(recent) == 0 {
|
||
return out, nil
|
||
}
|
||
|
||
pairs := pairLedgerIndices(recent)
|
||
outside := map[int]model.PointLedger{}
|
||
for _, p := range pairs {
|
||
if p[1] >= 0 {
|
||
continue
|
||
}
|
||
r := recent[p[0]]
|
||
if complementaryLedgerRef(r.Reason, r.RefType) == "" {
|
||
continue
|
||
}
|
||
mate, err := s.lookupLedgerMate(r)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if mate != nil {
|
||
outside[p[0]] = *mate
|
||
}
|
||
}
|
||
|
||
payers, err := s.bountyPayers(recent)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
userIDs := make([]uint, 0, len(recent)+len(outside)+len(payers))
|
||
for _, r := range recent {
|
||
userIDs = append(userIDs, r.UserID)
|
||
}
|
||
for _, m := range outside {
|
||
userIDs = append(userIDs, m.UserID)
|
||
}
|
||
for _, uid := range payers {
|
||
userIDs = append(userIDs, uid)
|
||
}
|
||
parties, err := s.loadLedgerParties(userIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
party := func(id uint) *LedgerParty {
|
||
if id == 0 {
|
||
return nil
|
||
}
|
||
if p, ok := parties[id]; ok {
|
||
return copyLedgerParty(p)
|
||
}
|
||
return &LedgerParty{ID: id, Nickname: "已注销"}
|
||
}
|
||
|
||
itemFrom := func(r model.PointLedger) AdminLedgerItem {
|
||
return AdminLedgerItem{
|
||
ID: r.ID, Delta: r.Delta, Balance: r.Balance, Reason: r.Reason,
|
||
RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
|
||
User: party(r.UserID),
|
||
}
|
||
}
|
||
|
||
for _, p := range pairs {
|
||
if len(out) >= adminRecentLedgerLimit {
|
||
break
|
||
}
|
||
a := recent[p[0]]
|
||
var mate *model.PointLedger
|
||
if p[1] >= 0 {
|
||
m := recent[p[1]]
|
||
mate = &m
|
||
} else if m, ok := outside[p[0]]; ok {
|
||
mate = &m
|
||
}
|
||
|
||
if mate != nil {
|
||
merged := mergePairedLedger(a, *mate)
|
||
fromID, toID := transferEnds(a, *mate)
|
||
item := itemFrom(merged)
|
||
item.Delta = absInt(merged.Delta)
|
||
item.User = nil
|
||
item.From = party(fromID)
|
||
item.To = party(toID)
|
||
out = append(out, item)
|
||
continue
|
||
}
|
||
|
||
item := itemFrom(a)
|
||
if a.Reason == model.PointReasonBountyAward && a.Delta > 0 {
|
||
if payerID := payers[a.RefID]; payerID > 0 && payerID != a.UserID {
|
||
item.From = party(payerID)
|
||
item.To = party(a.UserID)
|
||
item.User = nil
|
||
item.Delta = a.Delta
|
||
}
|
||
}
|
||
out = append(out, item)
|
||
}
|
||
return out, nil
|
||
}
|