Skip to content

Commit 01ddfe9

Browse files
dengzhaofunclaude
andauthored
Add social modules — friend, guild, team, friend-gift (#18)
Four new game SaaS modules for social gameplay features: - **Friend**: settings, relationships (single-row pair storage), requests (send/accept/reject/cancel), blocks (with side effects: unfriend + cancel pending). 69 new API endpoints (admin + client). - **Guild**: settings, guild CRUD, membership (leader/officer/member roles), join requests/invitations, contribution tracking with guild XP + level-up. One-guild-per-user enforced at service layer. - **Team**: multi-config (per game mode), ephemeral teams, join/leave/dissolve, quick match, invitations. Atomic memberCount with version guards. - **Friend Gift**: settings, gift packages (admin templates), send/claim flow with daily limits, friendship validation, item service integration. Each module includes: - Schema (17 new tables) with migrations - Service layer (protocol-agnostic, no db.transaction) - Admin routes (OpenAPIHono) + client routes (HMAC auth) - Comprehensive service tests (129 tests total) - Admin dashboard pages with i18n (en/zh) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d07f4d4 commit 01ddfe9

61 files changed

Lines changed: 23274 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/admin/messages/en.json

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,5 +706,57 @@
706706
"collection_rescan_success": "Sync done, newly unlocked",
707707
"collection_rescan_need_user": "Please enter endUserId",
708708
"collection_rescan_failed": "Sync failed",
709-
"collection_saving": "Saving..."
709+
"collection_saving": "Saving...",
710+
711+
"nav_friend": "Friend",
712+
"nav_guild": "Guild",
713+
"nav_team": "Team",
714+
"nav_gift": "Gift",
715+
716+
"friend_title": "Friend System",
717+
"friend_settings": "Friend Settings",
718+
"friend_settings_updated": "Friend settings updated",
719+
"friend_max_friends": "Max Friends",
720+
"friend_max_blocked": "Max Blocked",
721+
"friend_max_pending": "Max Pending Requests",
722+
"friend_relationships": "Relationships",
723+
"friend_no_settings": "No friend settings configured yet.",
724+
"friend_no_relationships": "No friend relationships yet.",
725+
"friend_relationship_deleted": "Friendship removed",
726+
727+
"guild_title": "Guild System",
728+
"guild_settings": "Guild Settings",
729+
"guild_settings_updated": "Guild settings updated",
730+
"guild_max_members": "Max Members",
731+
"guild_max_officers": "Max Officers",
732+
"guild_join_mode": "Default Join Mode",
733+
"guild_guilds": "Guilds",
734+
"guild_no_settings": "No guild settings configured yet.",
735+
"guild_no_guilds": "No guilds yet.",
736+
"guild_disbanded": "Guild disbanded",
737+
738+
"team_title": "Team Configs",
739+
"team_new_config": "New Config",
740+
"team_config_created": "Team config created",
741+
"team_config_updated": "Team config updated",
742+
"team_config_deleted": "Team config deleted",
743+
"team_no_configs": "No team configs yet.",
744+
"team_max_members": "Max Members",
745+
"team_auto_dissolve": "Auto Dissolve on Leader Leave",
746+
"team_quick_match": "Quick Match",
747+
748+
"gift_title": "Friend Gift",
749+
"gift_settings": "Gift Settings",
750+
"gift_settings_updated": "Gift settings updated",
751+
"gift_daily_send_limit": "Daily Send Limit",
752+
"gift_daily_receive_limit": "Daily Receive Limit",
753+
"gift_timezone": "Timezone",
754+
"gift_packages": "Gift Packages",
755+
"gift_new_package": "New Package",
756+
"gift_package_created": "Gift package created",
757+
"gift_package_updated": "Gift package updated",
758+
"gift_package_deleted": "Gift package deleted",
759+
"gift_no_settings": "No gift settings configured yet.",
760+
"gift_no_packages": "No gift packages yet.",
761+
"gift_items_count": "Items"
710762
}

apps/admin/messages/zh.json

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,5 +706,57 @@
706706
"collection_rescan_success": "同步成功,新解锁",
707707
"collection_rescan_need_user": "请输入 endUserId",
708708
"collection_rescan_failed": "同步失败",
709-
"collection_saving": "保存中..."
709+
"collection_saving": "保存中...",
710+
711+
"nav_friend": "好友",
712+
"nav_guild": "公会",
713+
"nav_team": "组队",
714+
"nav_gift": "赠礼",
715+
716+
"friend_title": "好友系统",
717+
"friend_settings": "好友设置",
718+
"friend_settings_updated": "好友设置已更新",
719+
"friend_max_friends": "好友上限",
720+
"friend_max_blocked": "拉黑上限",
721+
"friend_max_pending": "待处理请求上限",
722+
"friend_relationships": "好友关系",
723+
"friend_no_settings": "尚未配置好友设置。",
724+
"friend_no_relationships": "暂无好友关系。",
725+
"friend_relationship_deleted": "已解除好友关系",
726+
727+
"guild_title": "公会系统",
728+
"guild_settings": "公会设置",
729+
"guild_settings_updated": "公会设置已更新",
730+
"guild_max_members": "成员上限",
731+
"guild_max_officers": "干部上限",
732+
"guild_join_mode": "默认加入模式",
733+
"guild_guilds": "公会列表",
734+
"guild_no_settings": "尚未配置公会设置。",
735+
"guild_no_guilds": "暂无公会。",
736+
"guild_disbanded": "公会已解散",
737+
738+
"team_title": "组队配置",
739+
"team_new_config": "新建配置",
740+
"team_config_created": "组队配置已创建",
741+
"team_config_updated": "组队配置已更新",
742+
"team_config_deleted": "组队配置已删除",
743+
"team_no_configs": "暂无组队配置。",
744+
"team_max_members": "队伍人数上限",
745+
"team_auto_dissolve": "队长离开时自动解散",
746+
"team_quick_match": "快速匹配",
747+
748+
"gift_title": "好友赠礼",
749+
"gift_settings": "赠礼设置",
750+
"gift_settings_updated": "赠礼设置已更新",
751+
"gift_daily_send_limit": "每日发送上限",
752+
"gift_daily_receive_limit": "每日接收上限",
753+
"gift_timezone": "时区",
754+
"gift_packages": "礼包模板",
755+
"gift_new_package": "新建礼包",
756+
"gift_package_created": "礼包已创建",
757+
"gift_package_updated": "礼包已更新",
758+
"gift_package_deleted": "礼包已删除",
759+
"gift_no_settings": "尚未配置赠礼设置。",
760+
"gift_no_packages": "暂无礼包模板。",
761+
"gift_items_count": "物品数"
710762
}

apps/admin/src/components/AppSidebar.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import {
55
BookOpen,
66
CalendarCheck,
77
GalleryHorizontal,
8+
Gift,
89
KeyRound,
910
LayoutDashboard,
1011
Mail,
1112
MessagesSquare,
1213
Package,
14+
Shield,
1315
ShoppingCart,
16+
Swords,
1417
Ticket,
18+
Users,
1519
} from "lucide-react"
1620

1721
import {
@@ -42,6 +46,10 @@ function getNavItems() {
4246
{ title: m.nav_banner(), to: "/banner" as const, icon: GalleryHorizontal },
4347
{ title: m.nav_dialogue(), to: "/dialogue" as const, icon: MessagesSquare },
4448
{ title: m.nav_collection(), to: "/collection" as const, icon: BookOpen },
49+
{ title: m.nav_friend(), to: "/friend" as const, icon: Users },
50+
{ title: m.nav_guild(), to: "/guild" as const, icon: Shield },
51+
{ title: m.nav_team(), to: "/team" as const, icon: Swords },
52+
{ title: m.nav_gift(), to: "/friend-gift" as const, icon: Gift },
4553
{ title: m.nav_api_keys(), to: "/api-keys" as const, icon: KeyRound },
4654
]
4755
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
2+
3+
import { api } from "#/lib/api-client"
4+
import type {
5+
CreateFriendGiftPackageInput,
6+
FriendGiftPackage,
7+
FriendGiftSend,
8+
FriendGiftSettings,
9+
UpdateFriendGiftPackageInput,
10+
UpsertFriendGiftSettingsInput,
11+
} from "#/lib/types/friend-gift"
12+
13+
const SETTINGS_KEY = ["friend-gift-settings"] as const
14+
const PACKAGES_KEY = ["friend-gift-packages"] as const
15+
const SENDS_KEY = ["friend-gift-sends"] as const
16+
17+
export function useFriendGiftSettings() {
18+
return useQuery({
19+
queryKey: SETTINGS_KEY,
20+
queryFn: () => api.get<FriendGiftSettings>("/api/friend-gift/settings"),
21+
})
22+
}
23+
24+
export function useUpsertFriendGiftSettings() {
25+
const qc = useQueryClient()
26+
return useMutation({
27+
mutationFn: (input: UpsertFriendGiftSettingsInput) =>
28+
api.put<FriendGiftSettings>("/api/friend-gift/settings", input),
29+
onSuccess: () => qc.invalidateQueries({ queryKey: SETTINGS_KEY }),
30+
})
31+
}
32+
33+
export function useFriendGiftPackages() {
34+
return useQuery({
35+
queryKey: PACKAGES_KEY,
36+
queryFn: () =>
37+
api.get<{ items: FriendGiftPackage[] }>("/api/friend-gift/packages"),
38+
select: (data) => data.items,
39+
})
40+
}
41+
42+
export function useFriendGiftPackage(id: string) {
43+
return useQuery({
44+
queryKey: [...PACKAGES_KEY, id],
45+
queryFn: () =>
46+
api.get<FriendGiftPackage>(`/api/friend-gift/packages/${id}`),
47+
enabled: !!id,
48+
})
49+
}
50+
51+
export function useCreateFriendGiftPackage() {
52+
const qc = useQueryClient()
53+
return useMutation({
54+
mutationFn: (input: CreateFriendGiftPackageInput) =>
55+
api.post<FriendGiftPackage>("/api/friend-gift/packages", input),
56+
onSuccess: () => qc.invalidateQueries({ queryKey: PACKAGES_KEY }),
57+
})
58+
}
59+
60+
export function useUpdateFriendGiftPackage() {
61+
const qc = useQueryClient()
62+
return useMutation({
63+
mutationFn: ({
64+
id,
65+
input,
66+
}: {
67+
id: string
68+
input: UpdateFriendGiftPackageInput
69+
}) => api.patch<FriendGiftPackage>(`/api/friend-gift/packages/${id}`, input),
70+
onSuccess: () => qc.invalidateQueries({ queryKey: PACKAGES_KEY }),
71+
})
72+
}
73+
74+
export function useDeleteFriendGiftPackage() {
75+
const qc = useQueryClient()
76+
return useMutation({
77+
mutationFn: (id: string) =>
78+
api.delete(`/api/friend-gift/packages/${id}`),
79+
onSuccess: () => qc.invalidateQueries({ queryKey: PACKAGES_KEY }),
80+
})
81+
}
82+
83+
export function useFriendGiftSends() {
84+
return useQuery({
85+
queryKey: SENDS_KEY,
86+
queryFn: () =>
87+
api.get<{ items: FriendGiftSend[] }>("/api/friend-gift/sends"),
88+
select: (data) => data.items,
89+
})
90+
}

apps/admin/src/hooks/use-friend.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
2+
3+
import { api } from "#/lib/api-client"
4+
import type {
5+
FriendRelationship,
6+
FriendSettings,
7+
UpsertFriendSettingsInput,
8+
} from "#/lib/types/friend"
9+
10+
const SETTINGS_KEY = ["friend-settings"] as const
11+
const RELATIONSHIPS_KEY = ["friend-relationships"] as const
12+
13+
export function useFriendSettings() {
14+
return useQuery({
15+
queryKey: SETTINGS_KEY,
16+
queryFn: () => api.get<FriendSettings>("/api/friend/settings"),
17+
})
18+
}
19+
20+
export function useUpsertFriendSettings() {
21+
const qc = useQueryClient()
22+
return useMutation({
23+
mutationFn: (input: UpsertFriendSettingsInput) =>
24+
api.put<FriendSettings>("/api/friend/settings", input),
25+
onSuccess: () => qc.invalidateQueries({ queryKey: SETTINGS_KEY }),
26+
})
27+
}
28+
29+
export function useFriendRelationships() {
30+
return useQuery({
31+
queryKey: RELATIONSHIPS_KEY,
32+
queryFn: () =>
33+
api.get<{ items: FriendRelationship[] }>("/api/friend/relationships"),
34+
select: (data) => data.items,
35+
})
36+
}
37+
38+
export function useDeleteFriendRelationship() {
39+
const qc = useQueryClient()
40+
return useMutation({
41+
mutationFn: (id: string) =>
42+
api.delete(`/api/friend/relationships/${id}`),
43+
onSuccess: () =>
44+
qc.invalidateQueries({ queryKey: RELATIONSHIPS_KEY }),
45+
})
46+
}

apps/admin/src/hooks/use-guild.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
2+
3+
import { api } from "#/lib/api-client"
4+
import type {
5+
Guild,
6+
GuildSettings,
7+
UpsertGuildSettingsInput,
8+
} from "#/lib/types/guild"
9+
10+
const SETTINGS_KEY = ["guild-settings"] as const
11+
const GUILDS_KEY = ["guilds"] as const
12+
13+
export function useGuildSettings() {
14+
return useQuery({
15+
queryKey: SETTINGS_KEY,
16+
queryFn: () => api.get<GuildSettings>("/api/guild/settings"),
17+
})
18+
}
19+
20+
export function useUpsertGuildSettings() {
21+
const qc = useQueryClient()
22+
return useMutation({
23+
mutationFn: (input: UpsertGuildSettingsInput) =>
24+
api.put<GuildSettings>("/api/guild/settings", input),
25+
onSuccess: () => qc.invalidateQueries({ queryKey: SETTINGS_KEY }),
26+
})
27+
}
28+
29+
export function useGuilds() {
30+
return useQuery({
31+
queryKey: GUILDS_KEY,
32+
queryFn: () => api.get<{ items: Guild[] }>("/api/guild/guilds"),
33+
select: (data) => data.items,
34+
})
35+
}
36+
37+
export function useGuild(id: string) {
38+
return useQuery({
39+
queryKey: [...GUILDS_KEY, id],
40+
queryFn: () => api.get<Guild>(`/api/guild/guilds/${id}`),
41+
enabled: !!id,
42+
})
43+
}
44+
45+
export function useDeleteGuild() {
46+
const qc = useQueryClient()
47+
return useMutation({
48+
mutationFn: (id: string) => api.delete(`/api/guild/guilds/${id}`),
49+
onSuccess: () => qc.invalidateQueries({ queryKey: GUILDS_KEY }),
50+
})
51+
}

0 commit comments

Comments
 (0)