商城c端前端
This commit is contained in:
@@ -23,6 +23,8 @@ type Repository interface {
|
||||
ActiveMemberLevels(context.Context, int64) ([]store.MemberLevel, error)
|
||||
ActiveMemberNotices(context.Context, int64, int64, int) ([]store.MemberNotice, error)
|
||||
MemberTaskSummary(context.Context, int64, int64) (*store.TaskSummary, error)
|
||||
TaskLeaderboard(context.Context, int64, int) ([]store.LeaderboardMember, error)
|
||||
ActiveCustomerServices(context.Context, int64) ([]store.CustomerService, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
@@ -95,6 +97,26 @@ type MemberLevel struct {
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
||||
type LeaderboardEntry struct {
|
||||
Rank int `json:"rank"`
|
||||
Username string `json:"username"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
CompletedCount int64 `json:"completed_count"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type CustomerService struct {
|
||||
ID int64 `json:"id"`
|
||||
ServiceCode string `json:"service_code"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
WorkTime string `json:"work_time"`
|
||||
OnlineStatus string `json:"online_status"`
|
||||
ServiceURL string `json:"service_url"`
|
||||
BackupContact string `json:"backup_contact"`
|
||||
PrivacyNotice string `json:"privacy_notice"`
|
||||
}
|
||||
|
||||
func New(repository Repository) *Service {
|
||||
return &Service{repository: repository}
|
||||
}
|
||||
@@ -183,6 +205,51 @@ func (s *Service) MemberLevels(ctx context.Context, tenantID int64) ([]MemberLev
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) Leaderboard(ctx context.Context, tenantID int64) ([]LeaderboardEntry, error) {
|
||||
rows, err := s.repository.TaskLeaderboard(ctx, tenantID, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]LeaderboardEntry, 0, len(rows))
|
||||
for index, row := range rows {
|
||||
items = append(items, LeaderboardEntry{Rank: index + 1, Username: maskedUsername(row.Username),
|
||||
AvatarURL: row.AvatarURL, CompletedCount: row.CompletedCount, Status: "领取成功"})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) CustomerServices(ctx context.Context, tenantID int64) ([]CustomerService, error) {
|
||||
rows, err := s.repository.ActiveCustomerServices(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]CustomerService, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, CustomerService{
|
||||
ID: row.ID, ServiceCode: row.ServiceCode, Name: row.Name,
|
||||
AvatarURL: safeExternalURL(row.AvatarURL), WorkTime: row.WorkTime,
|
||||
OnlineStatus: customerServiceStatus(row.OnlineStatus), ServiceURL: safeExternalURL(row.ServiceURL),
|
||||
BackupContact: row.BackupContact,
|
||||
PrivacyNotice: "请勿在客服会话中发送登录密码、支付密码或短信验证码",
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func maskedUsername(value string) string {
|
||||
runes := []rune(strings.TrimSpace(value))
|
||||
switch len(runes) {
|
||||
case 0:
|
||||
return "会员"
|
||||
case 1:
|
||||
return string(runes[0]) + "*"
|
||||
case 2:
|
||||
return string(runes[0]) + "*"
|
||||
default:
|
||||
return string(runes[0]) + "***" + string(runes[len(runes)-1])
|
||||
}
|
||||
}
|
||||
|
||||
func parseFeatures(raw json.RawMessage) Features {
|
||||
var config map[string]any
|
||||
if json.Unmarshal(raw, &config) != nil {
|
||||
@@ -241,6 +308,29 @@ func bannerTarget(kind int, value string) (string, string) {
|
||||
return "none", ""
|
||||
}
|
||||
|
||||
func safeExternalURL(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil {
|
||||
return ""
|
||||
}
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func customerServiceStatus(status int) string {
|
||||
switch status {
|
||||
case 1:
|
||||
return "online"
|
||||
case 3:
|
||||
return "busy"
|
||||
default:
|
||||
return "offline"
|
||||
}
|
||||
}
|
||||
|
||||
func noticeSourceName(source int) string {
|
||||
if source == 2 {
|
||||
return "operational_simulation"
|
||||
|
||||
@@ -13,12 +13,14 @@ import (
|
||||
)
|
||||
|
||||
type fakeRepository struct {
|
||||
config json.RawMessage
|
||||
banners []store.Banner
|
||||
content *store.Content
|
||||
levels []store.MemberLevel
|
||||
notices []store.MemberNotice
|
||||
summary *store.TaskSummary
|
||||
config json.RawMessage
|
||||
banners []store.Banner
|
||||
content *store.Content
|
||||
levels []store.MemberLevel
|
||||
notices []store.MemberNotice
|
||||
summary *store.TaskSummary
|
||||
leaders []store.LeaderboardMember
|
||||
services []store.CustomerService
|
||||
}
|
||||
|
||||
func (r fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
|
||||
@@ -42,6 +44,12 @@ func (r fakeRepository) ActiveMemberNotices(context.Context, int64, int64, int)
|
||||
func (r fakeRepository) MemberTaskSummary(context.Context, int64, int64) (*store.TaskSummary, error) {
|
||||
return r.summary, nil
|
||||
}
|
||||
func (r fakeRepository) TaskLeaderboard(context.Context, int64, int) ([]store.LeaderboardMember, error) {
|
||||
return r.leaders, nil
|
||||
}
|
||||
func (r fakeRepository) ActiveCustomerServices(context.Context, int64) ([]store.CustomerService, error) {
|
||||
return r.services, nil
|
||||
}
|
||||
|
||||
func TestParseFeaturesSupportsNestedAndLegacyKeys(t *testing.T) {
|
||||
features := parseFeatures(json.RawMessage(`{
|
||||
@@ -126,3 +134,33 @@ func TestHomeUsesSettledSummaryAndSnakeCaseJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeaderboardMasksUsernameAndAddsRank(t *testing.T) {
|
||||
service := New(fakeRepository{leaders: []store.LeaderboardMember{
|
||||
{MemberID: 7, Username: "风轻云淡", CompletedCount: 12},
|
||||
{MemberID: 8, Username: "李明", CompletedCount: 9},
|
||||
}})
|
||||
items, err := service.Leaderboard(context.Background(), 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 2 || items[0].Rank != 1 || items[0].Username != "风***淡" ||
|
||||
items[1].Username != "李*" || items[1].Status != "领取成功" {
|
||||
t.Fatalf("unexpected leaderboard: %+v", items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomerServicesSanitizeURLsAndMapStatus(t *testing.T) {
|
||||
service := New(fakeRepository{services: []store.CustomerService{
|
||||
{ID: 1, Name: "在线客服", AvatarURL: "https://example.com/avatar.png", ServiceURL: "https://example.com/chat", OnlineStatus: 1},
|
||||
{ID: 2, Name: "离线客服", AvatarURL: "javascript:alert(1)", ServiceURL: "http://example.com/chat", OnlineStatus: 2},
|
||||
}})
|
||||
items, err := service.CustomerServices(context.Background(), 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(items) != 2 || items[0].OnlineStatus != "online" || items[0].ServiceURL == "" ||
|
||||
items[1].OnlineStatus != "offline" || items[1].AvatarURL != "" || items[1].ServiceURL != "" {
|
||||
t.Fatalf("unexpected customer services: %+v", items)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user