feat: Implement notification service and integrate with user interactions

- Added notification service to handle sending and listing notifications.
- Integrated notification sending on user follow and order payment events.
- Updated user service to include fetching followed tenants.
- Enhanced content service to manage access control for content assets.
- Implemented logic for listing content topics based on genre.
- Updated creator service to manage content updates and pricing.
- Improved order service to include detailed order information and notifications.
- Added tests for notification CRUD operations and order details.
This commit is contained in:
2025-12-30 09:57:12 +08:00
parent 9ef9642965
commit 5cf2295f91
14 changed files with 741 additions and 52 deletions

View File

@@ -42,7 +42,8 @@ func (s *order) ListUserOrders(ctx context.Context, status string) ([]user_dto.O
var data []user_dto.Order
for _, v := range list {
data = append(data, s.toUserOrderDTO(v))
dto, _ := s.composeOrderDTO(ctx, v)
data = append(data, dto)
}
return data, nil
}
@@ -64,7 +65,10 @@ func (s *order) GetUserOrder(ctx context.Context, id string) (*user_dto.Order, e
return nil, errorx.ErrDatabaseError.WithCause(err)
}
dto := s.toUserOrderDTO(item)
dto, err := s.composeOrderDTO(ctx, item)
if err != nil {
return nil, err
}
return &dto, nil
}
@@ -87,6 +91,8 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor
price, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First()
if err != nil {
// If price missing, treat as error? Or maybe 0?
// Better to require price record.
return nil, errorx.ErrDataCorrupted.WithCause(err).WithMsg("价格信息缺失")
}
@@ -96,12 +102,12 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor
UserID: uid,
Type: consts.OrderTypeContentPurchase,
Status: consts.OrderStatusCreated,
Currency: consts.Currency(price.Currency), // price.Currency is consts.Currency in DB? Yes.
Currency: consts.Currency(price.Currency),
AmountOriginal: price.PriceAmount,
AmountDiscount: 0, // Calculate discount if needed
AmountPaid: price.PriceAmount, // Expected to pay
IdempotencyKey: uuid.NewString(), // Should be from client ideally
Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Populate details
AmountDiscount: 0,
AmountPaid: price.PriceAmount,
IdempotencyKey: uuid.NewString(),
Snapshot: types.NewJSONType(fields.OrdersSnapshot{}),
}
if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil {
@@ -147,13 +153,13 @@ func (s *order) Pay(ctx context.Context, id string, form *transaction_dto.OrderP
return s.payWithBalance(ctx, o)
}
// External payment (mock)
return &transaction_dto.OrderPayResponse{
PayParams: "mock_pay_params",
}, nil
}
func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transaction_dto.OrderPayResponse, error) {
var tenantOwnerID int64
err := models.Q.Transaction(func(tx *models.Query) error {
// 1. Deduct User Balance
info, err := tx.User.WithContext(ctx).
@@ -179,6 +185,12 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
// 3. Grant Content Access
items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find()
for _, item := range items {
// Check if access already exists (idempotency)
exists, _ := tx.ContentAccess.WithContext(ctx).Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.Eq(item.ContentID)).Exists()
if exists {
continue
}
access := &models.ContentAccess{
TenantID: item.TenantID,
UserID: o.UserID,
@@ -196,6 +208,7 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
if err != nil {
return err
}
tenantOwnerID = t.UserID
ledger := &models.TenantLedger{
TenantID: o.TenantID,
@@ -224,16 +237,74 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
return nil, errorx.ErrDatabaseError.WithCause(err)
}
if Notification != nil {
_ = Notification.Send(ctx, o.UserID, "order", "支付成功", "订单已支付,您可以查看已购内容。")
if tenantOwnerID > 0 {
_ = Notification.Send(ctx, tenantOwnerID, "order", "新的订单", "您的店铺有新的订单,收入已入账。")
}
}
return &transaction_dto.OrderPayResponse{
PayParams: "balance_paid",
}, nil
}
func (s *order) Status(ctx context.Context, id string) (*transaction_dto.OrderStatusResponse, error) {
// ... check status ...
return nil, nil
}
func (s *order) composeOrderDTO(ctx context.Context, o *models.Order) (user_dto.Order, error) {
dto := user_dto.Order{
ID: cast.ToString(o.ID),
Status: string(o.Status),
Amount: float64(o.AmountPaid) / 100.0,
CreateTime: o.CreatedAt.Format(time.RFC3339),
TenantID: cast.ToString(o.TenantID),
}
// Fetch Tenant Name
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(o.TenantID)).First()
if err == nil {
dto.TenantName = t.Name
}
// Fetch Items
items, err := models.OrderItemQuery.WithContext(ctx).Where(models.OrderItemQuery.OrderID.Eq(o.ID)).Find()
if err == nil {
dto.Quantity = len(items)
for _, item := range items {
// Fetch Content
var c models.Content
err := models.ContentQuery.WithContext(ctx).
Where(models.ContentQuery.ID.Eq(item.ContentID)).
UnderlyingDB().
Preload("ContentAssets").
Preload("ContentAssets.Asset").
First(&c).Error
if err == nil {
ci := transaction_dto.ContentItem{
ID: cast.ToString(c.ID),
Title: c.Title,
Genre: c.Genre,
AuthorID: cast.ToString(c.UserID),
Price: float64(item.AmountPaid) / 100.0, // Use actual paid amount
}
// Cover logic (simplified from content service)
for _, asset := range c.ContentAssets {
if asset.Role == consts.ContentAssetRoleCover && asset.Asset != nil {
ci.Cover = Common.GetAssetURL(asset.Asset.ObjectKey)
break
}
}
dto.Items = append(dto.Items, ci)
}
}
}
return dto, nil
}
func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order {
return user_dto.Order{
ID: cast.ToString(o.ID),