feat: add token store p0 backend flow

This commit is contained in:
Losita
2026-05-04 21:49:29 +08:00
parent 46874f0806
commit 4fc6c0cac3
18 changed files with 1921 additions and 97 deletions

View File

@@ -0,0 +1,84 @@
package sv
import (
"context"
"strings"
"github.com/LoveLosita/smartflow/backend/respond"
tokenstoredao "github.com/LoveLosita/smartflow/backend/services/tokenstore/dao"
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
)
// GetSummary 聚合当前用户在 token-store 账本中的获得记录。
//
// 职责边界:
// 1. 只统计 token_grants不读取 user/auth 的权威额度。
// 2. 只汇总正向获取额度,避免把未来的冲正或补偿误算进 P0 展示口径。
// 3. quota_sync_status 在 P0 固定为 not_connected明确告知尚未打通权威额度。
func (s *Service) GetSummary(ctx context.Context, actorUserID uint64) (*tokencontracts.TokenSummary, error) {
if err := s.Ready(); err != nil {
return nil, err
}
if actorUserID == 0 {
return nil, respond.MissingParam
}
summary, err := s.tokenDAO.SummarizePositiveGrants(ctx, actorUserID)
if err != nil {
return nil, err
}
pending := summary.RecordedTokenTotal - summary.AppliedTokenTotal
if pending < 0 {
pending = 0
}
return &tokencontracts.TokenSummary{
RecordedTokenTotal: summary.RecordedTokenTotal,
AppliedTokenTotal: summary.AppliedTokenTotal,
PendingApplyTokenTotal: pending,
QuotaSyncStatus: tokenSummaryQuotaStatusNotConnected,
Tip: tokenSummaryTipP0,
}, nil
}
// ListGrants 按用户分页查询 Token 获得记录。
//
// 职责边界:
// 1. 只支持 user_id 维度分页和 source 过滤,不做跨用户检索。
// 2. 负责把空 source 归一化为“不筛选”,避免 DAO 层重复处理入口噪音。
// 3. 结果只来自账本事实,不推导 user/auth 可用额度。
func (s *Service) ListGrants(ctx context.Context, req tokencontracts.ListTokenGrantsRequest) ([]tokencontracts.TokenGrantView, tokencontracts.PageResult, error) {
if err := s.Ready(); err != nil {
return nil, tokencontracts.PageResult{}, err
}
if req.ActorUserID == 0 {
return nil, tokencontracts.PageResult{}, respond.MissingParam
}
page, pageSize := normalizePage(req.Page, req.PageSize)
query := tokenstoredao.ListTokenGrantsQuery{
UserID: req.ActorUserID,
Page: page,
PageSize: pageSize,
Source: strings.TrimSpace(req.Source),
}
total, err := s.tokenDAO.CountGrants(ctx, query)
if err != nil {
return nil, tokencontracts.PageResult{}, err
}
grants, err := s.tokenDAO.ListGrants(ctx, query)
if err != nil {
return nil, tokencontracts.PageResult{}, err
}
if len(grants) == 0 {
return []tokencontracts.TokenGrantView{}, pageResult(page, pageSize, total), nil
}
result := make([]tokencontracts.TokenGrantView, 0, len(grants))
for _, grant := range grants {
result = append(result, grantViewFromModel(grant))
}
return result, pageResult(page, pageSize, total), nil
}

View File

@@ -0,0 +1,238 @@
package sv
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/respond"
tokenmodel "github.com/LoveLosita/smartflow/backend/services/tokenstore/model"
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
"github.com/google/uuid"
"gorm.io/gorm"
)
const (
defaultPage = 1
defaultPageSize = 20
maxPageSize = 50
tokenSummaryQuotaStatusNotConnected = "not_connected"
tokenSummaryTipP0 = "当前仅统计 Token 商店已记录的获得记录,尚未同步到 user/auth 可用额度。"
)
type productSnapshot struct {
ProductID uint64 `json:"product_id"`
SKU string `json:"sku"`
Name string `json:"name"`
Description string `json:"description"`
TokenAmount int64 `json:"token_amount"`
PriceCent int64 `json:"price_cent"`
PriceText string `json:"price_text"`
Currency string `json:"currency"`
Badge string `json:"badge"`
Status string `json:"status"`
SortOrder int `json:"sort_order"`
}
func normalizePage(page int, pageSize int) (int, int) {
if page <= 0 {
page = defaultPage
}
if pageSize <= 0 {
pageSize = defaultPageSize
}
if pageSize > maxPageSize {
pageSize = maxPageSize
}
return page, pageSize
}
func pageResult(page int, pageSize int, total int64) tokencontracts.PageResult {
return tokencontracts.PageResult{
Page: page,
PageSize: pageSize,
Total: int(total),
HasMore: int64(page*pageSize) < total,
}
}
func formatTime(value time.Time) string {
if value.IsZero() {
return ""
}
return value.Format(time.RFC3339)
}
func formatTimePtr(value *time.Time) *string {
if value == nil || value.IsZero() {
return nil
}
formatted := value.Format(time.RFC3339)
return &formatted
}
func formatPriceText(currency string, amountCent int64) string {
if strings.EqualFold(strings.TrimSpace(currency), "CNY") {
return fmt.Sprintf("¥%.2f", float64(amountCent)/100)
}
return fmt.Sprintf("%s %.2f", strings.ToUpper(strings.TrimSpace(currency)), float64(amountCent)/100)
}
func stringPtrFromNonEmpty(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
func productViewFromModel(product tokenmodel.TokenProduct) tokencontracts.TokenProductView {
return tokencontracts.TokenProductView{
ProductID: product.ID,
Name: product.Name,
Description: product.Description,
TokenAmount: product.TokenAmount,
PriceCent: product.PriceCent,
PriceText: formatPriceText(product.Currency, product.PriceCent),
Currency: product.Currency,
Badge: product.Badge,
Status: product.Status,
SortOrder: product.SortOrder,
}
}
func grantViewFromModel(grant tokenmodel.TokenGrant) tokencontracts.TokenGrantView {
return tokencontracts.TokenGrantView{
GrantID: grant.ID,
EventID: grant.EventID,
Source: grant.Source,
SourceLabel: grantSourceLabel(grant.Source, grant.SourceLabel),
Amount: grant.Amount,
Status: grant.Status,
QuotaApplied: grant.QuotaApplied,
Description: grant.Description,
CreatedAt: formatTime(grant.CreatedAt),
}
}
func orderViewFromModel(order tokenmodel.TokenOrder, grant *tokenmodel.TokenGrant) tokencontracts.TokenOrderView {
var grantView *tokencontracts.TokenGrantView
if grant != nil {
view := grantViewFromModel(*grant)
grantView = &view
}
return tokencontracts.TokenOrderView{
OrderID: order.ID,
OrderNo: order.OrderNo,
Status: order.Status,
ProductSnapshot: order.ProductSnapshotJSON,
ProductName: order.ProductName,
Quantity: order.Quantity,
TokenAmount: order.TokenAmount,
AmountCent: order.AmountCent,
PriceText: formatPriceText(order.Currency, order.AmountCent),
Currency: order.Currency,
PaymentMode: order.PaymentMode,
Grant: grantView,
CreatedAt: formatTime(order.CreatedAt),
PaidAt: formatTimePtr(order.PaidAt),
GrantedAt: formatTimePtr(order.GrantedAt),
}
}
func grantSourceLabel(source string, fallback string) string {
if strings.TrimSpace(fallback) != "" {
return fallback
}
switch strings.TrimSpace(source) {
case tokenmodel.TokenGrantSourcePurchase:
return "购买充值"
case tokenmodel.TokenGrantSourceForumLike:
return "计划被点赞"
case tokenmodel.TokenGrantSourceForumImport:
return "计划被导入"
case tokenmodel.TokenGrantSourceManual:
return "人工补发"
default:
return "Token 获得记录"
}
}
func buildProductSnapshot(product tokenmodel.TokenProduct) (string, error) {
snapshot := productSnapshot{
ProductID: product.ID,
SKU: product.SKU,
Name: product.Name,
Description: product.Description,
TokenAmount: product.TokenAmount,
PriceCent: product.PriceCent,
PriceText: formatPriceText(product.Currency, product.PriceCent),
Currency: product.Currency,
Badge: product.Badge,
Status: product.Status,
SortOrder: product.SortOrder,
}
raw, err := json.Marshal(snapshot)
if err != nil {
return "", err
}
return string(raw), nil
}
func newOrderNo() string {
return fmt.Sprintf(
"TS%s%s",
time.Now().Format("20060102150405"),
strings.ReplaceAll(uuid.NewString(), "-", ""),
)
}
func purchaseGrantEventID(orderID uint64) string {
return fmt.Sprintf("order:%d:paid", orderID)
}
func purchaseGrantDescription(productName string) string {
trimmed := strings.TrimSpace(productName)
if trimmed == "" {
return "购买 Token 商品"
}
return fmt.Sprintf("购买%s", trimmed)
}
func isDuplicateKeyError(err error) bool {
if err == nil {
return false
}
lower := strings.ToLower(err.Error())
return strings.Contains(lower, "duplicate entry") ||
strings.Contains(lower, "duplicate key") ||
strings.Contains(lower, "unique constraint") ||
strings.Contains(lower, "unique violation") ||
strings.Contains(lower, "error 1062")
}
func normalizeRecordNotFound(err error, fallback error) error {
if errorsIsRecordNotFound(err) {
return fallback
}
return err
}
func errorsIsRecordNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound)
}
// tokenStoreBadRequestStatus 是 token-store P0 统一业务校验错误码。
// 具体错误原因仍放在 Info避免为每个商品/订单校验分支提前扩散大量细分码。
const tokenStoreBadRequestStatus = "40067"
func tokenStoreBadRequest(message string) respond.Response {
return respond.Response{
Status: tokenStoreBadRequestStatus,
Info: strings.TrimSpace(message),
}
}

View File

@@ -0,0 +1,312 @@
package sv
import (
"context"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/respond"
tokenstoredao "github.com/LoveLosita/smartflow/backend/services/tokenstore/dao"
tokenmodel "github.com/LoveLosita/smartflow/backend/services/tokenstore/model"
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
)
// CreateOrder 创建 Token 商品订单。
//
// 职责边界:
// 1. 校验 actor_user_id、product_id、quantity 与幂等键。
// 2. 只生成 pending 订单和商品快照,不触发真实支付或 user/auth 同步。
// 3. 并发冲突时优先按 user_id + idempotency_key 回查旧单,保证 P0 幂等语义。
func (s *Service) CreateOrder(ctx context.Context, req tokencontracts.CreateTokenOrderRequest) (*tokencontracts.TokenOrderView, error) {
if err := s.Ready(); err != nil {
return nil, err
}
if req.ActorUserID == 0 || req.ProductID == 0 {
return nil, respond.MissingParam
}
if req.Quantity < 1 || req.Quantity > 99 {
return nil, tokenStoreBadRequest("quantity 仅支持 1 到 99")
}
idempotencyKey := strings.TrimSpace(req.IdempotencyKey)
if idempotencyKey != "" {
existing, err := s.tokenDAO.FindOrderByUserIdempotencyKey(ctx, req.ActorUserID, idempotencyKey)
if err != nil {
return nil, err
}
if existing != nil {
return s.orderViewByID(ctx, req.ActorUserID, existing.ID)
}
}
product, err := s.tokenDAO.FindActiveProductByID(ctx, req.ProductID)
if err != nil {
return nil, err
}
if product == nil {
return nil, tokenStoreBadRequest("商品不存在或已下架")
}
productSnapshot, err := buildProductSnapshot(*product)
if err != nil {
return nil, err
}
order := tokenmodel.TokenOrder{
OrderNo: newOrderNo(),
UserID: req.ActorUserID,
ProductID: product.ID,
ProductSKU: product.SKU,
ProductName: product.Name,
ProductSnapshotJSON: productSnapshot,
Quantity: req.Quantity,
TokenAmount: product.TokenAmount * int64(req.Quantity),
AmountCent: product.PriceCent * int64(req.Quantity),
Currency: product.Currency,
Status: tokenmodel.TokenOrderStatusPending,
PaymentMode: "mock",
IdempotencyKey: stringPtrFromNonEmpty(idempotencyKey),
}
if err := s.tokenDAO.CreateOrder(ctx, &order); err != nil {
if idempotencyKey != "" && isDuplicateKeyError(err) {
existing, findErr := s.tokenDAO.FindOrderByUserIdempotencyKey(ctx, req.ActorUserID, idempotencyKey)
if findErr != nil {
return nil, findErr
}
if existing != nil {
return s.orderViewByID(ctx, req.ActorUserID, existing.ID)
}
}
return nil, err
}
return s.orderViewByID(ctx, req.ActorUserID, order.ID)
}
// ListOrders 按用户分页查询订单列表。
//
// 职责边界:
// 1. 只支持当前用户维度分页,不做跨用户检索。
// 2. status 为空时不过滤,非空时按精确值过滤。
// 3. 负责把订单与 grant 账本拼装成统一视图。
func (s *Service) ListOrders(ctx context.Context, req tokencontracts.ListTokenOrdersRequest) ([]tokencontracts.TokenOrderView, tokencontracts.PageResult, error) {
if err := s.Ready(); err != nil {
return nil, tokencontracts.PageResult{}, err
}
if req.ActorUserID == 0 {
return nil, tokencontracts.PageResult{}, respond.MissingParam
}
page, pageSize := normalizePage(req.Page, req.PageSize)
query := tokenstoredao.ListTokenOrdersQuery{
UserID: req.ActorUserID,
Page: page,
PageSize: pageSize,
Status: strings.TrimSpace(req.Status),
}
total, err := s.tokenDAO.CountOrders(ctx, query)
if err != nil {
return nil, tokencontracts.PageResult{}, err
}
orders, err := s.tokenDAO.ListOrders(ctx, query)
if err != nil {
return nil, tokencontracts.PageResult{}, err
}
if len(orders) == 0 {
return []tokencontracts.TokenOrderView{}, pageResult(page, pageSize, total), nil
}
grantMap, err := s.orderGrantMap(ctx, collectOrderIDs(orders))
if err != nil {
return nil, tokencontracts.PageResult{}, err
}
result := make([]tokencontracts.TokenOrderView, 0, len(orders))
for _, order := range orders {
result = append(result, orderViewFromModel(order, grantMap[order.ID]))
}
return result, pageResult(page, pageSize, total), nil
}
// GetOrder 查询单个订单详情,并校验归属用户。
func (s *Service) GetOrder(ctx context.Context, actorUserID uint64, orderID uint64) (*tokencontracts.TokenOrderView, error) {
if err := s.Ready(); err != nil {
return nil, err
}
if actorUserID == 0 || orderID == 0 {
return nil, respond.MissingParam
}
return s.orderViewByID(ctx, actorUserID, orderID)
}
// MockPaidOrder 在同步事务里完成 mock paid 和 grant 入账。
//
// 职责边界:
// 1. 只处理订单状态流转与 token_grants 幂等写入,不调用 user/auth。
// 2. event_id 固定为 order:{order_id}:paid作为最终 grant 幂等边界。
// 3. 重复调用优先复用既有 grant再把订单补齐到 granted避免重复写账本。
func (s *Service) MockPaidOrder(ctx context.Context, req tokencontracts.MockPaidOrderRequest) (*tokencontracts.TokenOrderView, error) {
if err := s.Ready(); err != nil {
return nil, err
}
if req.ActorUserID == 0 || req.OrderID == 0 {
return nil, respond.MissingParam
}
var resultOrder tokenmodel.TokenOrder
var resultGrant *tokenmodel.TokenGrant
err := s.tokenDAO.Transaction(ctx, func(txDAO *tokenstoredao.TokenStoreDAO) error {
now := time.Now()
// 1. 先锁订单并校验归属,避免并发 mock paid 重复写 grant。
// 2. 订单不存在直接返回;订单不属于当前用户时明确拒绝。
// 3. closed 状态不允许继续支付,避免把关闭单重新拉回可用态。
order, err := txDAO.LockOrderByID(ctx, req.OrderID)
if err != nil {
return normalizeRecordNotFound(err, tokenStoreBadRequest("订单不存在"))
}
if order.UserID != req.ActorUserID {
return tokenStoreBadRequest("订单不属于当前用户")
}
switch order.Status {
case tokenmodel.TokenOrderStatusPending, tokenmodel.TokenOrderStatusPaid, tokenmodel.TokenOrderStatusGranted:
case tokenmodel.TokenOrderStatusClosed:
return tokenStoreBadRequest("订单已关闭,不能执行 mock paid")
default:
return tokenStoreBadRequest("订单状态不支持执行 mock paid")
}
eventID := purchaseGrantEventID(order.ID)
grant, err := txDAO.FindGrantByEventID(ctx, eventID)
if err != nil {
return err
}
// 1. grant 不存在时才尝试创建,保证账本幂等写入边界只在 event_id。
// 2. 即使因为历史脏数据或极端并发触发唯一冲突,也要立刻按 event_id 反查旧 grant。
// 3. 这里不写 user/auth只把 token-store 自己的账本事实补齐。
if grant == nil {
sourceRefID := order.ID
orderID := order.ID
newGrant := &tokenmodel.TokenGrant{
EventID: eventID,
UserID: order.UserID,
Source: tokenmodel.TokenGrantSourcePurchase,
SourceLabel: grantSourceLabel(tokenmodel.TokenGrantSourcePurchase, ""),
SourceRefID: &sourceRefID,
OrderID: &orderID,
Amount: order.TokenAmount,
Status: tokenmodel.TokenGrantStatusRecorded,
QuotaApplied: false,
Description: purchaseGrantDescription(order.ProductName),
}
if err := txDAO.CreateGrant(ctx, newGrant); err != nil {
if !isDuplicateKeyError(err) {
return err
}
newGrant, err = txDAO.FindGrantByEventID(ctx, eventID)
if err != nil {
return err
}
if newGrant == nil {
return tokenStoreBadRequest("Token 发放记录创建后未找到")
}
}
grant = newGrant
}
// 1. 无论订单原来是 pending、paid 还是 granted只要 grant 已确定,就把订单补齐到 granted。
// 2. paid_at 缺失时使用本次确认时间granted_at 缺失时优先复用 grant.created_at保证链路时间可追溯。
// 3. 这样即便出现“grant 已有、订单未完成切流”的历史半状态,也能在重复调用时自愈。
paidAt := order.PaidAt
if paidAt == nil || paidAt.IsZero() {
paidAt = &now
}
grantedAt := order.GrantedAt
if grantedAt == nil || grantedAt.IsZero() {
if grant != nil && !grant.CreatedAt.IsZero() {
grantCreatedAt := grant.CreatedAt
grantedAt = &grantCreatedAt
} else {
grantedAt = &now
}
}
paymentMode := strings.TrimSpace(order.PaymentMode)
if paymentMode == "" {
paymentMode = strings.TrimSpace(req.MockChannel)
}
if paymentMode == "" {
paymentMode = "mock"
}
if err := txDAO.UpdateOrderState(ctx, order.ID, tokenmodel.TokenOrderStatusGranted, paidAt, grantedAt, paymentMode); err != nil {
return err
}
order.Status = tokenmodel.TokenOrderStatusGranted
order.PaidAt = paidAt
order.GrantedAt = grantedAt
order.PaymentMode = paymentMode
resultOrder = *order
resultGrant = grant
return nil
})
if err != nil {
return nil, err
}
view := orderViewFromModel(resultOrder, resultGrant)
return &view, nil
}
func (s *Service) orderViewByID(ctx context.Context, actorUserID uint64, orderID uint64) (*tokencontracts.TokenOrderView, error) {
order, err := s.tokenDAO.FindOrderByID(ctx, orderID)
if err != nil {
return nil, err
}
if order == nil {
return nil, tokenStoreBadRequest("订单不存在")
}
if order.UserID != actorUserID {
return nil, tokenStoreBadRequest("订单不属于当前用户")
}
grant, err := s.tokenDAO.FindGrantByOrderID(ctx, order.ID)
if err != nil {
return nil, err
}
view := orderViewFromModel(*order, grant)
return &view, nil
}
func (s *Service) orderGrantMap(ctx context.Context, orderIDs []uint64) (map[uint64]*tokenmodel.TokenGrant, error) {
result := make(map[uint64]*tokenmodel.TokenGrant, len(orderIDs))
if len(orderIDs) == 0 {
return result, nil
}
grants, err := s.tokenDAO.ListGrantsByOrderIDs(ctx, orderIDs)
if err != nil {
return nil, err
}
for i := range grants {
grant := grants[i]
if grant.OrderID == nil {
continue
}
if _, exists := result[*grant.OrderID]; exists {
continue
}
grantCopy := grant
result[*grant.OrderID] = &grantCopy
}
return result, nil
}
func collectOrderIDs(orders []tokenmodel.TokenOrder) []uint64 {
result := make([]uint64, 0, len(orders))
for _, order := range orders {
result = append(result, order.ID)
}
return result
}

View File

@@ -0,0 +1,34 @@
package sv
import (
"context"
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
)
// ListProducts 返回当前可售商品列表。
//
// 职责边界:
// 1. 只返回 active 商品,不负责后台商品管理。
// 2. 负责补齐 price_text保持前端不必重复格式化价格。
// 3. actorUserID 当前仅保留为统一接口形状P0 不参与筛选。
func (s *Service) ListProducts(ctx context.Context, actorUserID uint64) ([]tokencontracts.TokenProductView, error) {
_ = actorUserID
if err := s.Ready(); err != nil {
return nil, err
}
products, err := s.tokenDAO.ListActiveProducts(ctx)
if err != nil {
return nil, err
}
if len(products) == 0 {
return []tokencontracts.TokenProductView{}, nil
}
result := make([]tokencontracts.TokenProductView, 0, len(products))
for _, product := range products {
result = append(result, productViewFromModel(product))
}
return result, nil
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
tokenstoredao "github.com/LoveLosita/smartflow/backend/services/tokenstore/dao"
tokencontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/tokenstore"
"gorm.io/gorm"
)
@@ -35,12 +36,14 @@ type Options struct {
// 3. 不负责真实第三方支付回调P0 只处理 mock paid。
type Service struct {
db *gorm.DB
tokenDAO *tokenstoredao.TokenStoreDAO
grantOutlet TokenGrantOutlet
}
func New(opts Options) *Service {
return &Service{
db: opts.DB,
tokenDAO: tokenstoredao.NewTokenStoreDAO(opts.DB),
grantOutlet: opts.GrantOutlet,
}
}
@@ -55,53 +58,3 @@ func (s *Service) Ready() error {
}
return nil
}
// ListProducts 是商品列表用例占位,第四步实现真实查询。
func (s *Service) ListProducts(ctx context.Context, actorUserID uint64) ([]tokencontracts.TokenProductView, error) {
_ = ctx
_ = actorUserID
return nil, ErrNotImplemented
}
// GetSummary 是 Token 概览用例占位,第四步实现 grant 账本聚合。
func (s *Service) GetSummary(ctx context.Context, actorUserID uint64) (*tokencontracts.TokenSummary, error) {
_ = ctx
_ = actorUserID
return nil, ErrNotImplemented
}
// CreateOrder 是创建订单用例占位,第四步实现商品读取、订单幂等和金额快照。
func (s *Service) CreateOrder(ctx context.Context, req tokencontracts.CreateTokenOrderRequest) (*tokencontracts.TokenOrderView, error) {
_ = ctx
_ = req
return nil, ErrNotImplemented
}
// ListOrders 是订单列表用例占位,第四步实现用户维度分页查询。
func (s *Service) ListOrders(ctx context.Context, req tokencontracts.ListTokenOrdersRequest) ([]tokencontracts.TokenOrderView, tokencontracts.PageResult, error) {
_ = ctx
_ = req
return nil, tokencontracts.PageResult{}, ErrNotImplemented
}
// GetOrder 是订单详情用例占位,第四步实现订单归属校验。
func (s *Service) GetOrder(ctx context.Context, actorUserID uint64, orderID uint64) (*tokencontracts.TokenOrderView, error) {
_ = ctx
_ = actorUserID
_ = orderID
return nil, ErrNotImplemented
}
// MockPaidOrder 是 P0 mock paid 用例占位,第四步实现支付态流转和 grant 账本。
func (s *Service) MockPaidOrder(ctx context.Context, req tokencontracts.MockPaidOrderRequest) (*tokencontracts.TokenOrderView, error) {
_ = ctx
_ = req
return nil, ErrNotImplemented
}
// ListGrants 是 Token 获取记录用例占位,第四步实现账本分页查询。
func (s *Service) ListGrants(ctx context.Context, req tokencontracts.ListTokenGrantsRequest) ([]tokencontracts.TokenGrantView, tokencontracts.PageResult, error) {
_ = ctx
_ = req
return nil, tokencontracts.PageResult{}, ErrNotImplemented
}