355 lines
13 KiB
Go
355 lines
13 KiB
Go
package cprelation
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
cpListDefaultPageSize = 50
|
|
cpListMaxPageSize = 100
|
|
dayMS = int64(24 * 60 * 60 * 1000)
|
|
)
|
|
|
|
var (
|
|
relationshipStatuses = []string{"active", "ended"}
|
|
applicationStatuses = []string{"pending", "accepted", "rejected", "expired", "blocked"}
|
|
)
|
|
|
|
type cpListOptions struct {
|
|
Page int
|
|
PageSize int
|
|
Status string
|
|
RelationType string
|
|
}
|
|
|
|
type cpUserDTO struct {
|
|
UserID int64 `json:"userId,string"`
|
|
DisplayUserID string `json:"displayUserId"`
|
|
Username string `json:"username"`
|
|
Avatar string `json:"avatar"`
|
|
}
|
|
|
|
type cpGiftSnapshotDTO struct {
|
|
GiftID string `json:"giftId"`
|
|
GiftName string `json:"giftName"`
|
|
GiftIconURL string `json:"giftIconUrl"`
|
|
GiftAnimationURL string `json:"giftAnimationUrl"`
|
|
GiftCount int64 `json:"giftCount"`
|
|
GiftValue int64 `json:"giftValue"`
|
|
BillingReceiptID string `json:"billingReceiptId"`
|
|
}
|
|
|
|
type cpRelationshipDTO struct {
|
|
RelationshipID string `json:"relationshipId"`
|
|
RelationType string `json:"relationType"`
|
|
Status string `json:"status"`
|
|
UserA cpUserDTO `json:"userA"`
|
|
UserB cpUserDTO `json:"userB"`
|
|
IntimacyValue int64 `json:"intimacyValue"`
|
|
Level int32 `json:"level"`
|
|
DurationDays int64 `json:"durationDays"`
|
|
Gift *cpGiftSnapshotDTO `json:"gift,omitempty"`
|
|
FormedAtMS int64 `json:"formedAtMs"`
|
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
|
EndedAtMS int64 `json:"endedAtMs"`
|
|
}
|
|
|
|
type cpRelationshipPageDTO struct {
|
|
Items []cpRelationshipDTO `json:"items"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
Total int64 `json:"total"`
|
|
ServerTimeMS int64 `json:"serverTimeMs"`
|
|
}
|
|
|
|
type cpApplicationDTO struct {
|
|
ApplicationID string `json:"applicationId"`
|
|
RelationType string `json:"relationType"`
|
|
Status string `json:"status"`
|
|
Requester cpUserDTO `json:"requester"`
|
|
Target cpUserDTO `json:"target"`
|
|
Gift cpGiftSnapshotDTO `json:"gift"`
|
|
RoomID string `json:"roomId"`
|
|
RoomRegionID int64 `json:"roomRegionId"`
|
|
CreatedAtMS int64 `json:"createdAtMs"`
|
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
|
ExpiresAtMS int64 `json:"expiresAtMs"`
|
|
DecidedAtMS int64 `json:"decidedAtMs"`
|
|
}
|
|
|
|
type cpApplicationPageDTO struct {
|
|
Items []cpApplicationDTO `json:"items"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
Total int64 `json:"total"`
|
|
ServerTimeMS int64 `json:"serverTimeMs"`
|
|
}
|
|
|
|
// ListRelationships 给管理端按全站维度分页读取关系;这里不能复用 App 侧按单用户读取的仓储,否则会漏掉“所有用户关系”这个运营视角。
|
|
func (s *Service) ListRelationships(ctx context.Context, appCode string, options cpListOptions) (cpRelationshipPageDTO, error) {
|
|
options = normalizeCPListOptions(options)
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
whereSQL, args, err := relationshipListWhere(appCode, options)
|
|
if err != nil {
|
|
return cpRelationshipPageDTO{}, err
|
|
}
|
|
if s == nil || s.db == nil {
|
|
return cpRelationshipPageDTO{}, errorsUserDBNotConfigured()
|
|
}
|
|
total, err := s.countRows(ctx, "SELECT COUNT(*) FROM user_cp_relationships r WHERE "+whereSQL, args...)
|
|
if err != nil {
|
|
return cpRelationshipPageDTO{}, err
|
|
}
|
|
queryArgs := append(append([]any{}, args...), options.PageSize, cpListOffset(options))
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT
|
|
r.relationship_id, r.relation_type, r.status,
|
|
r.user_a_id, COALESCE(user_a.current_display_user_id, ''), COALESCE(user_a.username, ''), COALESCE(user_a.avatar, ''),
|
|
r.user_b_id, COALESCE(user_b.current_display_user_id, ''), COALESCE(user_b.username, ''), COALESCE(user_b.avatar, ''),
|
|
r.intimacy_value, r.level_no, r.formed_at_ms, r.updated_at_ms, r.ended_at_ms,
|
|
COALESCE(source.gift_id, ''), COALESCE(source.gift_name, ''), COALESCE(source.gift_icon_url, ''),
|
|
COALESCE(source.gift_animation_url, ''), COALESCE(source.gift_count, 0), COALESCE(source.gift_value, 0),
|
|
COALESCE(source.billing_receipt_id, '')
|
|
FROM user_cp_relationships r
|
|
LEFT JOIN users user_a ON user_a.app_code = r.app_code AND user_a.user_id = r.user_a_id
|
|
LEFT JOIN users user_b ON user_b.app_code = r.app_code AND user_b.user_id = r.user_b_id
|
|
LEFT JOIN user_cp_applications source ON source.app_code = r.app_code AND source.application_id = r.source_application_id
|
|
WHERE `+whereSQL+`
|
|
ORDER BY r.formed_at_ms DESC, r.relationship_id DESC
|
|
LIMIT ? OFFSET ?`, queryArgs...)
|
|
if err != nil {
|
|
return cpRelationshipPageDTO{}, err
|
|
}
|
|
defer rows.Close()
|
|
items, err := scanRelationshipRows(rows, nowMS)
|
|
if err != nil {
|
|
return cpRelationshipPageDTO{}, err
|
|
}
|
|
return cpRelationshipPageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total, ServerTimeMS: nowMS}, nil
|
|
}
|
|
|
|
// ListApplications 读取所有用户的申请记录;读取前先沿用 App 侧懒过期语义,避免后台把已过期申请继续展示为 pending。
|
|
func (s *Service) ListApplications(ctx context.Context, appCode string, options cpListOptions) (cpApplicationPageDTO, error) {
|
|
options = normalizeCPListOptions(options)
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
whereSQL, args, err := applicationListWhere(appCode, options)
|
|
if err != nil {
|
|
return cpApplicationPageDTO{}, err
|
|
}
|
|
if s == nil || s.db == nil {
|
|
return cpApplicationPageDTO{}, errorsUserDBNotConfigured()
|
|
}
|
|
if err := s.expirePendingApplications(ctx, appCode, nowMS); err != nil {
|
|
return cpApplicationPageDTO{}, err
|
|
}
|
|
total, err := s.countRows(ctx, "SELECT COUNT(*) FROM user_cp_applications a WHERE "+whereSQL, args...)
|
|
if err != nil {
|
|
return cpApplicationPageDTO{}, err
|
|
}
|
|
queryArgs := append(append([]any{}, args...), options.PageSize, cpListOffset(options))
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT
|
|
a.application_id, a.relation_type, a.status,
|
|
a.requester_user_id, COALESCE(requester.current_display_user_id, ''), COALESCE(requester.username, ''), COALESCE(requester.avatar, ''),
|
|
a.target_user_id, COALESCE(target.current_display_user_id, ''), COALESCE(target.username, ''), COALESCE(target.avatar, ''),
|
|
a.gift_id, a.gift_name, a.gift_icon_url, a.gift_animation_url, a.gift_count, a.gift_value, a.billing_receipt_id,
|
|
a.room_id, a.room_region_id, a.created_at_ms, a.updated_at_ms, a.expires_at_ms, a.decided_at_ms
|
|
FROM user_cp_applications a
|
|
LEFT JOIN users requester ON requester.app_code = a.app_code AND requester.user_id = a.requester_user_id
|
|
LEFT JOIN users target ON target.app_code = a.app_code AND target.user_id = a.target_user_id
|
|
WHERE `+whereSQL+`
|
|
ORDER BY a.created_at_ms DESC, a.application_id DESC
|
|
LIMIT ? OFFSET ?`, queryArgs...)
|
|
if err != nil {
|
|
return cpApplicationPageDTO{}, err
|
|
}
|
|
defer rows.Close()
|
|
items, err := scanApplicationRows(rows)
|
|
if err != nil {
|
|
return cpApplicationPageDTO{}, err
|
|
}
|
|
return cpApplicationPageDTO{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total, ServerTimeMS: nowMS}, nil
|
|
}
|
|
|
|
func (s *Service) expirePendingApplications(ctx context.Context, appCode string, nowMS int64) error {
|
|
_, err := s.db.ExecContext(ctx, `
|
|
UPDATE user_cp_applications
|
|
SET status = 'expired', updated_at_ms = ?
|
|
WHERE app_code = ? AND status = 'pending' AND expires_at_ms > 0 AND expires_at_ms <= ?`,
|
|
nowMS, appCode, nowMS,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func scanRelationshipRows(rows *sql.Rows, nowMS int64) ([]cpRelationshipDTO, error) {
|
|
items := []cpRelationshipDTO{}
|
|
for rows.Next() {
|
|
var item cpRelationshipDTO
|
|
var gift cpGiftSnapshotDTO
|
|
if err := rows.Scan(
|
|
&item.RelationshipID, &item.RelationType, &item.Status,
|
|
&item.UserA.UserID, &item.UserA.DisplayUserID, &item.UserA.Username, &item.UserA.Avatar,
|
|
&item.UserB.UserID, &item.UserB.DisplayUserID, &item.UserB.Username, &item.UserB.Avatar,
|
|
&item.IntimacyValue, &item.Level, &item.FormedAtMS, &item.UpdatedAtMS, &item.EndedAtMS,
|
|
&gift.GiftID, &gift.GiftName, &gift.GiftIconURL, &gift.GiftAnimationURL, &gift.GiftCount, &gift.GiftValue, &gift.BillingReceiptID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
item.UserA = normalizeCPUser(item.UserA)
|
|
item.UserB = normalizeCPUser(item.UserB)
|
|
item.DurationDays = relationshipDurationDays(item.FormedAtMS, item.EndedAtMS, nowMS)
|
|
if !gift.empty() {
|
|
item.Gift = &gift
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func scanApplicationRows(rows *sql.Rows) ([]cpApplicationDTO, error) {
|
|
items := []cpApplicationDTO{}
|
|
for rows.Next() {
|
|
var item cpApplicationDTO
|
|
if err := rows.Scan(
|
|
&item.ApplicationID, &item.RelationType, &item.Status,
|
|
&item.Requester.UserID, &item.Requester.DisplayUserID, &item.Requester.Username, &item.Requester.Avatar,
|
|
&item.Target.UserID, &item.Target.DisplayUserID, &item.Target.Username, &item.Target.Avatar,
|
|
&item.Gift.GiftID, &item.Gift.GiftName, &item.Gift.GiftIconURL, &item.Gift.GiftAnimationURL, &item.Gift.GiftCount, &item.Gift.GiftValue, &item.Gift.BillingReceiptID,
|
|
&item.RoomID, &item.RoomRegionID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.ExpiresAtMS, &item.DecidedAtMS,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
item.Requester = normalizeCPUser(item.Requester)
|
|
item.Target = normalizeCPUser(item.Target)
|
|
items = append(items, item)
|
|
}
|
|
return items, rows.Err()
|
|
}
|
|
|
|
func relationshipListWhere(appCode string, options cpListOptions) (string, []any, error) {
|
|
where := []string{"r.app_code = ?"}
|
|
args := []any{appCode}
|
|
if err := appendRelationFilter(&where, &args, "r", options.RelationType); err != nil {
|
|
return "", nil, err
|
|
}
|
|
if err := appendStatusFilter(&where, &args, "r", options.Status, relationshipStatuses, "关系状态不正确"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
return strings.Join(where, " AND "), args, nil
|
|
}
|
|
|
|
func applicationListWhere(appCode string, options cpListOptions) (string, []any, error) {
|
|
where := []string{"a.app_code = ?"}
|
|
args := []any{appCode}
|
|
if err := appendRelationFilter(&where, &args, "a", options.RelationType); err != nil {
|
|
return "", nil, err
|
|
}
|
|
if err := appendStatusFilter(&where, &args, "a", options.Status, applicationStatuses, "申请状态不正确"); err != nil {
|
|
return "", nil, err
|
|
}
|
|
return strings.Join(where, " AND "), args, nil
|
|
}
|
|
|
|
func appendRelationFilter(where *[]string, args *[]any, alias string, value string) error {
|
|
relationType := normalizeCPFilterValue(value)
|
|
if relationType == "" {
|
|
return nil
|
|
}
|
|
if !slices.Contains(relationTypes, relationType) {
|
|
return fmt.Errorf("%w: 关系类型不正确", ErrInvalidArgument)
|
|
}
|
|
*where = append(*where, alias+".relation_type = ?")
|
|
*args = append(*args, relationType)
|
|
return nil
|
|
}
|
|
|
|
func appendStatusFilter(where *[]string, args *[]any, alias string, value string, allowed []string, message string) error {
|
|
status := normalizeCPFilterValue(value)
|
|
if status == "" {
|
|
return nil
|
|
}
|
|
if !slices.Contains(allowed, status) {
|
|
return fmt.Errorf("%w: %s", ErrInvalidArgument, message)
|
|
}
|
|
*where = append(*where, alias+".status = ?")
|
|
*args = append(*args, status)
|
|
return nil
|
|
}
|
|
|
|
func normalizeCPListOptions(options cpListOptions) cpListOptions {
|
|
if options.Page < 1 {
|
|
options.Page = 1
|
|
}
|
|
if options.PageSize < 1 {
|
|
options.PageSize = cpListDefaultPageSize
|
|
}
|
|
if options.PageSize > cpListMaxPageSize {
|
|
options.PageSize = cpListMaxPageSize
|
|
}
|
|
options.Status = normalizeCPFilterValue(options.Status)
|
|
options.RelationType = normalizeCPFilterValue(options.RelationType)
|
|
return options
|
|
}
|
|
|
|
func normalizeCPFilterValue(value string) string {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
if value == "all" {
|
|
return ""
|
|
}
|
|
return value
|
|
}
|
|
|
|
func cpListOffset(options cpListOptions) int {
|
|
return (options.Page - 1) * options.PageSize
|
|
}
|
|
|
|
func (s *Service) countRows(ctx context.Context, query string, args ...any) (int64, error) {
|
|
var total int64
|
|
if err := s.db.QueryRowContext(ctx, query, args...).Scan(&total); err != nil {
|
|
return 0, err
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func normalizeCPUser(user cpUserDTO) cpUserDTO {
|
|
if strings.TrimSpace(user.DisplayUserID) == "" && user.UserID > 0 {
|
|
user.DisplayUserID = strconv.FormatInt(user.UserID, 10)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func relationshipDurationDays(formedAtMS int64, endedAtMS int64, nowMS int64) int64 {
|
|
if formedAtMS <= 0 {
|
|
return 0
|
|
}
|
|
endAtMS := nowMS
|
|
if endedAtMS > 0 {
|
|
endAtMS = endedAtMS
|
|
}
|
|
if endAtMS < formedAtMS {
|
|
return 0
|
|
}
|
|
// 运营侧“天数”按自然关系天展示:刚建立也算第 1 天,避免表格出现 0 天这种难以理解的状态。
|
|
return ((endAtMS - formedAtMS) / dayMS) + 1
|
|
}
|
|
|
|
func (gift cpGiftSnapshotDTO) empty() bool {
|
|
return strings.TrimSpace(gift.GiftID) == "" &&
|
|
strings.TrimSpace(gift.GiftName) == "" &&
|
|
strings.TrimSpace(gift.GiftIconURL) == "" &&
|
|
strings.TrimSpace(gift.GiftAnimationURL) == "" &&
|
|
strings.TrimSpace(gift.BillingReceiptID) == "" &&
|
|
gift.GiftCount == 0 &&
|
|
gift.GiftValue == 0
|
|
}
|
|
|
|
func errorsUserDBNotConfigured() error {
|
|
return fmt.Errorf("user db is not configured")
|
|
}
|