会员积分改版V7
This commit is contained in:
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-C0AXQCDm.js"></script>
|
<script type="module" crossorigin src="/assets/index-DWWJqJI6.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const AdminCreditProducts: React.FC = () => {
|
|||||||
renewalEnabled: nextRenewalEnabled,
|
renewalEnabled: nextRenewalEnabled,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
grantCredits: item.grantCredits,
|
grantCredits: item.grantCredits,
|
||||||
|
validityMonths: item.validityMonths ?? 1,
|
||||||
sortOrder: item.sortOrder,
|
sortOrder: item.sortOrder,
|
||||||
isActive: item.isActive,
|
isActive: item.isActive,
|
||||||
});
|
});
|
||||||
@@ -85,6 +86,7 @@ const AdminCreditProducts: React.FC = () => {
|
|||||||
billingCycle: 'monthly',
|
billingCycle: 'monthly',
|
||||||
tierRank: 1,
|
tierRank: 1,
|
||||||
renewalEnabled: nextRenewalEnabled,
|
renewalEnabled: nextRenewalEnabled,
|
||||||
|
validityMonths: 1,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,6 +126,7 @@ const AdminCreditProducts: React.FC = () => {
|
|||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
price: values.price,
|
price: values.price,
|
||||||
grant_credits: values.grantCredits,
|
grant_credits: values.grantCredits,
|
||||||
|
validity_months: values.validityMonths,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const savedProduct = editing
|
const savedProduct = editing
|
||||||
@@ -273,7 +276,7 @@ const AdminCreditProducts: React.FC = () => {
|
|||||||
},
|
},
|
||||||
{ title: '积分', dataIndex: 'grantCredits', render: (value?: number) => Number(value || 0).toLocaleString() },
|
{ title: '积分', dataIndex: 'grantCredits', render: (value?: number) => Number(value || 0).toLocaleString() },
|
||||||
{ title: '价格', dataIndex: 'price', render: (value?: number | null) => formatMoney(value) },
|
{ title: '价格', dataIndex: 'price', render: (value?: number | null) => formatMoney(value) },
|
||||||
{ title: '有效期', render: () => '1 个自然月' },
|
{ title: '有效期', dataIndex: 'validityMonths', render: (value?: number | null) => `${Number(value || 1)} 个月` },
|
||||||
{ title: '状态', dataIndex: 'isActive', render: (enabled: boolean) => <Tag color={enabled ? 'green' : 'default'}>{enabled ? '上架' : '下架'}</Tag> },
|
{ title: '状态', dataIndex: 'isActive', render: (enabled: boolean) => <Tag color={enabled ? 'green' : 'default'}>{enabled ? '上架' : '下架'}</Tag> },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -377,7 +380,12 @@ const AdminCreditProducts: React.FC = () => {
|
|||||||
<Form.Item name="price" label="价格" rules={[{ required: true }]}>
|
<Form.Item name="price" label="价格" rules={[{ required: true }]}>
|
||||||
<InputNumber min={0} precision={2} />
|
<InputNumber min={0} precision={2} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="有效期"><Input value="1 个自然月" disabled /></Form.Item>
|
<Form.Item name="validityMonths" label="有效期" rules={[{ required: true, message: '请选择有效期' }]}>
|
||||||
|
<Select
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={Array.from({ length: 36 }, (_, index) => ({ value: index + 1, label: `${index + 1} 个月` }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
</Space>}
|
</Space>}
|
||||||
<Form.Item name="description" label="说明"><Input.TextArea rows={2} /></Form.Item>
|
<Form.Item name="description" label="说明"><Input.TextArea rows={2} /></Form.Item>
|
||||||
<Form.Item name="featuresText" label="权益说明(每行一项)"><Input.TextArea rows={4} /></Form.Item>
|
<Form.Item name="featuresText" label="权益说明(每行一项)"><Input.TextArea rows={4} /></Form.Item>
|
||||||
|
|||||||
@@ -291,8 +291,7 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
{ title: '模块步骤', maxWidth: 22, render: (r) => r.sourceStepCodeLabel || '-' },
|
{ title: '模块步骤', maxWidth: 22, render: (r) => r.sourceStepCodeLabel || '-' },
|
||||||
{ title: '计费场景', maxWidth: 32, render: (r) => r.sceneNameSnapshot || r.billingSceneLabel || '-' },
|
{ title: '计费场景', maxWidth: 32, render: (r) => r.sceneNameSnapshot || r.billingSceneLabel || '-' },
|
||||||
{ title: '媒体类型', maxWidth: 12, align: 'center', render: (r) => r.mediaTypeLabel || '-' },
|
{ title: '媒体类型', maxWidth: 12, align: 'center', render: (r) => r.mediaTypeLabel || '-' },
|
||||||
{ title: '业务积分', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0.00', render: (r) => r.amount },
|
{ title: '变动积分', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0.00', render: (r) => r.amount },
|
||||||
{ title: '有效余额变动', minWidth: 14, maxWidth: 16, align: 'right', numFmt: '#,##0.00', render: (r) => r.balanceDelta ?? r.amount },
|
|
||||||
{ title: '已过期积分', minWidth: 13, maxWidth: 15, align: 'right', numFmt: '#,##0.00', render: (r) => r.expiredAmount || 0 },
|
{ title: '已过期积分', minWidth: 13, maxWidth: 15, align: 'right', numFmt: '#,##0.00', render: (r) => r.expiredAmount || 0 },
|
||||||
{ title: '变动后余额', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0.00', render: (r) => r.balanceAfter },
|
{ title: '变动后余额', minWidth: 12, maxWidth: 14, align: 'right', numFmt: '#,##0.00', render: (r) => r.balanceAfter },
|
||||||
{ title: 'LLM调用', minWidth: 12, maxWidth: 15, align: 'right', render: (r) => `${r.llmCallCount || 0}(成${r.llmSuccessCallCount || 0}/败${r.llmFailedCallCount || 0})` },
|
{ title: 'LLM调用', minWidth: 12, maxWidth: 15, align: 'right', render: (r) => `${r.llmCallCount || 0}(成${r.llmSuccessCallCount || 0}/败${r.llmFailedCallCount || 0})` },
|
||||||
@@ -365,8 +364,7 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
{ title: '模块', dataIndex: 'sourceModuleLabel', width: 130, render: (v: string) => v || '-' },
|
{ title: '模块', dataIndex: 'sourceModuleLabel', width: 130, render: (v: string) => v || '-' },
|
||||||
{ title: '步骤/场景', key: 'scene', width: 210, render: (_: any, r: AdminCreditRecord) => <div><div>{r.sceneNameSnapshot || r.billingSceneLabel || '-'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.sourceStepCodeLabel || '-'}</div></div> },
|
{ title: '步骤/场景', key: 'scene', width: 210, render: (_: any, r: AdminCreditRecord) => <div><div>{r.sceneNameSnapshot || r.billingSceneLabel || '-'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.sourceStepCodeLabel || '-'}</div></div> },
|
||||||
{ title: '媒体', dataIndex: 'mediaTypeLabel', width: 80, render: (v: string) => v ? <Tag color="purple">{v}</Tag> : '-' },
|
{ title: '媒体', dataIndex: 'mediaTypeLabel', width: 80, render: (v: string) => v ? <Tag color="purple">{v}</Tag> : '-' },
|
||||||
{ title: '业务积分', dataIndex: 'amount', width: 120, sorter: (a: AdminCreditRecord, b: AdminCreditRecord) => a.amount - b.amount, render: (v: number) => <Typography.Text strong>{n(v)}</Typography.Text> },
|
{ title: '变动积分', dataIndex: 'amount', width: 120, sorter: (a: AdminCreditRecord, b: AdminCreditRecord) => a.amount - b.amount, render: (value: number) => <Typography.Text strong style={{ color: value > 0 ? '#10b981' : value < 0 ? '#ef4444' : '#64748b' }}>{value > 0 ? '+' : ''}{n(value)}</Typography.Text> },
|
||||||
{ title: '有效变动', dataIndex: 'balanceDelta', width: 120, render: (v: number | undefined, r: AdminCreditRecord) => { const value = v ?? r.amount; return <Typography.Text strong style={{ color: value > 0 ? '#10b981' : value < 0 ? '#ef4444' : '#64748b' }}>{value > 0 ? '+' : ''}{n(value)}</Typography.Text>; } },
|
|
||||||
{ title: '过期积分', dataIndex: 'expiredAmount', width: 110, render: (v: number) => v ? <Tag color="orange">{n(v)}</Tag> : '-' },
|
{ title: '过期积分', dataIndex: 'expiredAmount', width: 110, render: (v: number) => v ? <Tag color="orange">{n(v)}</Tag> : '-' },
|
||||||
{ title: '余额', dataIndex: 'balanceAfter', width: 110, render: (v: number) => n(v) },
|
{ title: '余额', dataIndex: 'balanceAfter', width: 110, render: (v: number) => n(v) },
|
||||||
{ title: 'LLM调用', key: 'llmCalls', width: 130, render: (_: any, r: AdminCreditRecord) => <div>{n(r.llmCallCount || 0)} 次<div style={{ fontSize: 12, color: '#94a3b8' }}>成 {n(r.llmSuccessCallCount || 0)} / 败 {n(r.llmFailedCallCount || 0)}</div></div> },
|
{ title: 'LLM调用', key: 'llmCalls', width: 130, render: (_: any, r: AdminCreditRecord) => <div>{n(r.llmCallCount || 0)} 次<div style={{ fontSize: 12, color: '#94a3b8' }}>成 {n(r.llmSuccessCallCount || 0)} / 败 {n(r.llmFailedCallCount || 0)}</div></div> },
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""expand credit addon validity months to 1-36
|
||||||
|
|
||||||
|
Revision ID: b7e2c4d91a63
|
||||||
|
Revises: 1a4d1f095fa1
|
||||||
|
Create Date: 2026-08-11 13:15:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision = "b7e2c4d91a63"
|
||||||
|
down_revision = "1a4d1f095fa1"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
CONSTRAINT_NAME = "ck_credit_products_type_required_fields"
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_type_constraint_if_exists() -> None:
|
||||||
|
op.execute(
|
||||||
|
f"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_class t ON t.oid = c.conrelid
|
||||||
|
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||||||
|
WHERE n.nspname = current_schema()
|
||||||
|
AND t.relname = 'credit_products'
|
||||||
|
AND c.conname = '{CONSTRAINT_NAME}'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE credit_products DROP CONSTRAINT {CONSTRAINT_NAME};
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_type_constraint(*, addon_condition: str) -> None:
|
||||||
|
op.create_check_constraint(
|
||||||
|
CONSTRAINT_NAME,
|
||||||
|
"credit_products",
|
||||||
|
"(product_type = 'subscription' AND tier_code IS NOT NULL AND tier_rank IS NOT NULL "
|
||||||
|
"AND billing_cycle IS NOT NULL AND monthly_grant_credits IS NOT NULL "
|
||||||
|
"AND first_purchase_price IS NOT NULL AND regular_price IS NOT NULL "
|
||||||
|
"AND grant_credits IS NULL AND validity_months IS NULL) "
|
||||||
|
"OR (product_type = 'credit_addon' AND grant_credits IS NOT NULL "
|
||||||
|
f"AND {addon_condition} AND tier_code IS NULL AND tier_rank IS NULL "
|
||||||
|
"AND billing_cycle IS NULL AND monthly_grant_credits IS NULL "
|
||||||
|
"AND first_purchase_price IS NULL AND regular_price IS NULL "
|
||||||
|
"AND activity_price IS NULL AND activity_start_at IS NULL AND activity_end_at IS NULL)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# 当前版本中增值包被旧约束固定为 validity_months = 1。
|
||||||
|
# 先移除旧约束,再开放为1-36个自然月;不修改任何现有商品数据。
|
||||||
|
_drop_type_constraint_if_exists()
|
||||||
|
_create_type_constraint(addon_condition="validity_months BETWEEN 1 AND 36")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# 回退到旧版本时,旧约束只允许1个月。为保证 downgrade 可执行,
|
||||||
|
# 将现有增值包有效期恢复为旧版本唯一合法值1个月。
|
||||||
|
_drop_type_constraint_if_exists()
|
||||||
|
op.execute(
|
||||||
|
"UPDATE credit_products "
|
||||||
|
"SET validity_months = 1 "
|
||||||
|
"WHERE product_type = 'credit_addon' "
|
||||||
|
"AND validity_months IS DISTINCT FROM 1"
|
||||||
|
)
|
||||||
|
_create_type_constraint(addon_condition="validity_months = 1")
|
||||||
@@ -35,7 +35,9 @@ def _apply_product_payload(product: CreditProduct, payload: dict) -> None:
|
|||||||
mapping = {"features": "features_json"}
|
mapping = {"features": "features_json"}
|
||||||
for key, value in payload.items():
|
for key, value in payload.items():
|
||||||
setattr(product, mapping.get(key, key), value)
|
setattr(product, mapping.get(key, key), value)
|
||||||
if product.product_type == "credit_addon":
|
if product.product_type == "credit_addon" and product.validity_months is None:
|
||||||
|
# 兼容旧管理端未提交有效期的请求,新建增值包仍默认1个月;
|
||||||
|
# 更新时未提交该字段则保留原值。
|
||||||
product.validity_months = 1
|
product.validity_months = 1
|
||||||
if product.product_type == "subscription":
|
if product.product_type == "subscription":
|
||||||
product.price = product.regular_price or 0
|
product.price = product.regular_price or 0
|
||||||
@@ -77,6 +79,8 @@ def _validate_product_entity(product: CreditProduct) -> None:
|
|||||||
elif product.product_type == "credit_addon":
|
elif product.product_type == "credit_addon":
|
||||||
if product.grant_credits is None or product.price is None:
|
if product.grant_credits is None or product.price is None:
|
||||||
raise HTTPException(status_code=400, detail="积分增值包必须配置价格和积分数量")
|
raise HTTPException(status_code=400, detail="积分增值包必须配置价格和积分数量")
|
||||||
|
if product.validity_months is None or not 1 <= int(product.validity_months) <= 36:
|
||||||
|
raise HTTPException(status_code=400, detail="积分增值包有效期必须为1-36个月")
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="不支持的积分商品类型")
|
raise HTTPException(status_code=400, detail="不支持的积分商品类型")
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class CreditProduct(Base, TimestampMixin):
|
|||||||
"AND first_purchase_price IS NOT NULL AND regular_price IS NOT NULL "
|
"AND first_purchase_price IS NOT NULL AND regular_price IS NOT NULL "
|
||||||
"AND grant_credits IS NULL AND validity_months IS NULL) "
|
"AND grant_credits IS NULL AND validity_months IS NULL) "
|
||||||
"OR (product_type = 'credit_addon' AND grant_credits IS NOT NULL "
|
"OR (product_type = 'credit_addon' AND grant_credits IS NOT NULL "
|
||||||
"AND validity_months = 1 AND tier_code IS NULL AND tier_rank IS NULL "
|
"AND validity_months BETWEEN 1 AND 36 AND tier_code IS NULL AND tier_rank IS NULL "
|
||||||
"AND billing_cycle IS NULL AND monthly_grant_credits IS NULL "
|
"AND billing_cycle IS NULL AND monthly_grant_credits IS NULL "
|
||||||
"AND first_purchase_price IS NULL AND regular_price IS NULL "
|
"AND first_purchase_price IS NULL AND regular_price IS NULL "
|
||||||
"AND activity_price IS NULL AND activity_start_at IS NULL AND activity_end_at IS NULL)",
|
"AND activity_price IS NULL AND activity_start_at IS NULL AND activity_end_at IS NULL)",
|
||||||
@@ -63,7 +63,7 @@ class CreditProduct(Base, TimestampMixin):
|
|||||||
Boolean, nullable=False, default=True, server_default="true"
|
Boolean, nullable=False, default=True, server_default="true"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 积分增值包字段;当前固定一个自然月有效。
|
# 积分增值包字段;有效期按自然月配置,范围1-36个月。
|
||||||
grant_credits: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
grant_credits: Mapped[Decimal | None] = mapped_column(Numeric(20, 2), nullable=True)
|
||||||
validity_months: Mapped[int | None] = mapped_column(nullable=True)
|
validity_months: Mapped[int | None] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class CreditProductBase(BaseModel):
|
|||||||
|
|
||||||
price: float = Field(default=0, ge=0, le=999999999.99)
|
price: float = Field(default=0, ge=0, le=999999999.99)
|
||||||
grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
||||||
|
validity_months: int | None = Field(default=None, ge=1, le=36)
|
||||||
credit_level: Literal["promotional", "general"] = "general"
|
credit_level: Literal["promotional", "general"] = "general"
|
||||||
currency: str = Field(default="CNY", min_length=1, max_length=8)
|
currency: str = Field(default="CNY", min_length=1, max_length=8)
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
@@ -55,6 +56,8 @@ class CreditProductBase(BaseModel):
|
|||||||
raise ValueError("积分增值包必须配置积分数量")
|
raise ValueError("积分增值包必须配置积分数量")
|
||||||
if self.price < 0:
|
if self.price < 0:
|
||||||
raise ValueError("增值包价格不能小于0")
|
raise ValueError("增值包价格不能小于0")
|
||||||
|
if self.validity_months is not None and not 1 <= self.validity_months <= 36:
|
||||||
|
raise ValueError("积分增值包有效期必须为1-36个月")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +82,7 @@ class CreditProductUpdate(BaseModel):
|
|||||||
renewal_enabled: bool | None = None
|
renewal_enabled: bool | None = None
|
||||||
price: float | None = Field(default=None, ge=0, le=999999999.99)
|
price: float | None = Field(default=None, ge=0, le=999999999.99)
|
||||||
grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
grant_credits: float | None = Field(default=None, gt=0, le=999999999.99)
|
||||||
|
validity_months: int | None = Field(default=None, ge=1, le=36)
|
||||||
credit_level: Literal["promotional", "general"] | None = None
|
credit_level: Literal["promotional", "general"] | None = None
|
||||||
currency: str | None = Field(default=None, min_length=1, max_length=8)
|
currency: str | None = Field(default=None, min_length=1, max_length=8)
|
||||||
is_active: bool | None = None
|
is_active: bool | None = None
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ def product_to_dict(
|
|||||||
"activity_end_at": product.activity_end_at,
|
"activity_end_at": product.activity_end_at,
|
||||||
"renewal_enabled": bool(product.renewal_enabled),
|
"renewal_enabled": bool(product.renewal_enabled),
|
||||||
"grant_credits": float(product.grant_credits or 0),
|
"grant_credits": float(product.grant_credits or 0),
|
||||||
"validity_months": 1 if product.is_credit_addon else None,
|
"validity_months": int(product.validity_months or 1) if product.is_credit_addon else None,
|
||||||
"price": float(user_price if user_price is not None else product.price),
|
"price": float(user_price if user_price is not None else product.price),
|
||||||
"current_price": float(user_price if user_price is not None else product.price),
|
"current_price": float(user_price if user_price is not None else product.price),
|
||||||
"target_price": float(target_price) if target_price is not None else None,
|
"target_price": float(target_price) if target_price is not None else None,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ def _product_snapshot(product: CreditProduct) -> dict:
|
|||||||
"regular_price": float(product.regular_price or 0),
|
"regular_price": float(product.regular_price or 0),
|
||||||
"activity_price": float(product.activity_price) if product.activity_price is not None else None,
|
"activity_price": float(product.activity_price) if product.activity_price is not None else None,
|
||||||
"grant_credits": float(product.grant_credits or 0),
|
"grant_credits": float(product.grant_credits or 0),
|
||||||
|
"validity_months": int(product.validity_months or 1) if product.is_credit_addon else None,
|
||||||
"credit_level": product.credit_level,
|
"credit_level": product.credit_level,
|
||||||
"features": product.features_json or [],
|
"features": product.features_json or [],
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,24 @@ def _snapshot_decimal(snapshot: dict, key: str) -> Decimal:
|
|||||||
return to_credit_decimal(snapshot.get(key) or 0)
|
return to_credit_decimal(snapshot.get(key) or 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _snapshot_validity_months(snapshot: dict) -> int:
|
||||||
|
raw_value = snapshot.get("validity_months")
|
||||||
|
if raw_value is None:
|
||||||
|
# 兼容历史订单快照:旧版本增值包固定为1个自然月。
|
||||||
|
return 1
|
||||||
|
if isinstance(raw_value, bool):
|
||||||
|
raise ValueError("积分增值包有效期快照无效")
|
||||||
|
try:
|
||||||
|
months = int(raw_value)
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ValueError("积分增值包有效期快照无效") from exc
|
||||||
|
if isinstance(raw_value, float) and not raw_value.is_integer():
|
||||||
|
raise ValueError("积分增值包有效期快照无效")
|
||||||
|
if not 1 <= months <= 36:
|
||||||
|
raise ValueError("积分增值包有效期必须为1-36个月")
|
||||||
|
return months
|
||||||
|
|
||||||
|
|
||||||
async def _create_subscription(
|
async def _create_subscription(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
*,
|
*,
|
||||||
@@ -489,7 +508,7 @@ async def fulfill_payment_product(
|
|||||||
description=f"购买积分增值包:{order.product_name_snapshot or snapshot.get('name') or '积分增值包'}",
|
description=f"购买积分增值包:{order.product_name_snapshot or snapshot.get('name') or '积分增值包'}",
|
||||||
source_type=CreditBalanceSourceType.CREDIT_ADDON.value,
|
source_type=CreditBalanceSourceType.CREDIT_ADDON.value,
|
||||||
valid_from=checked_at,
|
valid_from=checked_at,
|
||||||
expires_at=add_natural_months(checked_at, 1),
|
expires_at=add_natural_months(checked_at, _snapshot_validity_months(snapshot)),
|
||||||
credit_level=str(snapshot.get("credit_level") or "general"),
|
credit_level=str(snapshot.get("credit_level") or "general"),
|
||||||
source_id=order.id,
|
source_id=order.id,
|
||||||
product_id=order.product_id,
|
product_id=order.product_id,
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<title>民众智创</title>
|
<title>民众智创</title>
|
||||||
<script type="module" crossorigin src="/assets/index-Bni9FiTX.js"></script>
|
<script type="module" crossorigin src="/assets/index-zFXYmniI.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1598,6 +1598,9 @@ const AppLayout: React.FC = () => {
|
|||||||
fontSize: 20, fontWeight: 800, marginTop: 2,
|
fontSize: 20, fontWeight: 800, marginTop: 2,
|
||||||
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
||||||
}}>¥{opt.currentPrice || opt.price}</div>
|
}}>¥{opt.currentPrice || opt.price}</div>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
有效期 {Number(opt.validityMonths || 1)} 个月
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,20 +67,17 @@ const CreditRecordsPage: React.FC = () => {
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '有效积分变动',
|
title: '变动积分',
|
||||||
key: 'balanceDelta',
|
dataIndex: 'amount',
|
||||||
|
key: 'amount',
|
||||||
width: 140,
|
width: 140,
|
||||||
align: 'right' as const,
|
align: 'right' as const,
|
||||||
render: (_: unknown, record: any) => {
|
render: (value: number) => {
|
||||||
const delta = record.balanceDelta ?? record.balance_delta ?? record.amount ?? 0;
|
const delta = Number(value ?? 0);
|
||||||
const expired = record.expiredAmount ?? record.expired_amount ?? 0;
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Typography.Text strong style={{ fontWeight: 700, color: delta > 0 ? '#10b981' : delta < 0 ? '#ef4444' : '#64748b', fontSize: 15 }}>
|
||||||
<Typography.Text strong style={{ fontWeight: 700, color: delta > 0 ? '#10b981' : delta < 0 ? '#ef4444' : '#64748b', fontSize: 15 }}>
|
{delta > 0 ? '+' : ''}{delta}
|
||||||
{delta > 0 ? '+' : ''}{delta}
|
</Typography.Text>
|
||||||
</Typography.Text>
|
|
||||||
{expired > 0 && <div style={{ color: '#94a3b8', fontSize: 12 }}>已过期 {expired}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user