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';