From 747191e33bdb1f48cd31a5dc0f447ace2c42e78f Mon Sep 17 00:00:00 2001
From: taojiacheng
Date: Mon, 22 Jun 2026 17:24:51 +0800
Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E7=A7=81=E6=9C=89?=
=?UTF-8?q?=E5=8C=96=E4=BA=A4=E6=98=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
drizzle/0002_puzzling_wildside.sql | 16 +
drizzle/meta/0002_snapshot.json | 4489 +++++++++++++++++
drizzle/meta/_journal.json | 7 +
.../components/auction/ListItemModal.tsx | 99 +
.../consumables/talismanDisplay.test.ts | 32 +
.../feature/consumables/talismanDisplay.ts | 14 +
src/react-app/routes/game/auction/route.tsx | 11 +
src/react-app/routes/game/mail/route.tsx | 713 ++-
src/server/lib/drizzle/schema.ts | 33 +
.../lib/repositories/auctionRepository.ts | 19 +-
.../repositories/retentionRepository.test.ts | 47 +
.../lib/repositories/retentionRepository.ts | 9 +-
.../lib/services/AuctionService.test.ts | 81 +
src/server/lib/services/AuctionService.ts | 239 +-
src/server/lib/services/FriendService.test.ts | 73 +
src/server/lib/services/FriendService.ts | 228 +
.../lib/services/PlayerMailService.test.ts | 167 +
src/server/lib/services/PlayerMailService.ts | 219 +
.../lib/services/TalismanScenarioService.ts | 56 +
src/server/routes/api/auction.router.ts | 24 +-
src/server/routes/api/cultivator.router.ts | 77 +
src/server/routes/api/friends.router.ts | 99 +
src/server/routes/api/index.ts | 2 +
src/shared/config/socialConfig.ts | 3 +
24 files changed, 6687 insertions(+), 70 deletions(-)
create mode 100644 drizzle/0002_puzzling_wildside.sql
create mode 100644 drizzle/meta/0002_snapshot.json
create mode 100644 src/server/lib/repositories/retentionRepository.test.ts
create mode 100644 src/server/lib/services/FriendService.test.ts
create mode 100644 src/server/lib/services/FriendService.ts
create mode 100644 src/server/lib/services/PlayerMailService.test.ts
create mode 100644 src/server/lib/services/PlayerMailService.ts
create mode 100644 src/server/lib/services/TalismanScenarioService.ts
create mode 100644 src/server/routes/api/friends.router.ts
create mode 100644 src/shared/config/socialConfig.ts
diff --git a/drizzle/0002_puzzling_wildside.sql b/drizzle/0002_puzzling_wildside.sql
new file mode 100644
index 00000000..bfe5b205
--- /dev/null
+++ b/drizzle/0002_puzzling_wildside.sql
@@ -0,0 +1,16 @@
+CREATE TABLE "wanjiedaoyou_cultivator_friends" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "cultivator_id" uuid NOT NULL,
+ "friend_cultivator_id" uuid NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "wanjiedaoyou_auction_listings" ADD COLUMN "visibility" varchar(20) DEFAULT 'public' NOT NULL;--> statement-breakpoint
+ALTER TABLE "wanjiedaoyou_auction_listings" ADD COLUMN "target_cultivator_id" uuid;--> statement-breakpoint
+ALTER TABLE "wanjiedaoyou_auction_listings" ADD COLUMN "target_cultivator_name" varchar(100);--> statement-breakpoint
+ALTER TABLE "wanjiedaoyou_cultivator_friends" ADD CONSTRAINT "wanjiedaoyou_cultivator_friends_cultivator_id_wanjiedaoyou_cultivators_id_fk" FOREIGN KEY ("cultivator_id") REFERENCES "public"."wanjiedaoyou_cultivators"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "wanjiedaoyou_cultivator_friends" ADD CONSTRAINT "wanjiedaoyou_cultivator_friends_friend_cultivator_id_wanjiedaoyou_cultivators_id_fk" FOREIGN KEY ("friend_cultivator_id") REFERENCES "public"."wanjiedaoyou_cultivators"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE UNIQUE INDEX "cultivator_friends_pair_uidx" ON "wanjiedaoyou_cultivator_friends" USING btree ("cultivator_id","friend_cultivator_id");--> statement-breakpoint
+CREATE INDEX "cultivator_friends_friend_idx" ON "wanjiedaoyou_cultivator_friends" USING btree ("friend_cultivator_id");--> statement-breakpoint
+ALTER TABLE "wanjiedaoyou_auction_listings" ADD CONSTRAINT "wanjiedaoyou_auction_listings_target_cultivator_id_wanjiedaoyou_cultivators_id_fk" FOREIGN KEY ("target_cultivator_id") REFERENCES "public"."wanjiedaoyou_cultivators"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "auction_visibility_target_status_idx" ON "wanjiedaoyou_auction_listings" USING btree ("visibility","target_cultivator_id","status");
\ No newline at end of file
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
new file mode 100644
index 00000000..7403fdf6
--- /dev/null
+++ b/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,4489 @@
+{
+ "id": "ab223b8e-f969-4274-92c2-2601e2ef0fef",
+ "prevId": "64fb4265-615c-4b7b-8514-2feb788b358c",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.wanjiedaoyou_admin_message_templates": {
+ "name": "wanjiedaoyou_admin_message_templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "channel": {
+ "name": "channel",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_template": {
+ "name": "subject_template",
+ "type": "varchar(300)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_template": {
+ "name": "content_template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_payload": {
+ "name": "default_payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "admin_templates_channel_status_created_idx": {
+ "name": "admin_templates_channel_status_created_idx",
+ "columns": [
+ {
+ "expression": "channel",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_alchemy_formulas": {
+ "name": "wanjiedaoyou_alchemy_formulas",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "family": {
+ "name": "family",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pattern": {
+ "name": "pattern",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "blueprint": {
+ "name": "blueprint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mastery": {
+ "name": "mastery",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"level\":0,\"exp\":0}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "alchemy_formulas_cultivator_updated_idx": {
+ "name": "alchemy_formulas_cultivator_updated_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "alchemy_formulas_cultivator_family_idx": {
+ "name": "alchemy_formulas_cultivator_family_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "family",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_alchemy_formulas_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_alchemy_formulas_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_alchemy_formulas",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_app_settings": {
+ "name": "wanjiedaoyou_app_settings",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(128)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_auction_listings": {
+ "name": "wanjiedaoyou_auction_listings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "seller_id": {
+ "name": "seller_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "seller_name": {
+ "name": "seller_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_type": {
+ "name": "item_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_snapshot": {
+ "name": "item_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "price": {
+ "name": "price",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'public'"
+ },
+ "target_cultivator_id": {
+ "name": "target_cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_cultivator_name": {
+ "name": "target_cultivator_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sold_at": {
+ "name": "sold_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "auction_status_expires_created_idx": {
+ "name": "auction_status_expires_created_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auction_seller_status_idx": {
+ "name": "auction_seller_status_idx",
+ "columns": [
+ {
+ "expression": "seller_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auction_status_expires_price_idx": {
+ "name": "auction_status_expires_price_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "price",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auction_status_expires_item_type_idx": {
+ "name": "auction_status_expires_item_type_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "item_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auction_visibility_target_status_idx": {
+ "name": "auction_visibility_target_status_idx",
+ "columns": [
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_auction_listings_seller_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_auction_listings_seller_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_auction_listings",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "seller_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_auction_listings_target_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_auction_listings_target_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_auction_listings",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "target_cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_battle_records": {
+ "name": "wanjiedaoyou_battle_records",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "challenge_type": {
+ "name": "challenge_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "opponent_cultivator_id": {
+ "name": "opponent_cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "battle_result": {
+ "name": "battle_result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "battle_report": {
+ "name": "battle_report",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "battle_records_cultivator_created_idx": {
+ "name": "battle_records_cultivator_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "battle_records_opponent_created_idx": {
+ "name": "battle_records_opponent_created_idx",
+ "columns": [
+ {
+ "expression": "opponent_cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_battle_records_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_battle_records_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_battle_records",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_battle_records_opponent_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_battle_records_opponent_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_battle_records",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "opponent_cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_battle_records_v2": {
+ "name": "wanjiedaoyou_battle_records_v2",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "battle_type": {
+ "name": "battle_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'normal'"
+ },
+ "opponent_cultivator_id": {
+ "name": "opponent_cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "engine_version": {
+ "name": "engine_version",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'battle-v5'"
+ },
+ "result_version": {
+ "name": "result_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 2
+ },
+ "battle_result": {
+ "name": "battle_result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "battle_report": {
+ "name": "battle_report",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "battle_records_v2_cultivator_created_idx": {
+ "name": "battle_records_v2_cultivator_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "battle_records_v2_opponent_created_idx": {
+ "name": "battle_records_v2_opponent_created_idx",
+ "columns": [
+ {
+ "expression": "opponent_cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "battle_records_v2_user_created_idx": {
+ "name": "battle_records_v2_user_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_battle_records_v2_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_battle_records_v2_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_battle_records_v2",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_battle_records_v2_opponent_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_battle_records_v2_opponent_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_battle_records_v2",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "opponent_cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_bet_battles": {
+ "name": "wanjiedaoyou_bet_battles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "creator_id": {
+ "name": "creator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator_name": {
+ "name": "creator_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "min_realm": {
+ "name": "min_realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "max_realm": {
+ "name": "max_realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "taunt": {
+ "name": "taunt",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "creator_stake_snapshot": {
+ "name": "creator_stake_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "challenger_stake_snapshot": {
+ "name": "challenger_stake_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "challenger_id": {
+ "name": "challenger_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "challenger_name": {
+ "name": "challenger_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "winner_cultivator_id": {
+ "name": "winner_cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "battle_record_id": {
+ "name": "battle_record_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "battle_record_v2_id": {
+ "name": "battle_record_v2_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "matched_at": {
+ "name": "matched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "settled_at": {
+ "name": "settled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bet_battles_status_expires_idx": {
+ "name": "bet_battles_status_expires_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bet_battles_creator_status_idx": {
+ "name": "bet_battles_creator_status_idx",
+ "columns": [
+ {
+ "expression": "creator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bet_battles_status_created_idx": {
+ "name": "bet_battles_status_created_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_bet_battles_creator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_bet_battles_creator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_bet_battles",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "creator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_bet_battles_challenger_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_bet_battles_challenger_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_bet_battles",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "challenger_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_bet_battles_winner_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_bet_battles_winner_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_bet_battles",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "winner_cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_bet_battles_battle_record_id_wanjiedaoyou_battle_records_id_fk": {
+ "name": "wanjiedaoyou_bet_battles_battle_record_id_wanjiedaoyou_battle_records_id_fk",
+ "tableFrom": "wanjiedaoyou_bet_battles",
+ "tableTo": "wanjiedaoyou_battle_records",
+ "columnsFrom": [
+ "battle_record_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_bet_battles_battle_record_v2_id_wanjiedaoyou_battle_records_v2_id_fk": {
+ "name": "wanjiedaoyou_bet_battles_battle_record_v2_id_wanjiedaoyou_battle_records_v2_id_fk",
+ "tableFrom": "wanjiedaoyou_bet_battles",
+ "tableTo": "wanjiedaoyou_battle_records_v2",
+ "columnsFrom": [
+ "battle_record_v2_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_breakthrough_history": {
+ "name": "wanjiedaoyou_breakthrough_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_realm": {
+ "name": "from_realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_stage": {
+ "name": "from_stage",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "to_realm": {
+ "name": "to_realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "to_stage": {
+ "name": "to_stage",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "age": {
+ "name": "age",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "years_spent": {
+ "name": "years_spent",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "story": {
+ "name": "story",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "breakthrough_history_cultivator_created_idx": {
+ "name": "breakthrough_history_cultivator_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_breakthrough_history_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_breakthrough_history_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_breakthrough_history",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_consumables": {
+ "name": "wanjiedaoyou_consumables",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'凡品'"
+ },
+ "spec": {
+ "name": "spec",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "score": {
+ "name": "score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "consumables_cultivator_idx": {
+ "name": "consumables_cultivator_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "consumables_cultivator_name_quality_idx": {
+ "name": "consumables_cultivator_name_quality_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "quality",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "consumables_score_idx": {
+ "name": "consumables_score_idx",
+ "columns": [
+ {
+ "expression": "score",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_consumables_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_consumables_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_consumables",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_creation_products": {
+ "name": "wanjiedaoyou_creation_products",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "product_type": {
+ "name": "product_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "element": {
+ "name": "element",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slot": {
+ "name": "slot",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "score": {
+ "name": "score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_equipped": {
+ "name": "is_equipped",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "product_model": {
+ "name": "product_model",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "creation_products_cultivator_type_idx": {
+ "name": "creation_products_cultivator_type_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "product_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "creation_products_type_score_idx": {
+ "name": "creation_products_type_score_idx",
+ "columns": [
+ {
+ "expression": "product_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "score",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "creation_products_equipped_idx": {
+ "name": "creation_products_equipped_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_equipped",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_creation_products_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_creation_products_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_creation_products",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_cultivator_friends": {
+ "name": "wanjiedaoyou_cultivator_friends",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "friend_cultivator_id": {
+ "name": "friend_cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "cultivator_friends_pair_uidx": {
+ "name": "cultivator_friends_pair_uidx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "friend_cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "cultivator_friends_friend_idx": {
+ "name": "cultivator_friends_friend_idx",
+ "columns": [
+ {
+ "expression": "friend_cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_cultivator_friends_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_cultivator_friends_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_cultivator_friends",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_cultivator_friends_friend_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_cultivator_friends_friend_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_cultivator_friends",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "friend_cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_cultivator_state_versions": {
+ "name": "wanjiedaoyou_cultivator_state_versions",
+ "schema": "",
+ "columns": {
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "global_version": {
+ "name": "global_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "profile_version": {
+ "name": "profile_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "condition_version": {
+ "name": "condition_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "progress_version": {
+ "name": "progress_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "currency_version": {
+ "name": "currency_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "inventory_version": {
+ "name": "inventory_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "products_version": {
+ "name": "products_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "mail_version": {
+ "name": "mail_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "tasks_version": {
+ "name": "tasks_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "wanjiedaoyou_cultivator_state_versions_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_cultivator_state_versions_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_cultivator_state_versions",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_cultivator_tasks": {
+ "name": "wanjiedaoyou_cultivator_tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "definition_id": {
+ "name": "definition_id",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "current_stage": {
+ "name": "current_stage",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "objectives": {
+ "name": "objectives",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "cultivator_tasks_cultivator_status_updated_idx": {
+ "name": "cultivator_tasks_cultivator_status_updated_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "cultivator_tasks_cultivator_definition_unique": {
+ "name": "cultivator_tasks_cultivator_definition_unique",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "definition_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_cultivator_tasks_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_cultivator_tasks_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_cultivator_tasks",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_cultivators": {
+ "name": "wanjiedaoyou_cultivators",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gender": {
+ "name": "gender",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "personality": {
+ "name": "personality",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "background": {
+ "name": "background",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "realm": {
+ "name": "realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "realm_stage": {
+ "name": "realm_stage",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "age": {
+ "name": "age",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 18
+ },
+ "lifespan": {
+ "name": "lifespan",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "closed_door_years_total": {
+ "name": "closed_door_years_total",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "died_at": {
+ "name": "died_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vitality": {
+ "name": "vitality",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spirit": {
+ "name": "spirit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "wisdom": {
+ "name": "wisdom",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "speed": {
+ "name": "speed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "willpower": {
+ "name": "willpower",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spirit_stones": {
+ "name": "spirit_stones",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "reputation": {
+ "name": "reputation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "qi": {
+ "name": "qi",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 200
+ },
+ "qi_last_refreshed_at": {
+ "name": "qi_last_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_yield_at": {
+ "name": "last_yield_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "max_skills": {
+ "name": "max_skills",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 4
+ },
+ "balance_notes": {
+ "name": "balance_notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "condition": {
+ "name": "condition",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "game_settings": {
+ "name": "game_settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "cultivation_progress": {
+ "name": "cultivation_progress",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "cultivators_user_status_updated_idx": {
+ "name": "cultivators_user_status_updated_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "cultivators_status_created_idx": {
+ "name": "cultivators_status_created_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_dungeon_histories": {
+ "name": "wanjiedaoyou_dungeon_histories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "log": {
+ "name": "log",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "real_gains": {
+ "name": "real_gains",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "dungeon_histories_cultivator_created_idx": {
+ "name": "dungeon_histories_cultivator_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_dungeon_histories_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_dungeon_histories_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_dungeon_histories",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_dungeon_runs": {
+ "name": "wanjiedaoyou_dungeon_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "map_node_id": {
+ "name": "map_node_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'EXPLORING'"
+ },
+ "current_round": {
+ "name": "current_round",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "max_rounds": {
+ "name": "max_rounds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "danger_score": {
+ "name": "danger_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10
+ },
+ "run_state": {
+ "name": "run_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cost_ledger": {
+ "name": "cost_ledger",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "gain_ledger": {
+ "name": "gain_ledger",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "pending_action": {
+ "name": "pending_action",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active_battle_id": {
+ "name": "active_battle_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "battle_payload": {
+ "name": "battle_payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "dungeon_runs_cultivator_status_updated_idx": {
+ "name": "dungeon_runs_cultivator_status_updated_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "dungeon_runs_status_updated_idx": {
+ "name": "dungeon_runs_status_updated_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_dungeon_runs_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_dungeon_runs_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_dungeon_runs",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_feedbacks": {
+ "name": "wanjiedaoyou_feedbacks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "feedback_user_created_at_idx": {
+ "name": "feedback_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "feedback_status_type_created_at_idx": {
+ "name": "feedback_status_type_created_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_feedbacks_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_feedbacks_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_feedbacks",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_item_library": {
+ "name": "wanjiedaoyou_item_library",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "item_id": {
+ "name": "item_id",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'published'"
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "element": {
+ "name": "element",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sample_key": {
+ "name": "sample_key",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "editor_config": {
+ "name": "editor_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "item_library_item_id_unique": {
+ "name": "item_library_item_id_unique",
+ "columns": [
+ {
+ "expression": "item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "item_library_status_type_idx": {
+ "name": "item_library_status_type_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "item_library_material_sample_idx": {
+ "name": "item_library_material_sample_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "quality",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sample_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "item_library_name_idx": {
+ "name": "item_library_name_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_mails": {
+ "name": "wanjiedaoyou_mails",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "attachments": {
+ "name": "attachments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_read": {
+ "name": "is_read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_claimed": {
+ "name": "is_claimed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "mails_cultivator_created_idx": {
+ "name": "mails_cultivator_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mails_cultivator_is_read_created_idx": {
+ "name": "mails_cultivator_is_read_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_mails_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_mails_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_mails",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_materials": {
+ "name": "wanjiedaoyou_materials",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rank": {
+ "name": "rank",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "element": {
+ "name": "element",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "materials_cultivator_idx": {
+ "name": "materials_cultivator_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "materials_cultivator_name_idx": {
+ "name": "materials_cultivator_name_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "materials_cultivator_name_rank_idx": {
+ "name": "materials_cultivator_name_rank_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "rank",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_materials_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_materials_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_materials",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_player_state_events": {
+ "name": "wanjiedaoyou_player_state_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigint",
+ "primaryKey": true,
+ "notNull": true,
+ "identity": {
+ "type": "always",
+ "name": "wanjiedaoyou_player_state_events_id_seq",
+ "schema": "public",
+ "increment": "1",
+ "startWith": "1",
+ "minValue": "1",
+ "maxValue": "9223372036854775807",
+ "cache": "1",
+ "cycle": false
+ }
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "global_version": {
+ "name": "global_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain_version": {
+ "name": "domain_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "varchar(96)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "patch": {
+ "name": "patch",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "invalidates": {
+ "name": "invalidates",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(96)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "player_state_events_cultivator_version_idx": {
+ "name": "player_state_events_cultivator_version_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "global_version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "player_state_events_user_idx": {
+ "name": "player_state_events_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "player_state_events_created_idx": {
+ "name": "player_state_events_created_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_player_state_events_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_player_state_events_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_player_state_events",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_pre_heaven_fates": {
+ "name": "wanjiedaoyou_pre_heaven_fates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "effects": {
+ "name": "effects",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'::jsonb"
+ },
+ "registry_key": {
+ "name": "registry_key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "details": {
+ "name": "details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pre_heaven_fates_cultivator_idx": {
+ "name": "pre_heaven_fates_cultivator_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_pre_heaven_fates_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_pre_heaven_fates_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_pre_heaven_fates",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_qi_logs": {
+ "name": "wanjiedaoyou_qi_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action_instance_id": {
+ "name": "action_instance_id",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "qi_cost": {
+ "name": "qi_cost",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "qi_gain": {
+ "name": "qi_gain",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "qi_before": {
+ "name": "qi_before",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "qi_after": {
+ "name": "qi_after",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "qi_logs_action_instance_uidx": {
+ "name": "qi_logs_action_instance_uidx",
+ "columns": [
+ {
+ "expression": "action_instance_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "qi_logs_cultivator_created_idx": {
+ "name": "qi_logs_cultivator_created_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "qi_logs_status_created_idx": {
+ "name": "qi_logs_status_created_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_qi_logs_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_qi_logs_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_qi_logs",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_redeem_code_claims": {
+ "name": "wanjiedaoyou_redeem_code_claims",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "redeem_code_id": {
+ "name": "redeem_code_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mail_id": {
+ "name": "mail_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "redeem_code_claims_code_user_unique": {
+ "name": "redeem_code_claims_code_user_unique",
+ "columns": [
+ {
+ "expression": "redeem_code_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "redeem_code_claims_user_idx": {
+ "name": "redeem_code_claims_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "redeem_code_claims_code_idx": {
+ "name": "redeem_code_claims_code_idx",
+ "columns": [
+ {
+ "expression": "redeem_code_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_redeem_code_claims_redeem_code_id_wanjiedaoyou_redeem_codes_id_fk": {
+ "name": "wanjiedaoyou_redeem_code_claims_redeem_code_id_wanjiedaoyou_redeem_codes_id_fk",
+ "tableFrom": "wanjiedaoyou_redeem_code_claims",
+ "tableTo": "wanjiedaoyou_redeem_codes",
+ "columnsFrom": [
+ "redeem_code_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_redeem_code_claims_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_redeem_code_claims_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_redeem_code_claims",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_redeem_code_claims_mail_id_wanjiedaoyou_mails_id_fk": {
+ "name": "wanjiedaoyou_redeem_code_claims_mail_id_wanjiedaoyou_mails_id_fk",
+ "tableFrom": "wanjiedaoyou_redeem_code_claims",
+ "tableTo": "wanjiedaoyou_mails",
+ "columnsFrom": [
+ "mail_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_redeem_codes": {
+ "name": "wanjiedaoyou_redeem_codes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "code": {
+ "name": "code",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reward_preset_id": {
+ "name": "reward_preset_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reward_attachments": {
+ "name": "reward_attachments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mail_title": {
+ "name": "mail_title",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mail_content": {
+ "name": "mail_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "total_limit": {
+ "name": "total_limit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claimed_count": {
+ "name": "claimed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "starts_at": {
+ "name": "starts_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ends_at": {
+ "name": "ends_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "redeem_codes_code_unique": {
+ "name": "redeem_codes_code_unique",
+ "columns": [
+ {
+ "expression": "code",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "redeem_codes_status_created_idx": {
+ "name": "redeem_codes_status_created_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "redeem_codes_created_idx": {
+ "name": "redeem_codes_created_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_reputation_shop_items": {
+ "name": "wanjiedaoyou_reputation_shop_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "item_library_item_id": {
+ "name": "item_library_item_id",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "price": {
+ "name": "price",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "per_user_limit": {
+ "name": "per_user_limit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_by": {
+ "name": "updated_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reputation_shop_item_library_item_uidx": {
+ "name": "reputation_shop_item_library_item_uidx",
+ "columns": [
+ {
+ "expression": "item_library_item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reputation_shop_status_sort_idx": {
+ "name": "reputation_shop_status_sort_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_reputation_shop_items_item_library_item_id_wanjiedaoyou_item_library_item_id_fk": {
+ "name": "wanjiedaoyou_reputation_shop_items_item_library_item_id_wanjiedaoyou_item_library_item_id_fk",
+ "tableFrom": "wanjiedaoyou_reputation_shop_items",
+ "tableTo": "wanjiedaoyou_item_library",
+ "columnsFrom": [
+ "item_library_item_id"
+ ],
+ "columnsTo": [
+ "item_id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_reputation_shop_purchases": {
+ "name": "wanjiedaoyou_reputation_shop_purchases",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "shop_item_id": {
+ "name": "shop_item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_library_item_id": {
+ "name": "item_library_item_id",
+ "type": "varchar(120)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reputation_cost": {
+ "name": "reputation_cost",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "purchase_week": {
+ "name": "purchase_week",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reputation_shop_purchases_cultivator_item_idx": {
+ "name": "reputation_shop_purchases_cultivator_item_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "shop_item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reputation_shop_purchases_week_idx": {
+ "name": "reputation_shop_purchases_week_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "shop_item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "purchase_week",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reputation_shop_purchases_created_idx": {
+ "name": "reputation_shop_purchases_created_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_reputation_shop_purchases_shop_item_id_wanjiedaoyou_reputation_shop_items_id_fk": {
+ "name": "wanjiedaoyou_reputation_shop_purchases_shop_item_id_wanjiedaoyou_reputation_shop_items_id_fk",
+ "tableFrom": "wanjiedaoyou_reputation_shop_purchases",
+ "tableTo": "wanjiedaoyou_reputation_shop_items",
+ "columnsFrom": [
+ "shop_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "wanjiedaoyou_reputation_shop_purchases_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_reputation_shop_purchases_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_reputation_shop_purchases",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_retreat_records": {
+ "name": "wanjiedaoyou_retreat_records",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "realm": {
+ "name": "realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "realm_stage": {
+ "name": "realm_stage",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "years": {
+ "name": "years",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "success": {
+ "name": "success",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "chance": {
+ "name": "chance",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "roll": {
+ "name": "roll",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "modifiers": {
+ "name": "modifiers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "retreat_records_cultivator_timestamp_idx": {
+ "name": "retreat_records_cultivator_timestamp_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "timestamp",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_retreat_records_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_retreat_records_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_retreat_records",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_spiritual_roots": {
+ "name": "wanjiedaoyou_spiritual_roots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cultivator_id": {
+ "name": "cultivator_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "element": {
+ "name": "element",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "strength": {
+ "name": "strength",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grade": {
+ "name": "grade",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "spiritual_roots_cultivator_idx": {
+ "name": "spiritual_roots_cultivator_idx",
+ "columns": [
+ {
+ "expression": "cultivator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "wanjiedaoyou_spiritual_roots_cultivator_id_wanjiedaoyou_cultivators_id_fk": {
+ "name": "wanjiedaoyou_spiritual_roots_cultivator_id_wanjiedaoyou_cultivators_id_fk",
+ "tableFrom": "wanjiedaoyou_spiritual_roots",
+ "tableTo": "wanjiedaoyou_cultivators",
+ "columnsFrom": [
+ "cultivator_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.wanjiedaoyou_tower_enemy_floors": {
+ "name": "wanjiedaoyou_tower_enemy_floors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "season_key": {
+ "name": "season_key",
+ "type": "varchar(40)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "realm": {
+ "name": "realm",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "floor": {
+ "name": "floor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'ready'"
+ },
+ "schema_version": {
+ "name": "schema_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "enemy": {
+ "name": "enemy",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "generated_at": {
+ "name": "generated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tower_enemy_floors_season_realm_floor_uidx": {
+ "name": "tower_enemy_floors_season_realm_floor_uidx",
+ "columns": [
+ {
+ "expression": "season_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "realm",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "floor",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tower_enemy_floors_realm_floor_generated_idx": {
+ "name": "tower_enemy_floors_realm_floor_generated_idx",
+ "columns": [
+ {
+ "expression": "realm",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "floor",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "generated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tower_enemy_floors_season_realm_status_idx": {
+ "name": "tower_enemy_floors_season_realm_status_idx",
+ "columns": [
+ {
+ "expression": "season_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "realm",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 8d1be05d..cf4a0079 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1782107240879,
"tag": "0001_motionless_iron_patriot",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1782118374333,
+ "tag": "0002_puzzling_wildside",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/react-app/components/auction/ListItemModal.tsx b/src/react-app/components/auction/ListItemModal.tsx
index 41d0a426..da0b818b 100644
--- a/src/react-app/components/auction/ListItemModal.tsx
+++ b/src/react-app/components/auction/ListItemModal.tsx
@@ -13,6 +13,7 @@ import {
InkInput,
InkList,
InkNotice,
+ InkSelect,
InkTabs,
inkFieldVariants,
} from '@app/components/ui';
@@ -57,6 +58,16 @@ type SelectableItem = (Material | Artifact | Consumable) & {
};
type InventoryApiType = 'materials' | 'artifacts' | 'consumables';
+type ListingVisibility = 'public' | 'private';
+
+interface FriendSummary {
+ id: string;
+ name: string;
+ title: string | null;
+ realm: string;
+ realmStage: string;
+ status: string;
+}
interface InventoryPagination {
page: number;
@@ -202,6 +213,10 @@ export function ListItemModal({
const [selectedItem, setSelectedItem] = useState(null);
const [price, setPrice] = useState('');
const [quantity, setQuantity] = useState('1');
+ const [visibility, setVisibility] = useState('public');
+ const [targetCultivatorId, setTargetCultivatorId] = useState('');
+ const [friends, setFriends] = useState([]);
+ const [friendsLoading, setFriendsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const [listError, setListError] = useState('');
@@ -231,6 +246,42 @@ export function ListItemModal({
});
const requestIdRef = useRef(0);
+ useEffect(() => {
+ if (!cultivator?.id || step !== 'price') {
+ return;
+ }
+
+ let cancelled = false;
+ const loadFriends = async () => {
+ try {
+ setFriendsLoading(true);
+ const res = await fetch('/api/friends');
+ const data = await res.json();
+ if (cancelled) return;
+ if (!res.ok) {
+ throw new Error(data.error || '获取好友名录失败');
+ }
+ const nextFriends = (data.friends || []) as FriendSummary[];
+ setFriends(nextFriends);
+ setTargetCultivatorId((current) => current || nextFriends[0]?.id || '');
+ } catch (error) {
+ if (!cancelled) {
+ setError(error instanceof Error ? error.message : '获取好友名录失败');
+ }
+ } finally {
+ if (!cancelled) {
+ setFriendsLoading(false);
+ }
+ }
+ };
+
+ void loadFriends();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [cultivator?.id, step]);
+
const fetchItemPage = useCallback(
async (itemType: ItemType, page: number) => {
if (!cultivator?.id) return;
@@ -401,6 +452,8 @@ export function ListItemModal({
setSelectedItem(null);
setPrice('');
setQuantity('1');
+ setVisibility('public');
+ setTargetCultivatorId('');
setError('');
};
@@ -451,6 +504,10 @@ export function ListItemModal({
setError(`数量范围为 1 ~ ${selectedItem.quantity}`);
return;
}
+ if (visibility === 'private' && !targetCultivatorId) {
+ setError('专属交易必须指定好友');
+ return;
+ }
setIsSubmitting(true);
setError('');
@@ -465,6 +522,9 @@ export function ListItemModal({
itemId: selectedItem.id,
price: priceNum,
quantity: quantityNum,
+ visibility,
+ targetCultivatorId:
+ visibility === 'private' ? targetCultivatorId : undefined,
}),
}),
);
@@ -1147,6 +1207,44 @@ export function ListItemModal({
)}
+
+ {
+ const nextVisibility = value as ListingVisibility;
+ setVisibility(nextVisibility);
+ if (nextVisibility === 'public') {
+ setTargetCultivatorId('');
+ }
+ setError('');
+ }}
+ >
+
+
+
+ {visibility === 'private' && (
+
+
+ {friends.map((friend) => (
+
+ ))}
+
+ )}
+
+
{error && {error}
}
@@ -1154,6 +1252,7 @@ export function ListItemModal({
· 寄售后物品将从储物袋中扣除
· 寄售时限为 48 小时
· 交易成功后扣除 10% 手续费
+
· 专属交易只向指定好友展示,并额外消耗拍卖行贵宾符,可在天骄宝阁购买
· 未售出的物品将通过邮件返还
diff --git a/src/react-app/components/feature/consumables/talismanDisplay.test.ts b/src/react-app/components/feature/consumables/talismanDisplay.test.ts
index 9fd8e4ac..67310a60 100644
--- a/src/react-app/components/feature/consumables/talismanDisplay.test.ts
+++ b/src/react-app/components/feature/consumables/talismanDisplay.test.ts
@@ -1,6 +1,12 @@
+import {
+ AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO,
+ FRIEND_MAIL_TALISMAN_SCENARIO,
+} from '@shared/config/socialConfig';
import type { Consumable } from '@shared/types/cultivator';
import {
buildTalismanDetailText,
+ getTalismanActionHref,
+ getTalismanActionLabel,
getTalismanUsageHint,
isQiRestoreTalisman,
} from './talismanDisplay';
@@ -47,4 +53,30 @@ describe('talisman display helpers', () => {
expect(buildTalismanDetailText(item)).not.toContain('qi_restore_medium');
expect(buildTalismanDetailText(item)).not.toContain('consume_on_action');
});
+
+ it('routes friend mail talismans to the mail scene with a purchase hint', () => {
+ const item = talisman(FRIEND_MAIL_TALISMAN_SCENARIO);
+
+ expect(getTalismanActionHref(item)).toBe('/game/mail');
+ expect(getTalismanActionLabel(item)).toBe('去传音');
+ expect(getTalismanUsageHint(item)).toContain('传音玉简');
+ expect(getTalismanUsageHint(item)).toContain('天骄宝阁');
+ expect(buildTalismanDetailText(item)).toContain('传音玉简·好友传音');
+ expect(buildTalismanDetailText(item)).not.toContain(
+ FRIEND_MAIL_TALISMAN_SCENARIO,
+ );
+ });
+
+ it('routes private auction talismans to the auction scene with a purchase hint', () => {
+ const item = talisman(AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO);
+
+ expect(getTalismanActionHref(item)).toBe('/game/auction');
+ expect(getTalismanActionLabel(item)).toBe('去上架');
+ expect(getTalismanUsageHint(item)).toContain('拍卖行');
+ expect(getTalismanUsageHint(item)).toContain('天骄宝阁');
+ expect(buildTalismanDetailText(item)).toContain('拍卖行·专属交易');
+ expect(buildTalismanDetailText(item)).not.toContain(
+ AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO,
+ );
+ });
});
diff --git a/src/react-app/components/feature/consumables/talismanDisplay.ts b/src/react-app/components/feature/consumables/talismanDisplay.ts
index 89a99e0e..839dac20 100644
--- a/src/react-app/components/feature/consumables/talismanDisplay.ts
+++ b/src/react-app/components/feature/consumables/talismanDisplay.ts
@@ -2,6 +2,10 @@ import {
QI_RESTORE_TALISMAN_SCENARIOS,
isQiRestoreTalismanScenario,
} from '@shared/config/qiSystem';
+import {
+ AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO,
+ FRIEND_MAIL_TALISMAN_SCENARIO,
+} from '@shared/config/socialConfig';
import { isTalismanConsumable } from '@shared/lib/consumables';
import type { Consumable } from '@shared/types/cultivator';
import { buildManualDrawHref } from '@shared/types/manualDraw';
@@ -10,24 +14,34 @@ const TALISMAN_SCENARIO_LABELS: Record = {
fate_reshape: '命格重塑',
draw_gongfa: '问法寻卷·功法抽取',
draw_skill: '问法寻卷·神通抽取',
+ [FRIEND_MAIL_TALISMAN_SCENARIO]: '传音玉简·好友传音',
+ [AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO]: '拍卖行·专属交易',
};
const TALISMAN_SCENARIO_HREFS: Record = {
fate_reshape: '/game/fate-reshape',
draw_gongfa: buildManualDrawHref('gongfa'),
draw_skill: buildManualDrawHref('skill'),
+ [FRIEND_MAIL_TALISMAN_SCENARIO]: '/game/mail',
+ [AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO]: '/game/auction',
};
const TALISMAN_SCENARIO_ACTION_LABELS: Record = {
fate_reshape: '前往重塑',
draw_gongfa: '抽功法秘籍',
draw_skill: '抽神通秘籍',
+ [FRIEND_MAIL_TALISMAN_SCENARIO]: '去传音',
+ [AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO]: '去上架',
};
const TALISMAN_USAGE_HINTS: Record = {
fate_reshape: '【前往命格重塑功能页启封,开启时立即扣除】',
draw_gongfa: '【前往问法寻卷,直接消耗符箓抽取功法秘籍】',
draw_skill: '【前往问法寻卷,直接消耗符箓抽取神通秘籍】',
+ [FRIEND_MAIL_TALISMAN_SCENARIO]:
+ '【前往传音玉简,给好友发送传音时消耗;不足时可去天骄宝阁购买】',
+ [AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO]:
+ '【前往拍卖行,上架专属交易时消耗;不足时可去天骄宝阁购买】',
};
function getQiRestoreEffectText(scenario: string): string | null {
diff --git a/src/react-app/routes/game/auction/route.tsx b/src/react-app/routes/game/auction/route.tsx
index 670388ec..af84581e 100644
--- a/src/react-app/routes/game/auction/route.tsx
+++ b/src/react-app/routes/game/auction/route.tsx
@@ -41,6 +41,9 @@ type AuctionListing = {
itemId: string;
itemSnapshot: Material | Artifact | Consumable;
price: number;
+ visibility?: 'public' | 'private';
+ targetCultivatorId?: string | null;
+ targetCultivatorName?: string | null;
status: string;
createdAt: string;
expiresAt: string;
@@ -335,6 +338,14 @@ export default function AuctionPage() {
{listing.sellerId === cultivator?.id ? ' (我)' : ''}
数量: x{listedQuantity}
+ {listing.visibility === 'private' && (
+
+ 专属:
+ {listing.targetCultivatorId === cultivator?.id
+ ? '指定给我'
+ : listing.targetCultivatorName || '指定道友'}
+
+ )}
{SPIRIT_STONES_INFO.icon} {listing.price}{' '}
{SPIRIT_STONES_INFO.label}
diff --git a/src/react-app/routes/game/mail/route.tsx b/src/react-app/routes/game/mail/route.tsx
index 8656f30b..65b62002 100644
--- a/src/react-app/routes/game/mail/route.tsx
+++ b/src/react-app/routes/game/mail/route.tsx
@@ -2,16 +2,126 @@ import {
GameSceneAsideSection,
GameSceneFrame,
} from '@app/components/game-shell';
+import { InkModal } from '@app/components/layout';
import { MailDetailModal } from '@app/components/mail/MailDetailModal';
import { Mail, MailList } from '@app/components/mail/MailList';
import { useInkUI } from '@app/components/providers/InkUIProvider';
import { InkButton } from '@app/components/ui/InkButton';
+import { InkInput } from '@app/components/ui/InkInput';
+import { InkNotice } from '@app/components/ui/InkNotice';
+import { InkSelect } from '@app/components/ui/InkSelect';
+import { InkBadge, InkList, InkTabs } from '@app/components/ui';
+import { usePlayerStateView } from '@app/lib/player-state/selectors';
import { usePlayerStateActions } from '@app/lib/player-state/store';
-import { useCallback, useEffect, useState } from 'react';
+import { isPillConsumable } from '@shared/lib/consumables';
+import { QUALITY_ORDER, type Quality } from '@shared/types/constants';
+import type { Artifact, Consumable, Material } from '@shared/types/cultivator';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { useSearchParams } from 'react-router';
const PAGE_SIZE = 20;
+const MIN_TRANSFER_QUALITY = '玄品';
+const TRANSFER_ALLOWED_QUALITIES = Object.keys(QUALITY_ORDER).filter(
+ (quality) =>
+ QUALITY_ORDER[quality as keyof typeof QUALITY_ORDER] >=
+ QUALITY_ORDER[MIN_TRANSFER_QUALITY],
+) as Quality[];
+
+type FriendSummary = {
+ id: string;
+ name: string;
+ title: string | null;
+ realm: string;
+ realmStage: string;
+ status: string;
+};
+
+type AttachmentOption = {
+ key: string;
+ itemType: 'material' | 'artifact' | 'consumable';
+ itemId: string;
+ name: string;
+ quantity: number;
+ qualityLabel: string;
+};
+
+type AttachmentItemType = AttachmentOption['itemType'];
+type SelectableAttachment = (Material | Artifact | Consumable) & {
+ itemType: AttachmentItemType;
+};
+
+type InventoryApiPayload = {
+ success: boolean;
+ data?: {
+ items?: Array;
+ pagination?: InventoryPagination;
+ };
+ error?: string;
+};
+
+type InventoryPagination = {
+ page: number;
+ pageSize: number;
+ total: number;
+ totalPages: number;
+ hasMore: boolean;
+};
+
+const defaultAttachmentPagination: InventoryPagination = {
+ page: 1,
+ pageSize: PAGE_SIZE,
+ total: 0,
+ totalPages: 0,
+ hasMore: false,
+};
+
+function isTransferableQuality(quality: string | undefined): boolean {
+ return Boolean(
+ quality && quality in QUALITY_ORDER &&
+ QUALITY_ORDER[quality as keyof typeof QUALITY_ORDER] >=
+ QUALITY_ORDER[MIN_TRANSFER_QUALITY],
+ );
+}
+
+function getAttachmentQuality(item: SelectableAttachment): Quality {
+ if (item.itemType === 'material') {
+ return (item as Material).rank;
+ }
+
+ const quality = (item as Artifact | Consumable).quality || '凡品';
+ return quality in QUALITY_ORDER ? quality : '凡品';
+}
+
+function getAttachmentUnsupportedReason(item: SelectableAttachment): string | null {
+ if (item.itemType === 'artifact' && (item as Artifact).isEquipped) {
+ return '已装备法宝不可附带';
+ }
+ if (item.itemType === 'consumable' && !isPillConsumable(item as Consumable)) {
+ return '当前仅支持丹药附带';
+ }
+ if (!isTransferableQuality(getAttachmentQuality(item))) {
+ return `仅玄品及以上物品可附带,当前为${getAttachmentQuality(item)}`;
+ }
+ return null;
+}
+
+function toAttachmentOption(item: SelectableAttachment): AttachmentOption | null {
+ if (!item.id || getAttachmentUnsupportedReason(item)) {
+ return null;
+ }
+
+ return {
+ key: `${item.itemType}:${item.id}`,
+ itemType: item.itemType,
+ itemId: item.id,
+ name: item.name,
+ quantity: item.itemType === 'artifact' ? 1 : (item as Material | Consumable).quantity,
+ qualityLabel: getAttachmentQuality(item),
+ };
+}
export default function MailPage() {
+ const { cultivator } = usePlayerStateView();
const [mails, setMails] = useState([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
@@ -20,9 +130,138 @@ export default function MailPage() {
const [selectedMail, setSelectedMail] = useState(null);
const [batchClaiming, setBatchClaiming] = useState(false);
const [batchReading, setBatchReading] = useState(false);
+ const [friends, setFriends] = useState([]);
+ const [friendsLoading, setFriendsLoading] = useState(true);
+ const [inviteTarget, setInviteTarget] = useState(null);
+ const [inviteAlreadyFriend, setInviteAlreadyFriend] = useState(false);
+ const [inviteLoading, setInviteLoading] = useState(false);
+ const [showSendModal, setShowSendModal] = useState(false);
+ const [showAttachmentPicker, setShowAttachmentPicker] = useState(false);
+ const [sending, setSending] = useState(false);
+ const [recipientId, setRecipientId] = useState('');
+ const [content, setContent] = useState('');
+ const [activeAttachmentType, setActiveAttachmentType] =
+ useState('material');
+ const [selectedAttachment, setSelectedAttachment] =
+ useState(null);
+ const [attachmentQuantity, setAttachmentQuantity] = useState('1');
+ const [attachmentItems, setAttachmentItems] = useState<
+ Record
+ >({
+ material: [],
+ artifact: [],
+ consumable: [],
+ });
+ const [attachmentPagination, setAttachmentPagination] = useState<
+ Record
+ >({
+ material: defaultAttachmentPagination,
+ artifact: defaultAttachmentPagination,
+ consumable: defaultAttachmentPagination,
+ });
+ const [attachmentLoading, setAttachmentLoading] = useState(false);
+ const [attachmentError, setAttachmentError] = useState('');
+ const [searchParams, setSearchParams] = useSearchParams();
+ const attachmentRequestIdRef = useRef(0);
const { mutate } = usePlayerStateActions();
const { pushToast } = useInkUI();
+ const clearInviteParam = useCallback(() => {
+ setSearchParams((prev) => {
+ const next = new URLSearchParams(prev);
+ next.delete('addFriend');
+ return next;
+ });
+ }, [setSearchParams]);
+
+ const fetchFriends = useCallback(async (options: { showLoading?: boolean } = {}) => {
+ try {
+ if (options.showLoading) {
+ setFriendsLoading(true);
+ }
+ const res = await fetch('/api/friends');
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || '获取好友名录失败');
+ }
+ const nextFriends = (data.friends || []) as FriendSummary[];
+ setFriends(nextFriends);
+ setRecipientId((current) => current || nextFriends[0]?.id || '');
+ } catch (error) {
+ pushToast({
+ message: error instanceof Error ? error.message : '获取好友名录失败',
+ tone: 'warning',
+ });
+ } finally {
+ if (options.showLoading) {
+ setFriendsLoading(false);
+ }
+ }
+ }, [pushToast]);
+
+ const fetchAttachmentPage = useCallback(
+ async (itemType: AttachmentItemType, pageNumber: number) => {
+ if (!cultivator?.id) return;
+
+ const requestId = ++attachmentRequestIdRef.current;
+ setAttachmentLoading(true);
+ setAttachmentError('');
+
+ try {
+ const apiType =
+ itemType === 'material'
+ ? 'materials'
+ : itemType === 'artifact'
+ ? 'artifacts'
+ : 'consumables';
+ const params = new URLSearchParams({
+ type: apiType,
+ page: String(Math.max(1, pageNumber)),
+ pageSize: String(PAGE_SIZE),
+ });
+
+ if (itemType === 'material') {
+ params.set('materialRanks', TRANSFER_ALLOWED_QUALITIES.join(','));
+ params.set('materialSortBy', 'rank');
+ params.set('materialSortOrder', 'desc');
+ }
+
+ const res = await fetch(
+ `/api/cultivator/inventory?${params.toString()}`,
+ );
+ const result = (await res.json()) as InventoryApiPayload;
+ if (!res.ok || !result.success) {
+ throw new Error(result.error || '读取背包失败');
+ }
+ if (requestId !== attachmentRequestIdRef.current) return;
+
+ const mappedItems = (result.data?.items || []).map((item) => ({
+ ...item,
+ itemType,
+ })) as SelectableAttachment[];
+ setAttachmentItems((prev) => ({
+ ...prev,
+ [itemType]: mappedItems,
+ }));
+ setAttachmentPagination((prev) => ({
+ ...prev,
+ [itemType]: result.data?.pagination || defaultAttachmentPagination,
+ }));
+ } catch (error) {
+ if (requestId === attachmentRequestIdRef.current) {
+ setAttachmentError(
+ error instanceof Error ? error.message : '读取背包失败',
+ );
+ }
+ } finally {
+ if (requestId === attachmentRequestIdRef.current) {
+ setAttachmentLoading(false);
+ }
+ }
+ },
+ [cultivator?.id],
+ );
+
const fetchMails = useCallback(
async (targetPage: number, append: boolean) => {
try {
@@ -88,6 +327,73 @@ export default function MailPage() {
};
}, []);
+ useEffect(() => {
+ fetch('/api/friends')
+ .then(async (res) => {
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || '获取好友名录失败');
+ }
+ const nextFriends = (data.friends || []) as FriendSummary[];
+ setFriends(nextFriends);
+ setRecipientId((current) => current || nextFriends[0]?.id || '');
+ })
+ .catch((error) => {
+ pushToast({
+ message: error instanceof Error ? error.message : '获取好友名录失败',
+ tone: 'warning',
+ });
+ })
+ .finally(() => {
+ setFriendsLoading(false);
+ });
+ }, [pushToast]);
+
+ useEffect(() => {
+ const targetId = searchParams.get('addFriend');
+ if (!targetId || !cultivator) {
+ return;
+ }
+ if (targetId === cultivator.id) {
+ pushToast({ message: '不能将自己加入好友名录', tone: 'warning' });
+ clearInviteParam();
+ return;
+ }
+
+ let cancelled = false;
+ const loadInvite = async () => {
+ try {
+ setInviteLoading(true);
+ const res = await fetch(`/api/friends/invite/${targetId}`);
+ const data = await res.json();
+ if (cancelled) return;
+ if (!res.ok) {
+ throw new Error(data.error || '查询道友失败');
+ }
+ setInviteTarget(data.target);
+ setInviteAlreadyFriend(Boolean(data.isFriend));
+ } catch (error) {
+ if (!cancelled) {
+ pushToast({
+ message: error instanceof Error ? error.message : '查询道友失败',
+ tone: 'warning',
+ });
+ clearInviteParam();
+ }
+ } finally {
+ if (!cancelled) {
+ setInviteLoading(false);
+ }
+ }
+ };
+
+ void loadInvite();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [clearInviteParam, cultivator, pushToast, searchParams]);
+
const handleSelectMail = async (mail: Mail) => {
setSelectedMail(mail);
@@ -203,10 +509,127 @@ export default function MailPage() {
}
};
+ const handleCopyInviteLink = async () => {
+ if (!cultivator) return;
+ const link = `${window.location.origin}/game/mail?addFriend=${cultivator.id}`;
+ await navigator.clipboard.writeText(link);
+ pushToast({ message: '好友邀请链接已复制', tone: 'success' });
+ };
+
+ const handleOpenAttachmentPicker = () => {
+ setShowAttachmentPicker(true);
+ void fetchAttachmentPage(activeAttachmentType, 1);
+ };
+
+ const handleConfirmInvite = async () => {
+ if (!inviteTarget) return;
+ try {
+ const res = await fetch(`/api/friends/${inviteTarget.id}`, {
+ method: 'POST',
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || '添加道友失败');
+ }
+ pushToast({ message: '已加入好友名录', tone: 'success' });
+ setInviteTarget(null);
+ clearInviteParam();
+ await fetchFriends();
+ } catch (error) {
+ pushToast({
+ message: error instanceof Error ? error.message : '添加道友失败',
+ tone: 'danger',
+ });
+ }
+ };
+
+ const handleRemoveFriend = async (friendId: string) => {
+ try {
+ const res = await fetch(`/api/friends/${friendId}`, {
+ method: 'DELETE',
+ });
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || '移除道友失败');
+ }
+ setFriends((prev) => prev.filter((friend) => friend.id !== friendId));
+ if (recipientId === friendId) {
+ setRecipientId('');
+ }
+ pushToast({ message: '已移出好友名录', tone: 'success' });
+ } catch (error) {
+ pushToast({
+ message: error instanceof Error ? error.message : '移除道友失败',
+ tone: 'danger',
+ });
+ }
+ };
+
+ const handleSendMail = async () => {
+ if (!recipientId) {
+ pushToast({ message: '请选择收信道友', tone: 'warning' });
+ return;
+ }
+ if (!content.trim()) {
+ pushToast({ message: '请写下传音内容', tone: 'warning' });
+ return;
+ }
+
+ const quantity = Math.max(1, Number(attachmentQuantity) || 1);
+ const attachment =
+ selectedAttachment
+ ? {
+ itemType: selectedAttachment.itemType,
+ itemId: selectedAttachment.itemId,
+ quantity:
+ selectedAttachment.itemType === 'artifact'
+ ? 1
+ : Math.min(quantity, selectedAttachment.quantity),
+ }
+ : undefined;
+
+ try {
+ setSending(true);
+ await mutate(
+ fetch('/api/cultivator/mail/send', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ recipientCultivatorId: recipientId,
+ content,
+ attachment,
+ }),
+ }),
+ );
+ pushToast({ message: '传音已发出', tone: 'success' });
+ setShowSendModal(false);
+ setContent('');
+ setSelectedAttachment(null);
+ setAttachmentQuantity('1');
+ } catch (error) {
+ pushToast({
+ message: error instanceof Error ? error.message : '发送传音失败',
+ tone: 'danger',
+ });
+ } finally {
+ setSending(false);
+ }
+ };
+
const unreadCount = mails.filter((mail) => !mail.isRead).length;
const pendingAttachments = mails.filter(
(mail) => mail.type === 'reward' && !mail.isClaimed,
).length;
+ const attachmentTabs = [
+ { label: '材料', value: 'material' },
+ { label: '法宝', value: 'artifact' },
+ { label: '丹药', value: 'consumable' },
+ ];
+ const currentAttachmentItems = attachmentItems[activeAttachmentType]
+ .map(toAttachmentOption)
+ .filter((item): item is AttachmentOption => Boolean(item));
+ const currentAttachmentPagination =
+ attachmentPagination[activeAttachmentType];
return (
待领附件:{pendingAttachments} 封
+
+
+
+
+ 复制邀请链接
+
+ setShowSendModal(true)}
+ disabled={friends.length === 0}
+ >
+ 发送传音
+
+
+ {friendsLoading ? (
+
正在翻检名录...
+ ) : friends.length === 0 ? (
+
尚未收录道友。
+ ) : (
+
+ {friends.slice(0, 6).map((friend) => (
+
+
+
{friend.name}
+
+ {friend.realm} {friend.realmStage}
+
+
+
+
+ ))}
+
+ )}
+
+
+
setShowSendModal(true)}
+ disabled={friends.length === 0}
+ >
+ 发送传音
+
setSelectedMail(null)}
onUpdate={handleUpdate}
/>
+ setShowSendModal(false)}
+ title="发送传音"
+ >
+
+
+
+ {friends.map((friend) => (
+
+ ))}
+
+
+
+
+ 随附物品
+
+ {selectedAttachment ? (
+
+
+
+ {selectedAttachment.name}
+
+
+ {selectedAttachment.qualityLabel} · 可用{' '}
+ {selectedAttachment.quantity}
+
+
+
+
+ 更换
+
+ {
+ setSelectedAttachment(null);
+ setAttachmentQuantity('1');
+ }}
+ >
+ 移除
+
+
+
+ ) : (
+
+ 选择附件
+
+ )}
+
+ {selectedAttachment && selectedAttachment.itemType !== 'artifact' ? (
+
+ ) : null}
+
+ setShowSendModal(false)}>取消
+
+ {sending ? '发送中...' : '发出'}
+
+
+
+
+ setShowAttachmentPicker(false)}
+ title="选择附件"
+ className="max-w-2xl"
+ >
+
+
{
+ const nextType = value as AttachmentItemType;
+ setActiveAttachmentType(nextType);
+ setAttachmentError('');
+ void fetchAttachmentPage(nextType, 1);
+ }}
+ />
+ {attachmentError ? (
+ {attachmentError}
+ ) : null}
+ {attachmentLoading ? (
+
+ 正在翻检储物袋...
+
+ ) : currentAttachmentItems.length > 0 ? (
+
+ {currentAttachmentItems.map((item) => (
+
+
+
+ {item.name}
+
+
+ {item.qualityLabel} · 可用 {item.quantity}
+
+
+
{
+ setSelectedAttachment(item);
+ setAttachmentQuantity('1');
+ setShowAttachmentPicker(false);
+ }}
+ >
+ 选择
+
+
+ ))}
+
+ ) : (
+
+ {activeAttachmentType === 'material'
+ ? '暂无可附带材料(仅限玄品及以上)。'
+ : activeAttachmentType === 'artifact'
+ ? '暂无可附带法宝(仅限玄品及以上且未装备)。'
+ : '暂无可附带丹药(仅限玄品及以上)。'}
+
+ )}
+ {currentAttachmentPagination.totalPages > 1 ? (
+
+
+ void fetchAttachmentPage(
+ activeAttachmentType,
+ currentAttachmentPagination.page - 1,
+ )
+ }
+ >
+ 上一页
+
+
+ {currentAttachmentPagination.page} /{' '}
+ {currentAttachmentPagination.totalPages}
+
+ =
+ currentAttachmentPagination.totalPages
+ }
+ onClick={() =>
+ void fetchAttachmentPage(
+ activeAttachmentType,
+ currentAttachmentPagination.page + 1,
+ )
+ }
+ >
+ 下一页
+
+
+ ) : null}
+
+
+ {
+ setInviteTarget(null);
+ clearInviteParam();
+ }}
+ title="收录道友"
+ >
+ {inviteLoading ? (
+
+ 正在辨认玉简气息...
+
+ ) : inviteTarget ? (
+
+
+
+
{inviteTarget.name}
+
+ {inviteTarget.realm} {inviteTarget.realmStage}
+
+ {inviteTarget.title ? (
+
称号:{inviteTarget.title}
+ ) : null}
+
+
+
+ {
+ setInviteTarget(null);
+ clearInviteParam();
+ }}
+ >
+ 取消
+
+
+ {inviteAlreadyFriend ? '已在名录中' : '加入名录'}
+
+
+
+ ) : null}
+
);
}
diff --git a/src/server/lib/drizzle/schema.ts b/src/server/lib/drizzle/schema.ts
index c3807598..510340c5 100644
--- a/src/server/lib/drizzle/schema.ts
+++ b/src/server/lib/drizzle/schema.ts
@@ -511,6 +511,28 @@ export const mails = pgTable(
],
);
+// 好友名录:双向好友会写入两条记录,便于按当前角色快速查询
+export const cultivatorFriends = pgTable(
+ 'wanjiedaoyou_cultivator_friends',
+ {
+ id: uuid('id').primaryKey().defaultRandom(),
+ cultivatorId: uuid('cultivator_id')
+ .references(() => cultivators.id, { onDelete: 'cascade' })
+ .notNull(),
+ friendCultivatorId: uuid('friend_cultivator_id')
+ .references(() => cultivators.id, { onDelete: 'cascade' })
+ .notNull(),
+ createdAt: timestamp('created_at').defaultNow().notNull(),
+ },
+ (table) => [
+ uniqueIndex('cultivator_friends_pair_uidx').on(
+ table.cultivatorId,
+ table.friendCultivatorId,
+ ),
+ index('cultivator_friends_friend_idx').on(table.friendCultivatorId),
+ ],
+);
+
// 兑换码表
export const redeemCodes = pgTable(
'wanjiedaoyou_redeem_codes',
@@ -824,6 +846,12 @@ export const auctionListings = pgTable(
// 价格与状态
price: integer('price').notNull(), // 一口价(灵石)
status: varchar('status', { length: 20 }).notNull().default('active'), // active | sold | expired | cancelled
+ visibility: varchar('visibility', { length: 20 }).notNull().default('public'), // public | private
+ targetCultivatorId: uuid('target_cultivator_id').references(
+ () => cultivators.id,
+ { onDelete: 'set null' },
+ ),
+ targetCultivatorName: varchar('target_cultivator_name', { length: 100 }),
// 时间戳
createdAt: timestamp('created_at').defaultNow().notNull(),
@@ -851,6 +879,11 @@ export const auctionListings = pgTable(
table.expiresAt,
table.itemType,
),
+ index('auction_visibility_target_status_idx').on(
+ table.visibility,
+ table.targetCultivatorId,
+ table.status,
+ ),
],
);
diff --git a/src/server/lib/repositories/auctionRepository.ts b/src/server/lib/repositories/auctionRepository.ts
index f1b205db..76514d2b 100644
--- a/src/server/lib/repositories/auctionRepository.ts
+++ b/src/server/lib/repositories/auctionRepository.ts
@@ -1,4 +1,4 @@
-import { and, asc, desc, eq, gte, lte, sql, type SQL } from 'drizzle-orm';
+import { and, asc, desc, eq, gte, lte, or, sql, type SQL } from 'drizzle-orm';
import { getExecutor, type DbExecutor, type DbTransaction } from '../drizzle/db';
import * as schema from '../drizzle/schema';
@@ -17,6 +17,9 @@ export async function createListing(data: {
itemId: string;
itemSnapshot: unknown;
price: number;
+ visibility?: 'public' | 'private';
+ targetCultivatorId?: string;
+ targetCultivatorName?: string;
expiresAt: Date;
tx?: DbTransaction;
}): Promise {
@@ -30,6 +33,9 @@ export async function createListing(data: {
itemId: data.itemId,
itemSnapshot: data.itemSnapshot,
price: data.price,
+ visibility: data.visibility ?? 'public',
+ targetCultivatorId: data.targetCultivatorId,
+ targetCultivatorName: data.targetCultivatorName,
status: 'active',
expiresAt: data.expiresAt,
})
@@ -63,6 +69,7 @@ export interface FindActiveListingsOptions {
sortBy?: 'price_asc' | 'price_desc' | 'latest';
page?: number;
limit?: number;
+ viewerCultivatorId?: string;
}
export async function findActiveListings(
@@ -76,6 +83,7 @@ export async function findActiveListings(
sortBy = 'latest',
page = 1,
limit = 20,
+ viewerCultivatorId,
} = options;
// 构建筛选条件
@@ -93,6 +101,15 @@ export async function findActiveListings(
if (maxPrice !== undefined) {
conditions.push(lte(schema.auctionListings.price, maxPrice));
}
+ conditions.push(
+ viewerCultivatorId
+ ? or(
+ eq(schema.auctionListings.visibility, 'public'),
+ eq(schema.auctionListings.sellerId, viewerCultivatorId),
+ eq(schema.auctionListings.targetCultivatorId, viewerCultivatorId),
+ )!
+ : eq(schema.auctionListings.visibility, 'public'),
+ );
const whereClause = and(...conditions);
diff --git a/src/server/lib/repositories/retentionRepository.test.ts b/src/server/lib/repositories/retentionRepository.test.ts
new file mode 100644
index 00000000..efcdcb52
--- /dev/null
+++ b/src/server/lib/repositories/retentionRepository.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it, vi } from 'vitest';
+
+vi.mock('@server/lib/drizzle/db', () => ({
+ getExecutor: vi.fn(),
+}));
+
+import { pruneExpiredData } from './retentionRepository';
+
+function flattenSqlObject(value: unknown, seen = new Set()): string {
+ if (typeof value === 'string') return value;
+ if (!value || typeof value !== 'object' || seen.has(value)) return '';
+ seen.add(value);
+ if (Array.isArray(value)) {
+ return value.map((item) => flattenSqlObject(item, seen)).join(' ');
+ }
+ return Object.values(value as Record)
+ .map((item) => flattenSqlObject(item, seen))
+ .join(' ');
+}
+
+describe('retentionRepository', () => {
+ it('keeps old mails with unclaimed attachments when pruning expired data', async () => {
+ const returningMock = vi.fn().mockResolvedValue([]);
+ const whereMock = vi.fn(() => ({ returning: returningMock }));
+ const deleteMock = vi.fn(() => ({ where: whereMock }));
+ const q = { delete: deleteMock };
+ const cutoff = new Date('2026-06-01T00:00:00.000Z');
+
+ await pruneExpiredData(
+ {
+ mails: cutoff,
+ qiLogs: cutoff,
+ dungeonHistories: cutoff,
+ dungeonRuns: cutoff,
+ battleRecordsV2: cutoff,
+ reputationShopPurchases: cutoff,
+ auctionListings: cutoff,
+ },
+ q as any,
+ );
+
+ const mailWhereSql = flattenSqlObject((whereMock.mock.calls as unknown[][])[0]?.[0]);
+ expect(mailWhereSql).toContain('jsonb_array_length');
+ expect(mailWhereSql).toContain('is_claimed');
+ expect(mailWhereSql).toContain('attachments');
+ });
+});
diff --git a/src/server/lib/repositories/retentionRepository.ts b/src/server/lib/repositories/retentionRepository.ts
index 470a7292..99bba51d 100644
--- a/src/server/lib/repositories/retentionRepository.ts
+++ b/src/server/lib/repositories/retentionRepository.ts
@@ -9,7 +9,7 @@ import {
qiLogs,
reputationShopPurchases,
} from '@server/lib/drizzle/schema';
-import { and, inArray, lt, ne } from 'drizzle-orm';
+import { and, inArray, lt, ne, sql } from 'drizzle-orm';
export type ExpiredDataCleanupCutoffs = {
mails: Date;
@@ -46,7 +46,12 @@ export async function pruneExpiredData(
const mailsDeleted = await deleteExpiredRows(q, (executor) =>
executor
.delete(mails)
- .where(lt(mails.createdAt, cutoffs.mails))
+ .where(
+ and(
+ lt(mails.createdAt, cutoffs.mails),
+ sql`(${mails.isClaimed} = true OR ${mails.attachments} IS NULL OR jsonb_typeof(${mails.attachments}) <> 'array' OR jsonb_array_length(${mails.attachments}) = 0)`,
+ ),
+ )
.returning({ id: mails.id }),
);
diff --git a/src/server/lib/services/AuctionService.test.ts b/src/server/lib/services/AuctionService.test.ts
index d3e5a442..d70a03b4 100644
--- a/src/server/lib/services/AuctionService.test.ts
+++ b/src/server/lib/services/AuctionService.test.ts
@@ -7,6 +7,9 @@ const {
keysMock,
sendMailMock,
setMock,
+ assertFriendMock,
+ consumeFirstTalismanByScenarioMock,
+ getInviteTargetMock,
updateStatusMock,
} = vi.hoisted(() => ({
countActiveBySellerMock: vi.fn(),
@@ -17,6 +20,9 @@ const {
keysMock: vi.fn(),
sendMailMock: vi.fn(),
setMock: vi.fn(),
+ assertFriendMock: vi.fn(),
+ consumeFirstTalismanByScenarioMock: vi.fn(),
+ getInviteTargetMock: vi.fn(),
updateStatusMock: vi.fn(),
}));
@@ -46,6 +52,24 @@ vi.mock('@server/lib/services/MailService', () => ({
},
}));
+vi.mock('@server/lib/services/FriendService', () => ({
+ assertFriend: assertFriendMock,
+ FriendServiceError: class FriendServiceError extends Error {
+ constructor(
+ public readonly status: number,
+ message: string,
+ ) {
+ super(message);
+ }
+ },
+ getInviteTarget: getInviteTargetMock,
+}));
+
+vi.mock('@server/lib/services/TalismanScenarioService', () => ({
+ consumeFirstTalismanByScenario: consumeFirstTalismanByScenarioMock,
+ TalismanScenarioError: class TalismanScenarioError extends Error {},
+}));
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as schema from '../drizzle/schema';
import { buyItem, listItem } from './AuctionService';
@@ -216,6 +240,19 @@ describe('AuctionService', () => {
createListingMock.mockResolvedValue({ id: 'listing-1' });
updateStatusMock.mockResolvedValue(undefined);
sendMailMock.mockResolvedValue({ id: 'mail-1' });
+ assertFriendMock.mockResolvedValue(undefined);
+ consumeFirstTalismanByScenarioMock.mockResolvedValue(undefined);
+ getInviteTargetMock.mockResolvedValue({
+ isFriend: true,
+ target: {
+ id: 'target-1',
+ name: '南宫婉',
+ title: null,
+ realm: '筑基',
+ realmStage: '初期',
+ status: 'active',
+ },
+ });
});
it('allows listing pills and preserves their full spec in the auction snapshot', async () => {
@@ -225,6 +262,7 @@ describe('AuctionService', () => {
getExecutorMock.mockReturnValue(executor);
await listItem({
+ userId: 'user-1',
cultivatorId: 'cultivator-1',
cultivatorName: '韩立',
itemType: 'consumable',
@@ -270,6 +308,7 @@ describe('AuctionService', () => {
await expect(
listItem({
+ userId: 'user-1',
cultivatorId: 'cultivator-1',
cultivatorName: '韩立',
itemType: 'consumable',
@@ -281,6 +320,48 @@ describe('AuctionService', () => {
expect(createListingMock).not.toHaveBeenCalled();
});
+ it('consumes a private listing talisman and stores the target friend snapshot', async () => {
+ const { executor } = createExecutor({
+ consumableRow: createConsumableRow(),
+ });
+ getExecutorMock.mockReturnValue(executor);
+
+ await listItem({
+ userId: 'user-1',
+ cultivatorId: 'cultivator-1',
+ cultivatorName: '韩立',
+ itemType: 'consumable',
+ itemId: 'consumable-1',
+ price: 120,
+ quantity: 1,
+ visibility: 'private',
+ targetCultivatorId: 'target-1',
+ });
+
+ expect(getInviteTargetMock).toHaveBeenCalledWith(
+ 'cultivator-1',
+ 'target-1',
+ executor,
+ );
+ expect(assertFriendMock).toHaveBeenCalledWith(
+ 'cultivator-1',
+ 'target-1',
+ executor,
+ );
+ expect(consumeFirstTalismanByScenarioMock).toHaveBeenCalledWith(
+ 'cultivator-1',
+ 'auction_private_listing',
+ executor,
+ );
+ expect(createListingMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ visibility: 'private',
+ targetCultivatorId: 'target-1',
+ targetCultivatorName: '南宫婉',
+ }),
+ );
+ });
+
it('sends the buyer a consumable attachment whose pill spec matches the sold snapshot', async () => {
const pillSnapshot = {
id: 'consumable-1',
diff --git a/src/server/lib/services/AuctionService.ts b/src/server/lib/services/AuctionService.ts
index 1542623a..9e6d51ba 100644
--- a/src/server/lib/services/AuctionService.ts
+++ b/src/server/lib/services/AuctionService.ts
@@ -5,6 +5,7 @@ import {
TEMP_DISABLED_MESSAGES,
temporaryRestrictions,
} from '@shared/config/temporaryRestrictions';
+import { AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO } from '@shared/config/socialConfig';
import { isPillConsumable } from '@shared/lib/consumables';
import { QUALITY_ORDER, type Quality } from '@shared/types/constants';
import type { Artifact, Consumable, Material } from '@shared/types/cultivator';
@@ -14,6 +15,15 @@ import * as schema from '../drizzle/schema';
import { MailService } from './MailService';
import { mapConsumableRow } from './consumablePersistence';
import { toArtifactFromProduct } from './creationProductArtifactSupport';
+import {
+ assertFriend,
+ FriendServiceError,
+ getInviteTarget,
+} from './FriendService';
+import {
+ consumeFirstTalismanByScenario,
+ TalismanScenarioError,
+} from './TalismanScenarioService';
// ============================================================================
// Constants
@@ -59,10 +69,17 @@ export const AuctionError = {
INVALID_ITEM_QUALITY: 'INVALID_ITEM_QUALITY',
CONSUMABLE_LISTING_DISABLED: 'CONSUMABLE_LISTING_DISABLED',
SAME_OWNER: 'SAME_OWNER',
+ INVALID_VISIBILITY: 'INVALID_VISIBILITY',
+ TARGET_NOT_FRIEND: 'TARGET_NOT_FRIEND',
+ MISSING_TALISMAN: 'MISSING_TALISMAN',
+ NOT_TARGET_BUYER: 'NOT_TARGET_BUYER',
} as const;
export type AuctionErrorCode = (typeof AuctionError)[keyof typeof AuctionError];
+export type AuctionItemType = 'material' | 'artifact' | 'consumable';
+export type AuctionListingVisibility = 'public' | 'private';
+
export class AuctionServiceError extends Error {
constructor(
public code: AuctionErrorCode,
@@ -78,12 +95,15 @@ export class AuctionServiceError extends Error {
// ============================================================================
export interface ListItemInput {
+ userId: string;
cultivatorId: string;
cultivatorName: string;
- itemType: 'material' | 'artifact' | 'consumable';
+ itemType: AuctionItemType;
itemId: string;
price: number;
quantity: number;
+ visibility?: AuctionListingVisibility;
+ targetCultivatorId?: string;
}
export interface ListItemResult {
@@ -123,8 +143,8 @@ async function getArtifactProductSnapshot(
return rows[0] || null;
}
-async function getItemSnapshot(
- itemType: 'material' | 'artifact' | 'consumable',
+export async function getAuctionItemSnapshot(
+ itemType: AuctionItemType,
itemId: string,
cultivatorId: string,
executor?: DbExecutor,
@@ -183,8 +203,8 @@ export async function clearAuctionListingsCache(): Promise {
}
}
-function normalizeItemQuality(
- itemType: 'material' | 'artifact' | 'consumable',
+export function normalizeAuctionItemQuality(
+ itemType: AuctionItemType,
item: Material | Artifact | Consumable,
): Quality {
if (itemType === 'material') {
@@ -195,10 +215,60 @@ function normalizeItemQuality(
return quality in QUALITY_ORDER ? quality : '凡品';
}
-function isAuctionListableQuality(quality: Quality): boolean {
+export function isAuctionListableQuality(quality: Quality): boolean {
return QUALITY_ORDER[quality] >= QUALITY_ORDER[AUCTION_MIN_QUALITY];
}
+export function assertAuctionListableItem(
+ itemType: AuctionItemType,
+ itemSnapshot: Material | Artifact | Consumable,
+ quantity: number,
+): void {
+ if ((itemSnapshot as Artifact).isEquipped) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_ITEM_TYPE,
+ '已装备法宝不可寄售,请先卸下',
+ );
+ }
+ if (
+ itemType === 'consumable' &&
+ !isPillConsumable(itemSnapshot as Consumable)
+ ) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_ITEM_TYPE,
+ '当前仅支持丹药寄售',
+ );
+ }
+
+ const itemQuality = normalizeAuctionItemQuality(itemType, itemSnapshot);
+ if (!isAuctionListableQuality(itemQuality)) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_ITEM_QUALITY,
+ `仅玄品及以上物品可寄售,当前为${itemQuality}`,
+ );
+ }
+
+ if (itemType === 'artifact' && quantity !== 1) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_QUANTITY,
+ '法宝每次只能上架 1 件',
+ );
+ }
+
+ const availableQuantity =
+ itemType === 'artifact'
+ ? 1
+ : 'quantity' in itemSnapshot
+ ? itemSnapshot.quantity
+ : 0;
+ if (itemType !== 'artifact' && quantity > availableQuantity) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_QUANTITY,
+ `上架数量不足,当前仅有 ${availableQuantity}`,
+ );
+ }
+}
+
// ============================================================================
// Main Service Methods
// ============================================================================
@@ -211,8 +281,17 @@ export async function listItem(
options: AuctionMutationOptions = {},
): Promise {
const q = getExecutor(options.tx);
- const { cultivatorId, cultivatorName, itemType, itemId, price, quantity } =
- input;
+ const {
+ cultivatorId,
+ cultivatorName,
+ itemType,
+ itemId,
+ price,
+ quantity,
+ targetCultivatorId,
+ } = input;
+ const visibility = input.visibility ?? 'public';
+ let targetCultivatorName: string | undefined;
// 1. 校验价格(全局上限)
if (price < 1) {
@@ -244,6 +323,41 @@ export async function listItem(
);
}
+ if (!['public', 'private'].includes(visibility)) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_VISIBILITY,
+ '无效的拍卖可见范围',
+ );
+ }
+ if (visibility === 'private') {
+ if (!targetCultivatorId) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_VISIBILITY,
+ '专属交易必须指定好友',
+ );
+ }
+ try {
+ const target = await getInviteTarget(cultivatorId, targetCultivatorId, q);
+ if (!target.isFriend) {
+ throw new AuctionServiceError(
+ AuctionError.TARGET_NOT_FRIEND,
+ '专属交易只能指定好友名录中的道友',
+ );
+ }
+ targetCultivatorName = target.target.name;
+ } catch (error) {
+ if (error instanceof FriendServiceError) {
+ throw new AuctionServiceError(
+ error.status === 403
+ ? AuctionError.TARGET_NOT_FRIEND
+ : AuctionError.ITEM_NOT_FOUND,
+ error.message,
+ );
+ }
+ throw error;
+ }
+ }
+
if (
temporaryRestrictions.disableConsumableAuctionListing &&
itemType === 'consumable'
@@ -279,39 +393,20 @@ export async function listItem(
}
// 6. 获取物品快照并校验所有权
- const artifactProduct =
- itemType === 'artifact'
- ? await getArtifactProductSnapshot(itemId, cultivatorId, q)
- : null;
- const itemSnapshot = await getItemSnapshot(itemType, itemId, cultivatorId, q);
+ const itemSnapshot = await getAuctionItemSnapshot(
+ itemType,
+ itemId,
+ cultivatorId,
+ q,
+ );
if (!itemSnapshot) {
throw new AuctionServiceError(
AuctionError.ITEM_NOT_FOUND,
'物品不存在或已消耗',
);
}
- if (artifactProduct?.isEquipped) {
- throw new AuctionServiceError(
- AuctionError.INVALID_ITEM_TYPE,
- '已装备法宝不可寄售,请先卸下',
- );
- }
- if (
- itemType === 'consumable' &&
- !isPillConsumable(itemSnapshot as Consumable)
- ) {
- throw new AuctionServiceError(
- AuctionError.INVALID_ITEM_TYPE,
- '当前仅支持丹药寄售',
- );
- }
- const itemQuality = normalizeItemQuality(itemType, itemSnapshot);
- if (!isAuctionListableQuality(itemQuality)) {
- throw new AuctionServiceError(
- AuctionError.INVALID_ITEM_QUALITY,
- `仅玄品及以上物品可寄售,当前为${itemQuality}`,
- );
- }
+ assertAuctionListableItem(itemType, itemSnapshot, quantity);
+ const itemQuality = normalizeAuctionItemQuality(itemType, itemSnapshot);
// 按品质校验价格上限
const qualityCap = QUALITY_PRICE_CAPS[itemQuality];
@@ -322,32 +417,13 @@ export async function listItem(
);
}
- const availableQuantity =
- itemType === 'artifact'
- ? 1
- : 'quantity' in itemSnapshot
- ? itemSnapshot.quantity
- : 0;
- if (itemType === 'artifact' && quantity !== 1) {
- throw new AuctionServiceError(
- AuctionError.INVALID_QUANTITY,
- '法宝每次只能上架 1 件',
- );
- }
- if (itemType !== 'artifact' && quantity > availableQuantity) {
- throw new AuctionServiceError(
- AuctionError.INVALID_QUANTITY,
- `上架数量不足,当前仅有 ${availableQuantity}`,
- );
- }
-
// 7. 在事务中:扣减/删除物品 + 创建拍卖记录
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + LISTING_DURATION_HOURS);
const persistListing = async (tx: DbTransaction) => {
// 事务内二次校验并按数量扣减
- const ownedItem = await getItemSnapshot(
+ const ownedItem = await getAuctionItemSnapshot(
itemType,
itemId,
cultivatorId,
@@ -359,14 +435,37 @@ export async function listItem(
'物品不存在或已被消耗',
);
}
- if (
- itemType === 'consumable' &&
- !isPillConsumable(ownedItem as Consumable)
- ) {
- throw new AuctionServiceError(
- AuctionError.INVALID_ITEM_TYPE,
- '当前仅支持丹药寄售',
- );
+ assertAuctionListableItem(itemType, ownedItem, quantity);
+
+ if (visibility === 'private') {
+ if (!targetCultivatorId) {
+ throw new AuctionServiceError(
+ AuctionError.INVALID_VISIBILITY,
+ '专属交易必须指定好友',
+ );
+ }
+ try {
+ await assertFriend(cultivatorId, targetCultivatorId, tx);
+ await consumeFirstTalismanByScenario(
+ cultivatorId,
+ AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO,
+ tx,
+ );
+ } catch (error) {
+ if (error instanceof FriendServiceError) {
+ throw new AuctionServiceError(
+ AuctionError.TARGET_NOT_FRIEND,
+ error.message,
+ );
+ }
+ if (error instanceof TalismanScenarioError) {
+ throw new AuctionServiceError(
+ AuctionError.MISSING_TALISMAN,
+ '缺少拍卖行贵宾符,可前往天骄宝阁购买后再上架专属交易',
+ );
+ }
+ throw error;
+ }
}
const listingSnapshot =
@@ -470,6 +569,11 @@ export async function listItem(
itemId,
itemSnapshot: listingSnapshot,
price,
+ visibility,
+ targetCultivatorId:
+ visibility === 'private' ? targetCultivatorId : undefined,
+ targetCultivatorName:
+ visibility === 'private' ? targetCultivatorName : undefined,
expiresAt,
tx,
});
@@ -547,6 +651,15 @@ export async function buyItem(
'无法购买自己寄售的物品',
);
}
+ if (
+ listing.visibility === 'private' &&
+ listing.targetCultivatorId !== buyerCultivatorId
+ ) {
+ throw new AuctionServiceError(
+ AuctionError.NOT_TARGET_BUYER,
+ '此物为专属交易,不可购买',
+ );
+ }
const price = listing.price;
const feeAmount = Math.floor(price * FEE_RATE);
diff --git a/src/server/lib/services/FriendService.test.ts b/src/server/lib/services/FriendService.test.ts
new file mode 100644
index 00000000..26706ce6
--- /dev/null
+++ b/src/server/lib/services/FriendService.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it, vi } from 'vitest';
+
+vi.mock('@server/lib/drizzle/db', () => ({
+ getExecutor: vi.fn(),
+}));
+
+import { addFriendPair, FriendServiceError } from './FriendService';
+
+function createFriendTx() {
+ let selectCount = 0;
+ const valuesMock = vi.fn(() => ({ onConflictDoNothing: vi.fn() }));
+ return {
+ select: vi.fn(() => ({
+ from: vi.fn(() => ({
+ where: vi.fn(() => ({
+ limit: vi.fn(async () => {
+ selectCount += 1;
+ return [
+ selectCount === 1
+ ? {
+ id: 'cultivator-1',
+ name: '韩立',
+ title: null,
+ realm: '筑基',
+ realm_stage: '初期',
+ status: 'active',
+ }
+ : {
+ id: 'cultivator-2',
+ name: '南宫婉',
+ title: null,
+ realm: '结丹',
+ realm_stage: '中期',
+ status: 'active',
+ },
+ ];
+ }),
+ })),
+ })),
+ })),
+ insert: vi.fn(() => ({ values: valuesMock })),
+ valuesMock,
+ };
+}
+
+describe('FriendService', () => {
+ it('adds both directions for a friend pair', async () => {
+ const tx = createFriendTx();
+
+ await expect(
+ addFriendPair('cultivator-1', 'cultivator-2', tx as any),
+ ).resolves.toEqual(
+ expect.objectContaining({ id: 'cultivator-2', name: '南宫婉' }),
+ );
+
+ expect(tx.valuesMock).toHaveBeenCalledWith([
+ {
+ cultivatorId: 'cultivator-1',
+ friendCultivatorId: 'cultivator-2',
+ },
+ {
+ cultivatorId: 'cultivator-2',
+ friendCultivatorId: 'cultivator-1',
+ },
+ ]);
+ });
+
+ it('rejects adding self', async () => {
+ await expect(addFriendPair('cultivator-1', 'cultivator-1')).rejects.toBeInstanceOf(
+ FriendServiceError,
+ );
+ });
+});
diff --git a/src/server/lib/services/FriendService.ts b/src/server/lib/services/FriendService.ts
new file mode 100644
index 00000000..7606431e
--- /dev/null
+++ b/src/server/lib/services/FriendService.ts
@@ -0,0 +1,228 @@
+import {
+ getExecutor,
+ type DbExecutor,
+ type DbTransaction,
+} from '@server/lib/drizzle/db';
+import * as schema from '@server/lib/drizzle/schema';
+import { and, eq, inArray, or } from 'drizzle-orm';
+
+export type FriendCultivatorSummary = {
+ id: string;
+ name: string;
+ title: string | null;
+ realm: string;
+ realmStage: string;
+ status: string;
+};
+
+export class FriendServiceError extends Error {
+ constructor(
+ public readonly status: number,
+ message: string,
+ ) {
+ super(message);
+ this.name = 'FriendServiceError';
+ }
+}
+
+function toSummary(
+ row: Pick<
+ typeof schema.cultivators.$inferSelect,
+ 'id' | 'name' | 'title' | 'realm' | 'realm_stage' | 'status'
+ >,
+): FriendCultivatorSummary {
+ return {
+ id: row.id,
+ name: row.name,
+ title: row.title,
+ realm: row.realm,
+ realmStage: row.realm_stage,
+ status: row.status,
+ };
+}
+
+async function getActiveCultivatorSummary(
+ cultivatorId: string,
+ q: DbExecutor | DbTransaction,
+): Promise {
+ const [row] = await q
+ .select({
+ id: schema.cultivators.id,
+ name: schema.cultivators.name,
+ title: schema.cultivators.title,
+ realm: schema.cultivators.realm,
+ realm_stage: schema.cultivators.realm_stage,
+ status: schema.cultivators.status,
+ })
+ .from(schema.cultivators)
+ .where(
+ and(
+ eq(schema.cultivators.id, cultivatorId),
+ eq(schema.cultivators.status, 'active'),
+ ),
+ )
+ .limit(1);
+
+ return row ? toSummary(row) : null;
+}
+
+export async function areFriends(
+ cultivatorId: string,
+ friendCultivatorId: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ const q = executor ?? getExecutor();
+ const [row] = await q
+ .select({ id: schema.cultivatorFriends.id })
+ .from(schema.cultivatorFriends)
+ .where(
+ and(
+ eq(schema.cultivatorFriends.cultivatorId, cultivatorId),
+ eq(schema.cultivatorFriends.friendCultivatorId, friendCultivatorId),
+ ),
+ )
+ .limit(1);
+ return Boolean(row);
+}
+
+export async function listFriends(
+ cultivatorId: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ const q = executor ?? getExecutor();
+ const rows = await q
+ .select({
+ id: schema.cultivators.id,
+ name: schema.cultivators.name,
+ title: schema.cultivators.title,
+ realm: schema.cultivators.realm,
+ realm_stage: schema.cultivators.realm_stage,
+ status: schema.cultivators.status,
+ })
+ .from(schema.cultivatorFriends)
+ .innerJoin(
+ schema.cultivators,
+ eq(schema.cultivatorFriends.friendCultivatorId, schema.cultivators.id),
+ )
+ .where(eq(schema.cultivatorFriends.cultivatorId, cultivatorId));
+
+ return rows.map(toSummary);
+}
+
+export async function getInviteTarget(
+ currentCultivatorId: string,
+ targetCultivatorId: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise<{ target: FriendCultivatorSummary; isFriend: boolean }> {
+ const q = executor ?? getExecutor();
+ if (currentCultivatorId === targetCultivatorId) {
+ throw new FriendServiceError(400, '不能将自己加入好友名录');
+ }
+
+ const target = await getActiveCultivatorSummary(targetCultivatorId, q);
+ if (!target) {
+ throw new FriendServiceError(404, '未找到该道友');
+ }
+
+ return {
+ target,
+ isFriend: await areFriends(currentCultivatorId, targetCultivatorId, q),
+ };
+}
+
+export async function addFriendPair(
+ cultivatorId: string,
+ friendCultivatorId: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ if (cultivatorId === friendCultivatorId) {
+ throw new FriendServiceError(400, '不能将自己加入好友名录');
+ }
+
+ const persist = async (tx: DbTransaction) => {
+ const [current, friend] = await Promise.all([
+ getActiveCultivatorSummary(cultivatorId, tx),
+ getActiveCultivatorSummary(friendCultivatorId, tx),
+ ]);
+
+ if (!current) {
+ throw new FriendServiceError(404, '当前角色不存在');
+ }
+ if (!friend) {
+ throw new FriendServiceError(404, '未找到该道友');
+ }
+
+ await tx
+ .insert(schema.cultivatorFriends)
+ .values([
+ { cultivatorId, friendCultivatorId },
+ { cultivatorId: friendCultivatorId, friendCultivatorId: cultivatorId },
+ ])
+ .onConflictDoNothing();
+
+ return friend;
+ };
+
+ return executor
+ ? persist(executor as DbTransaction)
+ : getExecutor().transaction(persist);
+}
+
+export async function removeFriendPair(
+ cultivatorId: string,
+ friendCultivatorId: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ const q = executor ?? getExecutor();
+ await q
+ .delete(schema.cultivatorFriends)
+ .where(
+ or(
+ and(
+ eq(schema.cultivatorFriends.cultivatorId, cultivatorId),
+ eq(schema.cultivatorFriends.friendCultivatorId, friendCultivatorId),
+ ),
+ and(
+ eq(schema.cultivatorFriends.cultivatorId, friendCultivatorId),
+ eq(schema.cultivatorFriends.friendCultivatorId, cultivatorId),
+ ),
+ ),
+ );
+}
+
+export async function assertFriend(
+ cultivatorId: string,
+ friendCultivatorId: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ if (!(await areFriends(cultivatorId, friendCultivatorId, executor))) {
+ throw new FriendServiceError(403, '只能向好友名录中的道友发送');
+ }
+}
+
+export async function assertFriends(
+ cultivatorId: string,
+ friendCultivatorIds: string[],
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ if (friendCultivatorIds.length === 0) {
+ return;
+ }
+
+ const q = executor ?? getExecutor();
+ const rows = await q
+ .select({ friendCultivatorId: schema.cultivatorFriends.friendCultivatorId })
+ .from(schema.cultivatorFriends)
+ .where(
+ and(
+ eq(schema.cultivatorFriends.cultivatorId, cultivatorId),
+ inArray(
+ schema.cultivatorFriends.friendCultivatorId,
+ friendCultivatorIds,
+ ),
+ ),
+ );
+ if (rows.length !== new Set(friendCultivatorIds).size) {
+ throw new FriendServiceError(403, '只能指定好友名录中的道友');
+ }
+}
diff --git a/src/server/lib/services/PlayerMailService.test.ts b/src/server/lib/services/PlayerMailService.test.ts
new file mode 100644
index 00000000..377a1422
--- /dev/null
+++ b/src/server/lib/services/PlayerMailService.test.ts
@@ -0,0 +1,167 @@
+const {
+ assertAuctionListableItemMock,
+ assertFriendMock,
+ consumeFirstTalismanByScenarioMock,
+ deleteArtifactsByIdsAndCultivatorMock,
+ getAuctionItemSnapshotMock,
+ sendMailMock,
+} = vi.hoisted(() => ({
+ assertAuctionListableItemMock: vi.fn(),
+ assertFriendMock: vi.fn(),
+ consumeFirstTalismanByScenarioMock: vi.fn(),
+ deleteArtifactsByIdsAndCultivatorMock: vi.fn(),
+ getAuctionItemSnapshotMock: vi.fn(),
+ sendMailMock: vi.fn(),
+}));
+
+vi.mock('./AuctionService', () => ({
+ assertAuctionListableItem: assertAuctionListableItemMock,
+ AuctionServiceError: class AuctionServiceError extends Error {
+ constructor(
+ public readonly code: string,
+ message: string,
+ ) {
+ super(message);
+ }
+ },
+ getAuctionItemSnapshot: getAuctionItemSnapshotMock,
+}));
+
+vi.mock('./FriendService', () => ({
+ assertFriend: assertFriendMock,
+ FriendServiceError: class FriendServiceError extends Error {
+ constructor(
+ public readonly status: number,
+ message: string,
+ ) {
+ super(message);
+ }
+ },
+}));
+
+vi.mock('./TalismanScenarioService', () => ({
+ consumeFirstTalismanByScenario: consumeFirstTalismanByScenarioMock,
+ TalismanScenarioError: class TalismanScenarioError extends Error {},
+}));
+
+vi.mock('./MailService', () => ({
+ MailService: {
+ sendMail: sendMailMock,
+ },
+}));
+
+vi.mock('@server/lib/drizzle/db', () => ({
+ getExecutor: vi.fn(),
+}));
+
+vi.mock('@server/lib/repositories/creationProductRepository', () => ({
+ deleteArtifactsByIdsAndCultivator: deleteArtifactsByIdsAndCultivatorMock,
+}));
+
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import * as schema from '../drizzle/schema';
+import { sendPlayerMail } from './PlayerMailService';
+
+function createTx() {
+ const updateSetMock = vi.fn(() => ({ where: vi.fn() }));
+ return {
+ select: vi.fn(() => ({
+ from: vi.fn(() => ({
+ where: vi.fn(() => ({
+ limit: vi.fn(async () => [{ id: 'recipient-1', name: '南宫婉' }]),
+ })),
+ })),
+ })),
+ delete: vi.fn(() => ({ where: vi.fn() })),
+ update: vi.fn(() => ({ set: updateSetMock })),
+ updateSetMock,
+ };
+}
+
+describe('PlayerMailService', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ assertFriendMock.mockResolvedValue(undefined);
+ consumeFirstTalismanByScenarioMock.mockResolvedValue(undefined);
+ sendMailMock.mockResolvedValue({ id: 'mail-1' });
+ deleteArtifactsByIdsAndCultivatorMock.mockResolvedValue([{ id: 'artifact-1' }]);
+ getAuctionItemSnapshotMock.mockResolvedValue({
+ id: 'material-1',
+ name: '玄铁',
+ type: 'ore',
+ rank: '玄品',
+ quantity: 3,
+ });
+ });
+
+ it('consumes the blank jade slip, detaches one transferable attachment, and sends mail', async () => {
+ const tx = createTx();
+
+ await expect(
+ sendPlayerMail({
+ senderCultivatorId: 'sender-1',
+ senderName: '韩立',
+ recipientCultivatorId: 'recipient-1',
+ content: '此物赠与道友。',
+ attachment: {
+ itemType: 'material',
+ itemId: 'material-1',
+ quantity: 2,
+ },
+ tx: tx as any,
+ }),
+ ).resolves.toEqual({
+ recipientName: '南宫婉',
+ attachmentCount: 1,
+ });
+
+ expect(assertFriendMock).toHaveBeenCalledWith(
+ 'sender-1',
+ 'recipient-1',
+ tx,
+ );
+ expect(consumeFirstTalismanByScenarioMock).toHaveBeenCalledWith(
+ 'sender-1',
+ 'friend_mail_send',
+ tx,
+ );
+ expect(assertAuctionListableItemMock).toHaveBeenCalledWith(
+ 'material',
+ expect.objectContaining({ name: '玄铁', quantity: 3 }),
+ 2,
+ );
+ expect(tx.update).toHaveBeenCalledWith(schema.materials);
+ expect(tx.updateSetMock).toHaveBeenCalledWith({ quantity: 1 });
+ expect(sendMailMock).toHaveBeenCalledWith(
+ 'recipient-1',
+ '来自韩立的传音',
+ '此物赠与道友。',
+ [
+ {
+ type: 'material',
+ name: '玄铁',
+ quantity: 2,
+ data: expect.objectContaining({ name: '玄铁', quantity: 2 }),
+ },
+ ],
+ 'reward',
+ tx,
+ );
+ });
+
+ it('rejects sending to non-friends', async () => {
+ const tx = createTx();
+ assertFriendMock.mockRejectedValueOnce(new Error('只能向好友名录中的道友发送'));
+
+ await expect(
+ sendPlayerMail({
+ senderCultivatorId: 'sender-1',
+ senderName: '韩立',
+ recipientCultivatorId: 'recipient-1',
+ content: '此物赠与道友。',
+ tx: tx as any,
+ }),
+ ).rejects.toThrow('只能向好友名录中的道友发送');
+ expect(sendMailMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/server/lib/services/PlayerMailService.ts b/src/server/lib/services/PlayerMailService.ts
new file mode 100644
index 00000000..27227614
--- /dev/null
+++ b/src/server/lib/services/PlayerMailService.ts
@@ -0,0 +1,219 @@
+import { getExecutor, type DbTransaction } from '@server/lib/drizzle/db';
+import * as creationProductRepository from '@server/lib/repositories/creationProductRepository';
+import * as schema from '@server/lib/drizzle/schema';
+import { FRIEND_MAIL_TALISMAN_SCENARIO } from '@shared/config/socialConfig';
+import type { Artifact, Consumable, Material } from '@shared/types/cultivator';
+import type { MailAttachment } from '@shared/types/mail';
+import { and, eq } from 'drizzle-orm';
+import {
+ assertAuctionListableItem,
+ AuctionServiceError,
+ getAuctionItemSnapshot,
+ type AuctionItemType,
+} from './AuctionService';
+import { assertFriend, FriendServiceError } from './FriendService';
+import { MailService } from './MailService';
+import {
+ consumeFirstTalismanByScenario,
+ TalismanScenarioError,
+} from './TalismanScenarioService';
+
+export type PlayerMailAttachmentInput = {
+ itemType: AuctionItemType;
+ itemId: string;
+ quantity: number;
+};
+
+export class PlayerMailServiceError extends Error {
+ constructor(
+ public readonly status: number,
+ message: string,
+ ) {
+ super(message);
+ this.name = 'PlayerMailServiceError';
+ }
+}
+
+async function assertActiveRecipient(
+ recipientCultivatorId: string,
+ tx: DbTransaction,
+): Promise<{ id: string; name: string }> {
+ const [recipient] = await tx
+ .select({
+ id: schema.cultivators.id,
+ name: schema.cultivators.name,
+ })
+ .from(schema.cultivators)
+ .where(
+ and(
+ eq(schema.cultivators.id, recipientCultivatorId),
+ eq(schema.cultivators.status, 'active'),
+ ),
+ )
+ .limit(1);
+
+ if (!recipient) {
+ throw new PlayerMailServiceError(404, '未找到收信道友');
+ }
+
+ return recipient;
+}
+
+async function detachAttachment(
+ senderCultivatorId: string,
+ attachment: PlayerMailAttachmentInput,
+ tx: DbTransaction,
+): Promise {
+ if (attachment.quantity < 1) {
+ throw new PlayerMailServiceError(400, '附件数量必须至少为 1');
+ }
+
+ const item = await getAuctionItemSnapshot(
+ attachment.itemType,
+ attachment.itemId,
+ senderCultivatorId,
+ tx,
+ );
+ if (!item) {
+ throw new PlayerMailServiceError(404, '附件物品不存在或已被消耗');
+ }
+
+ try {
+ assertAuctionListableItem(attachment.itemType, item, attachment.quantity);
+ } catch (error) {
+ if (error instanceof AuctionServiceError) {
+ throw new PlayerMailServiceError(400, error.message);
+ }
+ throw error;
+ }
+
+ if (attachment.itemType === 'artifact') {
+ const deleted =
+ await creationProductRepository.deleteArtifactsByIdsAndCultivator(
+ senderCultivatorId,
+ [attachment.itemId],
+ tx,
+ );
+ if (deleted.length !== 1) {
+ throw new PlayerMailServiceError(404, '附件物品不存在或已被消耗');
+ }
+ } else if (attachment.itemType === 'material') {
+ const current = item as Material;
+ if (attachment.quantity === current.quantity) {
+ await tx
+ .delete(schema.materials)
+ .where(
+ and(
+ eq(schema.materials.id, attachment.itemId),
+ eq(schema.materials.cultivatorId, senderCultivatorId),
+ ),
+ );
+ } else {
+ await tx
+ .update(schema.materials)
+ .set({ quantity: current.quantity - attachment.quantity })
+ .where(
+ and(
+ eq(schema.materials.id, attachment.itemId),
+ eq(schema.materials.cultivatorId, senderCultivatorId),
+ ),
+ );
+ }
+ } else {
+ const current = item as Consumable;
+ if (attachment.quantity === current.quantity) {
+ await tx
+ .delete(schema.consumables)
+ .where(
+ and(
+ eq(schema.consumables.id, attachment.itemId),
+ eq(schema.consumables.cultivatorId, senderCultivatorId),
+ ),
+ );
+ } else {
+ await tx
+ .update(schema.consumables)
+ .set({ quantity: current.quantity - attachment.quantity })
+ .where(
+ and(
+ eq(schema.consumables.id, attachment.itemId),
+ eq(schema.consumables.cultivatorId, senderCultivatorId),
+ ),
+ );
+ }
+ }
+
+ const snapshot =
+ attachment.itemType === 'artifact'
+ ? (item as Artifact)
+ : ({ ...item, quantity: attachment.quantity } as Material | Consumable);
+
+ return {
+ type: attachment.itemType,
+ name: snapshot.name,
+ quantity: attachment.quantity,
+ data: snapshot,
+ };
+}
+
+export async function sendPlayerMail(input: {
+ senderCultivatorId: string;
+ senderName: string;
+ recipientCultivatorId: string;
+ content: string;
+ attachment?: PlayerMailAttachmentInput;
+ tx?: DbTransaction;
+}): Promise<{ recipientName: string; attachmentCount: number }> {
+ const persist = async (tx: DbTransaction) => {
+ if (input.senderCultivatorId === input.recipientCultivatorId) {
+ throw new PlayerMailServiceError(400, '不能给自己发送传音');
+ }
+
+ try {
+ await assertFriend(
+ input.senderCultivatorId,
+ input.recipientCultivatorId,
+ tx,
+ );
+ await consumeFirstTalismanByScenario(
+ input.senderCultivatorId,
+ FRIEND_MAIL_TALISMAN_SCENARIO,
+ tx,
+ );
+ } catch (error) {
+ if (error instanceof FriendServiceError) {
+ throw new PlayerMailServiceError(403, error.message);
+ }
+ if (error instanceof TalismanScenarioError) {
+ throw new PlayerMailServiceError(
+ 400,
+ '缺少空白传音符,可前往天骄宝阁购买后再发送传音',
+ );
+ }
+ throw error;
+ }
+
+ const recipient = await assertActiveRecipient(input.recipientCultivatorId, tx);
+ const attachments = input.attachment
+ ? [await detachAttachment(input.senderCultivatorId, input.attachment, tx)]
+ : [];
+
+ await MailService.sendMail(
+ input.recipientCultivatorId,
+ `来自${input.senderName}的传音`,
+ input.content,
+ attachments,
+ attachments.length > 0 ? 'reward' : 'system',
+ tx,
+ );
+
+ return {
+ recipientName: recipient.name,
+ attachmentCount: attachments.length,
+ };
+ };
+
+ return input.tx
+ ? persist(input.tx)
+ : getExecutor().transaction((tx) => persist(tx));
+}
diff --git a/src/server/lib/services/TalismanScenarioService.ts b/src/server/lib/services/TalismanScenarioService.ts
new file mode 100644
index 00000000..9940929b
--- /dev/null
+++ b/src/server/lib/services/TalismanScenarioService.ts
@@ -0,0 +1,56 @@
+import { getExecutor, type DbExecutor, type DbTransaction } from '@server/lib/drizzle/db';
+import * as schema from '@server/lib/drizzle/schema';
+import { isTalismanConsumable } from '@shared/lib/consumables';
+import { and, eq } from 'drizzle-orm';
+import { mapConsumableRow } from './consumablePersistence';
+
+export class TalismanScenarioError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'TalismanScenarioError';
+ }
+}
+
+export async function consumeFirstTalismanByScenario(
+ cultivatorId: string,
+ scenario: string,
+ executor?: DbExecutor | DbTransaction,
+): Promise {
+ const q = executor ?? getExecutor();
+ const rows = await q
+ .select()
+ .from(schema.consumables)
+ .where(
+ and(
+ eq(schema.consumables.cultivatorId, cultivatorId),
+ eq(schema.consumables.type, '符箓'),
+ ),
+ )
+ .limit(200);
+
+ const row = rows
+ .filter((candidate) => candidate.quantity > 0)
+ .find((candidate) => {
+ const consumable = mapConsumableRow(candidate);
+ return (
+ isTalismanConsumable(consumable) &&
+ consumable.spec.scenario === scenario
+ );
+ });
+
+ if (!row) {
+ throw new TalismanScenarioError('缺少对应符箓,无法完成此操作');
+ }
+
+ if (row.quantity <= 1) {
+ await q
+ .delete(schema.consumables)
+ .where(eq(schema.consumables.id, row.id));
+ return;
+ }
+
+ await q
+ .update(schema.consumables)
+ .set({ quantity: row.quantity - 1 })
+ .where(eq(schema.consumables.id, row.id));
+}
diff --git a/src/server/routes/api/auction.router.ts b/src/server/routes/api/auction.router.ts
index f057fd99..7eaa5c3f 100644
--- a/src/server/routes/api/auction.router.ts
+++ b/src/server/routes/api/auction.router.ts
@@ -37,6 +37,8 @@ const ListSchema = z.object({
itemId: z.string().uuid(),
price: z.number().int().min(1).max(9_999_999),
quantity: z.number().int().min(1).default(1),
+ visibility: z.enum(['public', 'private']).default('public'),
+ targetCultivatorId: z.string().uuid().optional(),
});
const statusMap: Record = {
@@ -53,6 +55,10 @@ const statusMap: Record = {
INVALID_ITEM_QUALITY: 400,
CONSUMABLE_LISTING_DISABLED: 400,
SAME_OWNER: 403,
+ INVALID_VISIBILITY: 400,
+ TARGET_NOT_FRIEND: 403,
+ MISSING_TALISMAN: 400,
+ NOT_TARGET_BUYER: 403,
};
function getAuctionErrorStatus(error: AuctionServiceError): number {
@@ -61,7 +67,12 @@ function getAuctionErrorStatus(error: AuctionServiceError): number {
const router = new Hono();
-router.get('/listings', async (c) => {
+router.get('/listings', requireActiveCultivator(), async (c) => {
+ const cultivator = c.get('cultivator');
+ if (!cultivator) {
+ return c.json({ error: '未授权访问' }, 401);
+ }
+
try {
const params = ListingsSchema.parse({
itemType: c.req.query('itemType') || undefined,
@@ -72,7 +83,10 @@ router.get('/listings', async (c) => {
limit: c.req.query('limit') ? Number(c.req.query('limit')) : undefined,
});
- const result = await auctionRepository.findActiveListings(params);
+ const result = await auctionRepository.findActiveListings({
+ ...params,
+ viewerCultivatorId: cultivator.id,
+ });
const page = params.page || 1;
const limit = params.limit || 20;
const totalPages = Math.ceil(result.total / limit);
@@ -165,7 +179,8 @@ router.post('/list', requireActiveCultivator(), async (c) => {
}
try {
- const { itemType, itemId, price, quantity } = ListSchema.parse(
+ const { itemType, itemId, price, quantity, visibility, targetCultivatorId } =
+ ListSchema.parse(
await c.req.json(),
);
@@ -176,12 +191,15 @@ router.post('/list', requireActiveCultivator(), async (c) => {
run: async (tx) => {
const result = await listItem(
{
+ userId: user.id,
cultivatorId: cultivator.id,
cultivatorName: cultivator.name,
itemType,
itemId,
price,
quantity,
+ visibility,
+ targetCultivatorId,
},
{ tx, deferCacheClear: true },
);
diff --git a/src/server/routes/api/cultivator.router.ts b/src/server/routes/api/cultivator.router.ts
index a058fedd..d1529963 100644
--- a/src/server/routes/api/cultivator.router.ts
+++ b/src/server/routes/api/cultivator.router.ts
@@ -52,6 +52,10 @@ import {
identifyMysteryMaterial,
MarketServiceError,
} from '@server/lib/services/MarketService';
+import {
+ PlayerMailServiceError,
+ sendPlayerMail,
+} from '@server/lib/services/PlayerMailService';
import { PillOperationExecutor } from '@server/lib/services/PillOperationExecutor';
import {
QiInsufficientError,
@@ -159,6 +163,18 @@ const ClaimMailSchema = z.object({
mailId: z.string(),
});
+const SendMailSchema = z.object({
+ recipientCultivatorId: z.string().uuid(),
+ content: z.string().trim().min(1).max(1000),
+ attachment: z
+ .object({
+ itemType: z.enum(['material', 'artifact', 'consumable']),
+ itemId: z.string().uuid(),
+ quantity: z.number().int().min(1).default(1),
+ })
+ .optional(),
+});
+
const ReadMailSchema = z.object({
mailId: z.string(),
});
@@ -2210,6 +2226,67 @@ mailRouter.get('/', requireActiveCultivatorRef(), async (c) => {
});
});
+mailRouter.post('/send', requireActiveCultivator(), async (c) => {
+ const user = c.get('user');
+ const cultivator = c.get('cultivator');
+ if (!user || !cultivator) {
+ return c.json({ error: '未授权访问' }, 401);
+ }
+
+ try {
+ const parsed = SendMailSchema.parse(await c.req.json());
+ const committed = await commitPlayerStateMutation({
+ userId: user.id,
+ cultivatorId: cultivator.id,
+ source: 'player_mail_send',
+ run: async (tx) => {
+ const result = await sendPlayerMail({
+ senderCultivatorId: cultivator.id,
+ senderName: cultivator.name,
+ recipientCultivatorId: parsed.recipientCultivatorId,
+ content: parsed.content,
+ attachment: parsed.attachment,
+ tx,
+ });
+
+ const changes: StateChangeDescriptor[] = [
+ {
+ domain: 'inventory',
+ eventType: 'inventory.mail.sent',
+ invalidates: ['inventory'],
+ },
+ ];
+ if (parsed.attachment?.itemType === 'artifact') {
+ changes.push({
+ domain: 'products',
+ eventType: 'products.mail.sent',
+ invalidates: ['products'],
+ });
+ }
+
+ return {
+ result: {
+ message: `已向${result.recipientName}发出传音`,
+ attachmentCount: result.attachmentCount,
+ },
+ changes,
+ };
+ },
+ });
+
+ return c.json(toPlayerStateMutationResponse(committed));
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return c.json({ error: '参数错误', details: error.issues }, 400);
+ }
+ if (error instanceof PlayerMailServiceError) {
+ return jsonWithStatus(c, { error: error.message }, error.status);
+ }
+ console.error('mail send api error:', error);
+ return c.json({ error: '发送传音失败' }, 500);
+ }
+});
+
mailRouter.post('/claim', requireActiveCultivator(), async (c) => {
const user = c.get('user');
const cultivator = c.get('cultivator');
diff --git a/src/server/routes/api/friends.router.ts b/src/server/routes/api/friends.router.ts
new file mode 100644
index 00000000..d727a1eb
--- /dev/null
+++ b/src/server/routes/api/friends.router.ts
@@ -0,0 +1,99 @@
+import { requireActiveCultivator } from '@server/lib/hono/middleware';
+import { jsonWithStatus } from '@server/lib/hono/response';
+import type { AppEnv } from '@server/lib/hono/types';
+import {
+ addFriendPair,
+ FriendServiceError,
+ getInviteTarget,
+ listFriends,
+ removeFriendPair,
+} from '@server/lib/services/FriendService';
+import { Hono } from 'hono';
+import { z } from 'zod';
+
+const CultivatorIdSchema = z.object({
+ cultivatorId: z.string().uuid(),
+});
+
+const router = new Hono();
+
+router.get('/', requireActiveCultivator(), async (c) => {
+ const cultivator = c.get('cultivator');
+ if (!cultivator) {
+ return c.json({ error: '未授权访问' }, 401);
+ }
+
+ const friends = await listFriends(cultivator.id);
+ return c.json({ friends });
+});
+
+router.get('/invite/:cultivatorId', requireActiveCultivator(), async (c) => {
+ const cultivator = c.get('cultivator');
+ if (!cultivator) {
+ return c.json({ error: '未授权访问' }, 401);
+ }
+
+ try {
+ const { cultivatorId } = CultivatorIdSchema.parse({
+ cultivatorId: c.req.param('cultivatorId'),
+ });
+ const result = await getInviteTarget(cultivator.id, cultivatorId);
+ return c.json(result);
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return c.json({ error: '参数错误', details: error.issues }, 400);
+ }
+ if (error instanceof FriendServiceError) {
+ return jsonWithStatus(c, { error: error.message }, error.status);
+ }
+ console.error('Friend invite API error:', error);
+ return c.json({ error: '查询道友失败' }, 500);
+ }
+});
+
+router.post('/:cultivatorId', requireActiveCultivator(), async (c) => {
+ const cultivator = c.get('cultivator');
+ if (!cultivator) {
+ return c.json({ error: '未授权访问' }, 401);
+ }
+
+ try {
+ const { cultivatorId } = CultivatorIdSchema.parse({
+ cultivatorId: c.req.param('cultivatorId'),
+ });
+ const friend = await addFriendPair(cultivator.id, cultivatorId);
+ return c.json({ friend, message: '已加入好友名录' });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return c.json({ error: '参数错误', details: error.issues }, 400);
+ }
+ if (error instanceof FriendServiceError) {
+ return jsonWithStatus(c, { error: error.message }, error.status);
+ }
+ console.error('Friend add API error:', error);
+ return c.json({ error: '添加道友失败' }, 500);
+ }
+});
+
+router.delete('/:cultivatorId', requireActiveCultivator(), async (c) => {
+ const cultivator = c.get('cultivator');
+ if (!cultivator) {
+ return c.json({ error: '未授权访问' }, 401);
+ }
+
+ try {
+ const { cultivatorId } = CultivatorIdSchema.parse({
+ cultivatorId: c.req.param('cultivatorId'),
+ });
+ await removeFriendPair(cultivator.id, cultivatorId);
+ return c.json({ message: '已移出好友名录' });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return c.json({ error: '参数错误', details: error.issues }, 400);
+ }
+ console.error('Friend delete API error:', error);
+ return c.json({ error: '移除道友失败' }, 500);
+ }
+});
+
+export default router;
diff --git a/src/server/routes/api/index.ts b/src/server/routes/api/index.ts
index d5b30f6c..69333a0a 100644
--- a/src/server/routes/api/index.ts
+++ b/src/server/routes/api/index.ts
@@ -13,6 +13,7 @@ import dungeonRouter from '@server/routes/api/dungeon.router';
import enemiesRouter from '@server/routes/api/enemies.router';
import fateReshapeRouter from '@server/routes/api/fate-reshape.router';
import feedbackRouter from '@server/routes/api/feedback.router';
+import friendsRouter from '@server/routes/api/friends.router';
import generateCharacterRouter from '@server/routes/api/generate-character.router';
import generateFatesRouter from '@server/routes/api/generate-fates.router';
import manualDrawRouter from '@server/routes/api/manual-draw.router';
@@ -67,6 +68,7 @@ apiRouter.route('/dungeon', dungeonRouter);
apiRouter.route('/enemies', enemiesRouter);
apiRouter.route('/fate-reshape', fateReshapeRouter);
apiRouter.route('/feedback', feedbackRouter);
+apiRouter.route('/friends', friendsRouter);
apiRouter.route('/generate-character', generateCharacterRouter);
apiRouter.route('/generate-fates', generateFatesRouter);
apiRouter.route('/manual-draw', manualDrawRouter);
diff --git a/src/shared/config/socialConfig.ts b/src/shared/config/socialConfig.ts
new file mode 100644
index 00000000..42f48c22
--- /dev/null
+++ b/src/shared/config/socialConfig.ts
@@ -0,0 +1,3 @@
+export const FRIEND_MAIL_TALISMAN_SCENARIO = 'friend_mail_send';
+export const AUCTION_PRIVATE_LISTING_TALISMAN_SCENARIO =
+ 'auction_private_listing';