Add market watch and match hub workflows
这个提交包含在:
@@ -0,0 +1,39 @@
|
||||
ALTER TABLE `tutorial_videos`
|
||||
ADD `slug` varchar(128),
|
||||
ADD `topicArea` varchar(32) DEFAULT 'tennis_skill',
|
||||
ADD `contentFormat` varchar(16) DEFAULT 'video',
|
||||
ADD `sourcePlatform` varchar(16) DEFAULT 'none',
|
||||
ADD `heroSummary` text,
|
||||
ADD `externalUrl` text,
|
||||
ADD `platformVideoId` varchar(64),
|
||||
ADD `estimatedEffortMinutes` int,
|
||||
ADD `prerequisites` json,
|
||||
ADD `learningObjectives` json,
|
||||
ADD `stepSections` json,
|
||||
ADD `deliverables` json,
|
||||
ADD `relatedDocPaths` json,
|
||||
ADD `viewCount` int,
|
||||
ADD `commentCount` int,
|
||||
ADD `metricsFetchedAt` timestamp NULL,
|
||||
ADD `completionAchievementKey` varchar(64),
|
||||
ADD `isFeatured` int DEFAULT 0,
|
||||
ADD `featuredOrder` int DEFAULT 0;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `tutorial_progress`
|
||||
ADD `completed` int DEFAULT 0,
|
||||
ADD `completedAt` timestamp NULL;
|
||||
--> statement-breakpoint
|
||||
UPDATE `tutorial_videos`
|
||||
SET
|
||||
`topicArea` = COALESCE(`topicArea`, 'tennis_skill'),
|
||||
`contentFormat` = COALESCE(`contentFormat`, 'video'),
|
||||
`sourcePlatform` = COALESCE(`sourcePlatform`, 'none'),
|
||||
`heroSummary` = COALESCE(`heroSummary`, `description`),
|
||||
`estimatedEffortMinutes` = COALESCE(`estimatedEffortMinutes`, CASE WHEN `duration` IS NULL THEN NULL ELSE ROUND(`duration` / 60) END),
|
||||
`isFeatured` = COALESCE(`isFeatured`, 0),
|
||||
`featuredOrder` = COALESCE(`featuredOrder`, 0);
|
||||
--> statement-breakpoint
|
||||
UPDATE `tutorial_progress`
|
||||
SET
|
||||
`completed` = CASE WHEN `watched` = 1 THEN 1 ELSE COALESCE(`completed`, 0) END,
|
||||
`completedAt` = CASE WHEN `watched` = 1 AND `completedAt` IS NULL THEN `updatedAt` ELSE `completedAt` END;
|
||||
@@ -0,0 +1,14 @@
|
||||
ALTER TABLE `users`
|
||||
ADD `manualNtrpRating` float,
|
||||
ADD `manualNtrpCapturedAt` timestamp NULL,
|
||||
ADD `heightCm` float,
|
||||
ADD `weightKg` float,
|
||||
ADD `sprintSpeedScore` int,
|
||||
ADD `explosivePowerScore` int,
|
||||
ADD `agilityScore` int,
|
||||
ADD `enduranceScore` int,
|
||||
ADD `flexibilityScore` int,
|
||||
ADD `coreStabilityScore` int,
|
||||
ADD `shoulderMobilityScore` int,
|
||||
ADD `hipMobilityScore` int,
|
||||
ADD `assessmentNotes` text;
|
||||
86
drizzle/0012_market_watch.sql
普通文件
86
drizzle/0012_market_watch.sql
普通文件
@@ -0,0 +1,86 @@
|
||||
CREATE TABLE `racket_listings` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`source` enum('xianyu','jd','zhuanzhuan') NOT NULL,
|
||||
`sourceListingId` varchar(128) NOT NULL,
|
||||
`title` varchar(512) NOT NULL,
|
||||
`description` text,
|
||||
`listingUrl` text NOT NULL,
|
||||
`imageUrl` text,
|
||||
`price` float NOT NULL,
|
||||
`originalPrice` float,
|
||||
`sellerName` varchar(128),
|
||||
`location` varchar(128),
|
||||
`publishedAtRaw` varchar(128),
|
||||
`brand` varchar(64),
|
||||
`model` varchar(128),
|
||||
`series` varchar(128),
|
||||
`category` varchar(64),
|
||||
`weightGram` float,
|
||||
`conditionLevel` enum('brand_new','almost_new','used_good','used_fair','unknown') NOT NULL DEFAULT 'unknown',
|
||||
`gradeLevel` enum('high_value','standard','overpriced','pending_review') NOT NULL DEFAULT 'pending_review',
|
||||
`gradeReason` text,
|
||||
`isLowPriceCandidate` int NOT NULL DEFAULT 0,
|
||||
`fingerprint` varchar(128) NOT NULL,
|
||||
`extra` json,
|
||||
`fetchedAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `racket_listings_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `racket_listings_source_listing_idx` UNIQUE(`source`,`sourceListingId`),
|
||||
CONSTRAINT `racket_listings_fingerprint_idx` UNIQUE(`fingerprint`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `racket_watch_rules` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`title` varchar(256) NOT NULL,
|
||||
`brand` varchar(64) NOT NULL,
|
||||
`modelKeyword` varchar(128),
|
||||
`seriesKeyword` varchar(128),
|
||||
`category` varchar(64),
|
||||
`weightMinGram` float,
|
||||
`weightMaxGram` float,
|
||||
`targetPrice` float NOT NULL,
|
||||
`pushEnabled` int NOT NULL DEFAULT 1,
|
||||
`isActive` int NOT NULL DEFAULT 1,
|
||||
`lastCheckedAt` timestamp,
|
||||
`lastMatchedAt` timestamp,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `racket_watch_rules_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `racket_watch_hits` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`watchRuleId` int NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`listingId` int NOT NULL,
|
||||
`matchedPrice` float NOT NULL,
|
||||
`status` enum('matched','push_queued','pushed','suppressed') NOT NULL DEFAULT 'matched',
|
||||
`firstMatchedAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`lastMatchedAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`lastPushPrice` float,
|
||||
`pushedAt` timestamp,
|
||||
`pushCount` int NOT NULL DEFAULT 0,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `racket_watch_hits_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `racket_watch_hits_rule_listing_idx` UNIQUE(`watchRuleId`,`listingId`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
ALTER TABLE `background_tasks`
|
||||
MODIFY COLUMN `type` enum(
|
||||
'media_finalize',
|
||||
'training_plan_generate',
|
||||
'training_plan_adjust',
|
||||
'analysis_corrections',
|
||||
'pose_correction_multimodal',
|
||||
'ntrp_refresh_user',
|
||||
'ntrp_refresh_all',
|
||||
'market_source_sync',
|
||||
'market_watch_refresh',
|
||||
'market_push_delivery'
|
||||
) NOT NULL;
|
||||
90
drizzle/0013_match_hub.sql
普通文件
90
drizzle/0013_match_hub.sql
普通文件
@@ -0,0 +1,90 @@
|
||||
CREATE TABLE `match_sessions` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`createdByUserId` int NOT NULL,
|
||||
`matchMode` enum('daily','competitive') NOT NULL DEFAULT 'daily',
|
||||
`workflowStatus` enum('draft','recording','review_pending','reviewed','finalizing','finalized','cancelled') NOT NULL DEFAULT 'draft',
|
||||
`title` varchar(256) NOT NULL,
|
||||
`courtName` varchar(128),
|
||||
`notes` text,
|
||||
`durationMinutes` int NOT NULL DEFAULT 90,
|
||||
`scheduledAt` timestamp,
|
||||
`startedAt` timestamp,
|
||||
`endedAt` timestamp,
|
||||
`suggestionStatus` enum('idle','queued','ready','failed') NOT NULL DEFAULT 'idle',
|
||||
`suggestionTaskId` varchar(64),
|
||||
`suggestedScore` json,
|
||||
`suggestedMetrics` json,
|
||||
`finalScore` json,
|
||||
`finalMetrics` json,
|
||||
`reviewNotes` text,
|
||||
`reviewSubmittedAt` timestamp,
|
||||
`reviewedByUserId` int,
|
||||
`reviewedAt` timestamp,
|
||||
`finalizedByUserId` int,
|
||||
`finalizedAt` timestamp,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `match_sessions_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `match_participants` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`matchId` int NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`playerSlot` enum('player_a','player_b') NOT NULL,
|
||||
`cameraSlot` enum('camera_a','camera_b') NOT NULL,
|
||||
`cameraStatus` enum('pending','bound','active','completed','failed') NOT NULL DEFAULT 'pending',
|
||||
`cameraLabel` varchar(128),
|
||||
`cameraVideoId` int,
|
||||
`cameraVideoUrl` text,
|
||||
`cameraSnapshot` json,
|
||||
`isWinner` int NOT NULL DEFAULT 0,
|
||||
`suggestedSetsWon` int NOT NULL DEFAULT 0,
|
||||
`suggestedGamesWon` int NOT NULL DEFAULT 0,
|
||||
`suggestedPointsWon` int NOT NULL DEFAULT 0,
|
||||
`finalSetsWon` int NOT NULL DEFAULT 0,
|
||||
`finalGamesWon` int NOT NULL DEFAULT 0,
|
||||
`finalPointsWon` int NOT NULL DEFAULT 0,
|
||||
`suggestedStats` json,
|
||||
`finalStats` json,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT `match_participants_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `match_participants_match_player_idx` UNIQUE(`matchId`,`playerSlot`),
|
||||
CONSTRAINT `match_participants_match_user_idx` UNIQUE(`matchId`,`userId`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
CREATE TABLE `match_score_events` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`matchId` int NOT NULL,
|
||||
`eventIndex` int NOT NULL,
|
||||
`source` enum('camera_a','camera_b','system','admin') NOT NULL DEFAULT 'system',
|
||||
`eventType` enum('point','game','set','metric','score_suggestion','review_adjustment','finalized') NOT NULL,
|
||||
`winnerSlot` enum('player_a','player_b'),
|
||||
`matchSecond` int,
|
||||
`confidence` float,
|
||||
`payload` json,
|
||||
`createdByUserId` int,
|
||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `match_score_events_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `match_score_events_match_event_idx` UNIQUE(`matchId`,`eventIndex`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
|
||||
ALTER TABLE `background_tasks`
|
||||
MODIFY COLUMN `type` enum(
|
||||
'media_finalize',
|
||||
'training_plan_generate',
|
||||
'training_plan_adjust',
|
||||
'analysis_corrections',
|
||||
'pose_correction_multimodal',
|
||||
'ntrp_refresh_user',
|
||||
'ntrp_refresh_all',
|
||||
'market_source_sync',
|
||||
'market_watch_refresh',
|
||||
'market_push_delivery',
|
||||
'match_score_suggest',
|
||||
'match_finalize'
|
||||
) NOT NULL;
|
||||
@@ -85,6 +85,20 @@
|
||||
"when": 1773691200000,
|
||||
"tag": "0011_live_analysis_runtime",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "5",
|
||||
"when": 1773955200000,
|
||||
"tag": "0012_market_watch",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "5",
|
||||
"when": 1774569600000,
|
||||
"tag": "0013_match_hub",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -280,6 +280,95 @@ export const liveActionSegments = mysqlTable("live_action_segments", {
|
||||
export type LiveActionSegment = typeof liveActionSegments.$inferSelect;
|
||||
export type InsertLiveActionSegment = typeof liveActionSegments.$inferInsert;
|
||||
|
||||
/**
|
||||
* Dual-player match sessions with admin-reviewed score settlement.
|
||||
*/
|
||||
export const matchSessions = mysqlTable("match_sessions", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
createdByUserId: int("createdByUserId").notNull(),
|
||||
matchMode: mysqlEnum("matchMode", ["daily", "competitive"]).default("daily").notNull(),
|
||||
workflowStatus: mysqlEnum("workflowStatus", ["draft", "recording", "review_pending", "reviewed", "finalizing", "finalized", "cancelled"]).default("draft").notNull(),
|
||||
title: varchar("title", { length: 256 }).notNull(),
|
||||
courtName: varchar("courtName", { length: 128 }),
|
||||
notes: text("notes"),
|
||||
durationMinutes: int("durationMinutes").default(90).notNull(),
|
||||
scheduledAt: timestamp("scheduledAt"),
|
||||
startedAt: timestamp("startedAt"),
|
||||
endedAt: timestamp("endedAt"),
|
||||
suggestionStatus: mysqlEnum("suggestionStatus", ["idle", "queued", "ready", "failed"]).default("idle").notNull(),
|
||||
suggestionTaskId: varchar("suggestionTaskId", { length: 64 }),
|
||||
suggestedScore: json("suggestedScore"),
|
||||
suggestedMetrics: json("suggestedMetrics"),
|
||||
finalScore: json("finalScore"),
|
||||
finalMetrics: json("finalMetrics"),
|
||||
reviewNotes: text("reviewNotes"),
|
||||
reviewSubmittedAt: timestamp("reviewSubmittedAt"),
|
||||
reviewedByUserId: int("reviewedByUserId"),
|
||||
reviewedAt: timestamp("reviewedAt"),
|
||||
finalizedByUserId: int("finalizedByUserId"),
|
||||
finalizedAt: timestamp("finalizedAt"),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
});
|
||||
|
||||
export type MatchSession = typeof matchSessions.$inferSelect;
|
||||
export type InsertMatchSession = typeof matchSessions.$inferInsert;
|
||||
|
||||
/**
|
||||
* Match-bound participants and their dedicated camera slot.
|
||||
*/
|
||||
export const matchParticipants = mysqlTable("match_participants", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
matchId: int("matchId").notNull(),
|
||||
userId: int("userId").notNull(),
|
||||
playerSlot: mysqlEnum("playerSlot", ["player_a", "player_b"]).notNull(),
|
||||
cameraSlot: mysqlEnum("cameraSlot", ["camera_a", "camera_b"]).notNull(),
|
||||
cameraStatus: mysqlEnum("cameraStatus", ["pending", "bound", "active", "completed", "failed"]).default("pending").notNull(),
|
||||
cameraLabel: varchar("cameraLabel", { length: 128 }),
|
||||
cameraVideoId: int("cameraVideoId"),
|
||||
cameraVideoUrl: text("cameraVideoUrl"),
|
||||
cameraSnapshot: json("cameraSnapshot"),
|
||||
isWinner: int("isWinner").default(0).notNull(),
|
||||
suggestedSetsWon: int("suggestedSetsWon").default(0).notNull(),
|
||||
suggestedGamesWon: int("suggestedGamesWon").default(0).notNull(),
|
||||
suggestedPointsWon: int("suggestedPointsWon").default(0).notNull(),
|
||||
finalSetsWon: int("finalSetsWon").default(0).notNull(),
|
||||
finalGamesWon: int("finalGamesWon").default(0).notNull(),
|
||||
finalPointsWon: int("finalPointsWon").default(0).notNull(),
|
||||
suggestedStats: json("suggestedStats"),
|
||||
finalStats: json("finalStats"),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
matchPlayerUnique: uniqueIndex("match_participants_match_player_idx").on(table.matchId, table.playerSlot),
|
||||
matchUserUnique: uniqueIndex("match_participants_match_user_idx").on(table.matchId, table.userId),
|
||||
}));
|
||||
|
||||
export type MatchParticipant = typeof matchParticipants.$inferSelect;
|
||||
export type InsertMatchParticipant = typeof matchParticipants.$inferInsert;
|
||||
|
||||
/**
|
||||
* Match score and metric events from camera automation or admin review.
|
||||
*/
|
||||
export const matchScoreEvents = mysqlTable("match_score_events", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
matchId: int("matchId").notNull(),
|
||||
eventIndex: int("eventIndex").notNull(),
|
||||
source: mysqlEnum("source", ["camera_a", "camera_b", "system", "admin"]).default("system").notNull(),
|
||||
eventType: mysqlEnum("eventType", ["point", "game", "set", "metric", "score_suggestion", "review_adjustment", "finalized"]).notNull(),
|
||||
winnerSlot: mysqlEnum("winnerSlot", ["player_a", "player_b"]),
|
||||
matchSecond: int("matchSecond"),
|
||||
confidence: float("confidence"),
|
||||
payload: json("payload"),
|
||||
createdByUserId: int("createdByUserId"),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
}, (table) => ({
|
||||
matchEventUnique: uniqueIndex("match_score_events_match_event_idx").on(table.matchId, table.eventIndex),
|
||||
}));
|
||||
|
||||
export type MatchScoreEvent = typeof matchScoreEvents.$inferSelect;
|
||||
export type InsertMatchScoreEvent = typeof matchScoreEvents.$inferInsert;
|
||||
|
||||
/**
|
||||
* Daily training aggregate used for streaks, achievements and daily NTRP refresh.
|
||||
*/
|
||||
@@ -523,6 +612,93 @@ export const notificationLog = mysqlTable("notification_log", {
|
||||
export type NotificationLogEntry = typeof notificationLog.$inferSelect;
|
||||
export type InsertNotificationLog = typeof notificationLog.$inferInsert;
|
||||
|
||||
/**
|
||||
* Normalized racket market listings aggregated from multiple public sources.
|
||||
*/
|
||||
export const racketListings = mysqlTable("racket_listings", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
source: mysqlEnum("source", ["xianyu", "jd", "zhuanzhuan"]).notNull(),
|
||||
sourceListingId: varchar("sourceListingId", { length: 128 }).notNull(),
|
||||
title: varchar("title", { length: 512 }).notNull(),
|
||||
description: text("description"),
|
||||
listingUrl: text("listingUrl").notNull(),
|
||||
imageUrl: text("imageUrl"),
|
||||
price: float("price").notNull(),
|
||||
originalPrice: float("originalPrice"),
|
||||
sellerName: varchar("sellerName", { length: 128 }),
|
||||
location: varchar("location", { length: 128 }),
|
||||
publishedAtRaw: varchar("publishedAtRaw", { length: 128 }),
|
||||
brand: varchar("brand", { length: 64 }),
|
||||
model: varchar("model", { length: 128 }),
|
||||
series: varchar("series", { length: 128 }),
|
||||
category: varchar("category", { length: 64 }),
|
||||
weightGram: float("weightGram"),
|
||||
conditionLevel: mysqlEnum("conditionLevel", ["brand_new", "almost_new", "used_good", "used_fair", "unknown"]).default("unknown").notNull(),
|
||||
gradeLevel: mysqlEnum("gradeLevel", ["high_value", "standard", "overpriced", "pending_review"]).default("pending_review").notNull(),
|
||||
gradeReason: text("gradeReason"),
|
||||
isLowPriceCandidate: int("isLowPriceCandidate").default(0).notNull(),
|
||||
fingerprint: varchar("fingerprint", { length: 128 }).notNull(),
|
||||
extra: json("extra"),
|
||||
fetchedAt: timestamp("fetchedAt").defaultNow().notNull(),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
sourceListingUnique: uniqueIndex("racket_listings_source_listing_idx").on(table.source, table.sourceListingId),
|
||||
fingerprintUnique: uniqueIndex("racket_listings_fingerprint_idx").on(table.fingerprint),
|
||||
}));
|
||||
|
||||
export type RacketListing = typeof racketListings.$inferSelect;
|
||||
export type InsertRacketListing = typeof racketListings.$inferInsert;
|
||||
|
||||
/**
|
||||
* User-defined racket watch rules for brand/model/price monitoring.
|
||||
*/
|
||||
export const racketWatchRules = mysqlTable("racket_watch_rules", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
userId: int("userId").notNull(),
|
||||
title: varchar("title", { length: 256 }).notNull(),
|
||||
brand: varchar("brand", { length: 64 }).notNull(),
|
||||
modelKeyword: varchar("modelKeyword", { length: 128 }),
|
||||
seriesKeyword: varchar("seriesKeyword", { length: 128 }),
|
||||
category: varchar("category", { length: 64 }),
|
||||
weightMinGram: float("weightMinGram"),
|
||||
weightMaxGram: float("weightMaxGram"),
|
||||
targetPrice: float("targetPrice").notNull(),
|
||||
pushEnabled: int("pushEnabled").default(1).notNull(),
|
||||
isActive: int("isActive").default(1).notNull(),
|
||||
lastCheckedAt: timestamp("lastCheckedAt"),
|
||||
lastMatchedAt: timestamp("lastMatchedAt"),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
});
|
||||
|
||||
export type RacketWatchRule = typeof racketWatchRules.$inferSelect;
|
||||
export type InsertRacketWatchRule = typeof racketWatchRules.$inferInsert;
|
||||
|
||||
/**
|
||||
* Historical watch hits to dedupe and manage push delivery state.
|
||||
*/
|
||||
export const racketWatchHits = mysqlTable("racket_watch_hits", {
|
||||
id: int("id").autoincrement().primaryKey(),
|
||||
watchRuleId: int("watchRuleId").notNull(),
|
||||
userId: int("userId").notNull(),
|
||||
listingId: int("listingId").notNull(),
|
||||
matchedPrice: float("matchedPrice").notNull(),
|
||||
status: mysqlEnum("status", ["matched", "push_queued", "pushed", "suppressed"]).default("matched").notNull(),
|
||||
firstMatchedAt: timestamp("firstMatchedAt").defaultNow().notNull(),
|
||||
lastMatchedAt: timestamp("lastMatchedAt").defaultNow().notNull(),
|
||||
lastPushPrice: float("lastPushPrice"),
|
||||
pushedAt: timestamp("pushedAt"),
|
||||
pushCount: int("pushCount").default(0).notNull(),
|
||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||
}, (table) => ({
|
||||
watchRuleListingUnique: uniqueIndex("racket_watch_hits_rule_listing_idx").on(table.watchRuleId, table.listingId),
|
||||
}));
|
||||
|
||||
export type RacketWatchHit = typeof racketWatchHits.$inferSelect;
|
||||
export type InsertRacketWatchHit = typeof racketWatchHits.$inferInsert;
|
||||
|
||||
/**
|
||||
* Background task queue for long-running or retryable work.
|
||||
*/
|
||||
@@ -537,6 +713,11 @@ export const backgroundTasks = mysqlTable("background_tasks", {
|
||||
"pose_correction_multimodal",
|
||||
"ntrp_refresh_user",
|
||||
"ntrp_refresh_all",
|
||||
"market_source_sync",
|
||||
"market_watch_refresh",
|
||||
"market_push_delivery",
|
||||
"match_score_suggest",
|
||||
"match_finalize",
|
||||
]).notNull(),
|
||||
status: mysqlEnum("status", ["queued", "running", "succeeded", "failed"]).notNull().default("queued"),
|
||||
title: varchar("title", { length: 256 }).notNull(),
|
||||
|
||||
在新工单中引用
屏蔽一个用户