Explorar o código

增加筛选项

xueyiming hai 1 semana
pai
achega
f0f3b9a1be
Modificáronse 3 ficheiros con 132 adicións e 1 borrados
  1. 31 0
      app/api/routes.py
  2. 36 0
      app/services/hot_content_demand_export_service.py
  3. 65 1
      frontend/src/App.tsx

+ 31 - 0
app/api/routes.py

@@ -435,6 +435,10 @@ async def get_hot_content_demand_exports(
         default=None,
         description="是否成为需求:0 否,1 是;不传表示全部",
     ),
+    has_matched_demand: int | None = Query(
+        default=None,
+        description="是否匹配需求:0 否,1 是;不传表示全部",
+    ),
     item_type: str | None = Query(
         default=None,
         description="需求类型:词(特征点)、点(短语);不传表示全部",
@@ -444,6 +448,16 @@ async def get_hot_content_demand_exports(
         ge=0,
         description="微信指数热度下限(wxindex_latest_score >= 该值);不传表示不限制",
     ),
+    min_event_sense_score: float | None = Query(
+        default=None,
+        ge=0,
+        description="事件性得分下限(event_sense_score >= 该值);不传表示不限制",
+    ),
+    min_senior_fit_score: float | None = Query(
+        default=None,
+        ge=0,
+        description="老年性得分下限(senior_fit_score >= 该值);不传表示不限制",
+    ),
     page: int = Query(default=1, ge=1, description="页码,从 1 开始"),
     page_size: int = Query(default=20, ge=1, le=200, description="每页条数"),
 ) -> dict[str, object]:
@@ -452,8 +466,11 @@ async def get_hot_content_demand_exports(
             start_dt=start_dt,
             end_dt=end_dt,
             is_as_demand=is_as_demand,
+            has_matched_demand=has_matched_demand,
             item_type=item_type,
             min_wxindex_latest_score=min_wxindex_latest_score,
+            min_event_sense_score=min_event_sense_score,
+            min_senior_fit_score=min_senior_fit_score,
             page=page,
             page_size=page_size,
         )
@@ -468,20 +485,34 @@ async def export_hot_content_demand_exports_api(
     start_dt: str | None = Query(default=None, description="开始日期"),
     end_dt: str | None = Query(default=None, description="结束日期"),
     is_as_demand: int | None = Query(default=None, description="是否成为需求:0/1"),
+    has_matched_demand: int | None = Query(default=None, description="是否匹配需求:0/1"),
     item_type: str | None = Query(default=None, description="需求类型:词/点"),
     min_wxindex_latest_score: float | None = Query(
         default=None,
         ge=0,
         description="微信指数热度下限",
     ),
+    min_event_sense_score: float | None = Query(
+        default=None,
+        ge=0,
+        description="事件性得分下限",
+    ),
+    min_senior_fit_score: float | None = Query(
+        default=None,
+        ge=0,
+        description="老年性得分下限",
+    ),
 ) -> StreamingResponse:
     try:
         items = export_hot_content_demand_exports(
             start_dt=start_dt,
             end_dt=end_dt,
             is_as_demand=is_as_demand,
+            has_matched_demand=has_matched_demand,
             item_type=item_type,
             min_wxindex_latest_score=min_wxindex_latest_score,
+            min_event_sense_score=min_event_sense_score,
+            min_senior_fit_score=min_senior_fit_score,
         )
     except RuntimeError as exc:
         raise HTTPException(status_code=503, detail=str(exc)) from exc

+ 36 - 0
app/services/hot_content_demand_export_service.py

@@ -105,8 +105,11 @@ def _build_filters(
     start_dt: str | None,
     end_dt: str | None,
     is_as_demand: int | None,
+    has_matched_demand: int | None,
     item_type: str | None,
     min_wxindex_latest_score: float | None,
+    min_event_sense_score: float | None,
+    min_senior_fit_score: float | None,
 ) -> tuple[str, dict[str, object], datetime, datetime]:
     start_at, end_at_exclusive = _resolve_date_range(start_dt, end_dt)
     where_parts = [
@@ -122,6 +125,17 @@ def _build_filters(
             raise ValueError("is_as_demand 须为 0 或 1")
         where_parts.append("e.is_as_demand = :is_as_demand")
         params["is_as_demand"] = is_as_demand
+    if has_matched_demand is not None:
+        if has_matched_demand not in (0, 1):
+            raise ValueError("has_matched_demand 须为 0 或 1")
+        if has_matched_demand == 1:
+            where_parts.append(
+                "e.matched_demand IS NOT NULL AND TRIM(e.matched_demand) <> ''"
+            )
+        else:
+            where_parts.append(
+                "(e.matched_demand IS NULL OR TRIM(e.matched_demand) = '')"
+            )
     normalized_item_type = _normalize_item_type(item_type)
     if normalized_item_type:
         where_parts.append("e.item_type = :item_type")
@@ -131,6 +145,16 @@ def _build_filters(
             raise ValueError("min_wxindex_latest_score 不能为负数")
         where_parts.append("e.wxindex_latest_score >= :min_wxindex_latest_score")
         params["min_wxindex_latest_score"] = min_wxindex_latest_score
+    if min_event_sense_score is not None:
+        if min_event_sense_score < 0:
+            raise ValueError("min_event_sense_score 不能为负数")
+        where_parts.append("e.event_sense_score >= :min_event_sense_score")
+        params["min_event_sense_score"] = min_event_sense_score
+    if min_senior_fit_score is not None:
+        if min_senior_fit_score < 0:
+            raise ValueError("min_senior_fit_score 不能为负数")
+        where_parts.append("e.senior_fit_score >= :min_senior_fit_score")
+        params["min_senior_fit_score"] = min_senior_fit_score
     where_sql = f"WHERE {' AND '.join(where_parts)}"
     return where_sql, params, start_at, end_at_exclusive
 
@@ -176,8 +200,11 @@ def query_hot_content_demand_exports(
     start_dt: str | None = None,
     end_dt: str | None = None,
     is_as_demand: int | None = None,
+    has_matched_demand: int | None = None,
     item_type: str | None = None,
     min_wxindex_latest_score: float | None = None,
+    min_event_sense_score: float | None = None,
+    min_senior_fit_score: float | None = None,
     page: int = 1,
     page_size: int = 20,
 ) -> dict[str, object]:
@@ -185,8 +212,11 @@ def query_hot_content_demand_exports(
         start_dt=start_dt,
         end_dt=end_dt,
         is_as_demand=is_as_demand,
+        has_matched_demand=has_matched_demand,
         item_type=item_type,
         min_wxindex_latest_score=min_wxindex_latest_score,
+        min_event_sense_score=min_event_sense_score,
+        min_senior_fit_score=min_senior_fit_score,
     )
     offset = (page - 1) * page_size
     count_sql = text(
@@ -222,15 +252,21 @@ def export_hot_content_demand_exports(
     start_dt: str | None = None,
     end_dt: str | None = None,
     is_as_demand: int | None = None,
+    has_matched_demand: int | None = None,
     item_type: str | None = None,
     min_wxindex_latest_score: float | None = None,
+    min_event_sense_score: float | None = None,
+    min_senior_fit_score: float | None = None,
 ) -> list[dict[str, object]]:
     where_sql, params, _, _ = _build_filters(
         start_dt=start_dt,
         end_dt=end_dt,
         is_as_demand=is_as_demand,
+        has_matched_demand=has_matched_demand,
         item_type=item_type,
         min_wxindex_latest_score=min_wxindex_latest_score,
+        min_event_sense_score=min_event_sense_score,
+        min_senior_fit_score=min_senior_fit_score,
     )
     export_sql = text(
         f"""

+ 65 - 1
frontend/src/App.tsx

@@ -1319,6 +1319,7 @@ type HotContentDemandExportResponse = {
 };
 
 type IsAsDemandFilter = "all" | "yes" | "no";
+type MatchedDemandFilter = "all" | "yes" | "no";
 type ItemTypeFilter = "all" | "word" | "point";
 
 function HotContentDemandExportPanel({ active }: { active: boolean }) {
@@ -1327,11 +1328,17 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
     return [today, today];
   });
   const [isAsDemandFilter, setIsAsDemandFilter] = useState<IsAsDemandFilter>("all");
+  const [matchedDemandFilter, setMatchedDemandFilter] = useState<MatchedDemandFilter>("all");
   const [itemTypeFilter, setItemTypeFilter] = useState<ItemTypeFilter>("all");
   const [appliedIsAsDemand, setAppliedIsAsDemand] = useState<IsAsDemandFilter>("all");
+  const [appliedMatchedDemand, setAppliedMatchedDemand] = useState<MatchedDemandFilter>("all");
   const [appliedItemType, setAppliedItemType] = useState<ItemTypeFilter>("all");
   const [minWxindexInput, setMinWxindexInput] = useState<number | null>(null);
   const [appliedMinWxindex, setAppliedMinWxindex] = useState<number | null>(null);
+  const [minEventSenseInput, setMinEventSenseInput] = useState<number | null>(null);
+  const [appliedMinEventSense, setAppliedMinEventSense] = useState<number | null>(null);
+  const [minSeniorFitInput, setMinSeniorFitInput] = useState<number | null>(null);
+  const [appliedMinSeniorFit, setAppliedMinSeniorFit] = useState<number | null>(null);
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize, setPageSize] = useState(20);
   const [refreshTick, setRefreshTick] = useState(0);
@@ -1363,14 +1370,25 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
     } else if (appliedIsAsDemand === "no") {
       url.searchParams.set("is_as_demand", "0");
     }
+    if (appliedMatchedDemand === "yes") {
+      url.searchParams.set("has_matched_demand", "1");
+    } else if (appliedMatchedDemand === "no") {
+      url.searchParams.set("has_matched_demand", "0");
+    }
     if (appliedItemType === "word") {
       url.searchParams.set("item_type", "词");
-    } else     if (appliedItemType === "point") {
+    } else if (appliedItemType === "point") {
       url.searchParams.set("item_type", "点");
     }
     if (appliedMinWxindex !== null) {
       url.searchParams.set("min_wxindex_latest_score", String(appliedMinWxindex));
     }
+    if (appliedMinEventSense !== null) {
+      url.searchParams.set("min_event_sense_score", String(appliedMinEventSense));
+    }
+    if (appliedMinSeniorFit !== null) {
+      url.searchParams.set("min_senior_fit_score", String(appliedMinSeniorFit));
+    }
   };
 
   const buildRequestUrl = (page: number, size: number = pageSize) => {
@@ -1399,8 +1417,11 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
     startDate,
     endDate,
     appliedIsAsDemand,
+    appliedMatchedDemand,
     appliedItemType,
     appliedMinWxindex,
+    appliedMinEventSense,
+    appliedMinSeniorFit,
     currentPage,
     pageSize,
     refreshTick,
@@ -1443,8 +1464,11 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
       return;
     }
     setAppliedIsAsDemand(isAsDemandFilter);
+    setAppliedMatchedDemand(matchedDemandFilter);
     setAppliedItemType(itemTypeFilter);
     setAppliedMinWxindex(minWxindexInput);
+    setAppliedMinEventSense(minEventSenseInput);
+    setAppliedMinSeniorFit(minSeniorFitInput);
     setCurrentPage(1);
     setRefreshTick((value) => value + 1);
   };
@@ -1468,11 +1492,17 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
     const today = dayjs();
     setDateRange([today, today]);
     setIsAsDemandFilter("all");
+    setMatchedDemandFilter("all");
     setItemTypeFilter("all");
     setAppliedIsAsDemand("all");
+    setAppliedMatchedDemand("all");
     setAppliedItemType("all");
     setMinWxindexInput(null);
     setAppliedMinWxindex(null);
+    setMinEventSenseInput(null);
+    setAppliedMinEventSense(null);
+    setMinSeniorFitInput(null);
+    setAppliedMinSeniorFit(null);
     setCurrentPage(1);
     setRefreshTick((value) => value + 1);
   };
@@ -1630,6 +1660,18 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
                 ]}
               />
             </Form.Item>
+            <Form.Item label="是否匹配需求">
+              <Select<MatchedDemandFilter>
+                className="hot-content-filter-select"
+                value={matchedDemandFilter}
+                onChange={setMatchedDemandFilter}
+                options={[
+                  { label: "全部", value: "all" },
+                  { label: "是", value: "yes" },
+                  { label: "否", value: "no" },
+                ]}
+              />
+            </Form.Item>
             <Form.Item label="微信指数热度 ≥">
               <InputNumber
                 className="hot-content-wxindex-input"
@@ -1640,6 +1682,28 @@ function HotContentDemandExportPanel({ active }: { active: boolean }) {
                 onChange={(v) => setMinWxindexInput(v ?? null)}
               />
             </Form.Item>
+            <Form.Item label="事件性得分 ≥">
+              <InputNumber
+                className="hot-content-wxindex-input"
+                min={0}
+                max={10}
+                step={0.1}
+                placeholder="不限制"
+                value={minEventSenseInput}
+                onChange={(v) => setMinEventSenseInput(v ?? null)}
+              />
+            </Form.Item>
+            <Form.Item label="老年性得分 ≥">
+              <InputNumber
+                className="hot-content-wxindex-input"
+                min={0}
+                max={10}
+                step={0.1}
+                placeholder="不限制"
+                value={minSeniorFitInput}
+                onChange={(v) => setMinSeniorFitInput(v ?? null)}
+              />
+            </Form.Item>
             <Form.Item label=" ">
               <Button
                 type="primary"