feat(phase2): 数据回传聚合与可信分账(F09/F18)

- internal/playback: 播放事件存储/MA码维度聚合/分账结算(CP60/平台34/服务费6)
- service: ReportPlayback(链上状态门禁)/PlaybackSummary/ComputeSettlement
- api: /data/playback, /data/playback-summary, /settlement/compute
- 分账取余兜底无丢分; 未知/已下架MA码回传被拒
- 13项新测试通过; 端到端验证: 回传3条→聚合40元→分账24/13.6/2.4
This commit is contained in:
selfrelease
2026-06-14 17:00:57 +08:00
parent a329d4906b
commit f44c53c5bb
8 changed files with 389 additions and 14 deletions
+31
View File
@@ -174,3 +174,34 @@ func (s *Service) RestoreEpisode(role chain.Role, maCode string, episode int) er
func certContainsMA(cert, maCode string) bool {
return cert != "" && maCode != "" && strings.Contains(cert, maCode)
}
// ---- 二期 F09/F18:数据回传聚合与分账(需求9/需求21) ----
// ReportPlayback 运营商以 MA 码为维度批量回传播放/消费事件(需求9-AC1)。
// 仅当 MA 码存在且处于流通状态时接收,保证数据归属可信。
func (s *Service) ReportPlayback(events []model.PlaybackEvent) (accepted int, rejected int) {
valid := make([]model.PlaybackEvent, 0, len(events))
for _, e := range events {
c, err := s.chain.QueryContent(e.MACode)
if err != nil || c.Status == model.StatusRevoked {
rejected++
continue
}
valid = append(valid, e)
}
accepted = s.pb.Ingest(valid)
return accepted, rejected
}
// PlaybackSummary 查询按 MA 码聚合的可信播放数据(需求9-AC2/AC3)。
func (s *Service) PlaybackSummary(maCode string) model.PlaybackSummary {
return s.pb.Summary(maCode)
}
// ComputeSettlement 基于可信播放数据计算分账(需求21-AC3)。
func (s *Service) ComputeSettlement(maCode, period string) (model.Settlement, error) {
if _, err := s.chain.QueryContent(maCode); err != nil {
return model.Settlement{}, err
}
return s.pb.ComputeSettlement(maCode, period, model.DefaultShareConfig())
}
+3 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/macode"
"github.com/tcs-iptv/tcs/internal/model"
"github.com/tcs-iptv/tcs/internal/playback"
)
// 业务错误。
@@ -49,6 +50,7 @@ type SubmissionResult struct {
type Service struct {
chain chain.Client
gen *macode.Generator
pb *playback.Store
mu sync.Mutex
seqMu sync.Mutex
seqs map[string]int // 按前缀独立计数(REV/ctid/DIST 各自从 1 递增)
@@ -65,7 +67,7 @@ type reviewItem struct {
// New 创建业务服务。
func New(c chain.Client, gen *macode.Generator) *Service {
return &Service{chain: c, gen: gen, seqs: make(map[string]int), reviews: make(map[string]*reviewItem)}
return &Service{chain: c, gen: gen, pb: playback.NewStore(), seqs: make(map[string]int), reviews: make(map[string]*reviewItem)}
}
func (s *Service) nextID(prefix string) string {
@@ -0,0 +1,59 @@
package service
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tcs-iptv/tcs/internal/chain"
"github.com/tcs-iptv/tcs/internal/model"
)
func TestReportPlaybackAndSettle(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
// 运营商回传播放/购买事件
acc, rej := s.ReportPlayback([]model.PlaybackEvent{
{MACode: maCode, PlatformID: "CT-SX", EventType: model.EventPlay},
{MACode: maCode, PlatformID: "CT-SX", EventType: model.EventPurchase, RevenueCent: 1500},
{MACode: maCode, PlatformID: "CM-SX", EventType: model.EventPurchase, RevenueCent: 2500},
})
assert.Equal(t, 3, acc)
assert.Equal(t, 0, rej)
// 聚合可信播放数据
sum := s.PlaybackSummary(maCode)
assert.Equal(t, int64(4000), sum.TotalRevenue)
assert.Equal(t, int64(1), sum.TotalPlays)
// 分账:CP60/平台34/服务费6
st, err := s.ComputeSettlement(maCode, "2026-06")
require.NoError(t, err)
assert.Equal(t, int64(4000), st.TotalRevenue)
assert.Equal(t, int64(2400), st.CPShare)
assert.Equal(t, int64(1360), st.PlatformShare)
assert.Equal(t, int64(240), st.HubFee)
assert.Equal(t, st.TotalRevenue, st.CPShare+st.PlatformShare+st.HubFee)
}
func TestReportPlayback_RejectsUnknownOrRevoked(t *testing.T) {
s := newService(t)
maCode, _, _ := issueOne(t, s)
// 未知 MA 码被拒
acc, rej := s.ReportPlayback([]model.PlaybackEvent{
{MACode: "MA.156.8531.6101/WD/不存在", PlatformID: "P", EventType: model.EventPlay},
})
assert.Equal(t, 0, acc)
assert.Equal(t, 1, rej)
// 下架后回传被拒(数据归属不可信)
_, err := s.Takedown(chain.RoleRegulator, maCode, "违规")
require.NoError(t, err)
acc, rej = s.ReportPlayback([]model.PlaybackEvent{
{MACode: maCode, PlatformID: "P", EventType: model.EventPlay},
})
assert.Equal(t, 0, acc)
assert.Equal(t, 1, rej)
}