diff --git a/alembic/versions/0077_balance_columns_bigint.py b/alembic/versions/0077_balance_columns_bigint.py new file mode 100644 index 0000000..40c7b60 --- /dev/null +++ b/alembic/versions/0077_balance_columns_bigint.py @@ -0,0 +1,88 @@ +"""0077 金额/算力/信用额度列 int → bigint(杜绝 Out of range 溢出) + +覆盖全部金额、算力额度、信用积分列(含 raw SQL 表,无 ORM 模型者一并处理)。 +Revision ID: 0077 +Revises: 0076 +Create Date: 2026-09-10 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0077" +down_revision = "0076" +branch_labels = None +depends_on = None + +# (表, 列):opc 库中所有金额/算力/信用积分列 +COLUMNS = [ + ("users", "compute_quota"), + ("users", "compute_used_quota"), + ("users", "compute_personal_balance"), + ("park_companies", "compute_quota"), + ("park_companies", "compute_quota_used"), + ("park_companies", "compute_balance"), + ("park_companies", "compute_balance_used"), + ("company_members", "compute_balance"), + ("company_members", "compute_balance_used"), + ("compute_recharges", "amount"), + ("compute_recharges", "compute_amount"), + ("compute_balance_allocations", "amount"), + ("compute_usage_records", "standard_price"), + ("compute_usage_records", "actual_amount"), + ("compute_recharge_orders", "amount_fen"), + ("compute_recharge_orders", "quota_micro"), + ("compute_recharge_orders", "bonus_quota_micro"), + ("market_items", "price_fen"), + ("market_purchase_orders", "amount_fen"), + ("market_purchases", "amount_fen"), + ("event_orders", "amount_fen"), + ("events", "price_fen"), + ("opc_services", "price"), + ("service_orders", "price"), + ("service_orders", "amount_cents"), + ("service_orders", "platform_fee_cents"), + ("service_orders", "refund_amount_cents"), + ("service_orders", "seller_income_cents"), + ("settlement_logs", "amount"), + ("certification_types", "fee"), + ("certification_types", "credit_points"), + ("certifications", "fee_amount"), + ("certifications", "credit_points"), + ("training_courses", "price"), + ("training_course_enrollments", "price_paid"), + ("subsidy_applications", "amount"), + ("escrows", "amount"), + ("invoices", "amount"), + ("finance_records", "amount"), + ("investor_preferences", "amount_min"), + ("investor_preferences", "amount_max"), + ("badges", "credit_bonus"), + ("credit_ledger", "balance"), +] + + +def upgrade() -> None: + conn = op.get_bind() + for table, col in COLUMNS: + # 幂等:仅当列存在且为 int 时才改 + row = conn.execute(sa.text( + "SELECT COLUMN_TYPE, IS_NULLABLE, IFNULL(COLUMN_DEFAULT,'') FROM information_schema.COLUMNS " + "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:t AND COLUMN_NAME=:c" + ), {"t": table, "c": col}).fetchone() + if not row: + continue + col_type, nullable, default = row + if col_type.lower().startswith("bigint"): + continue + nn = "" if nullable == "YES" else " NOT NULL" + dflt = "" + if default == "0": + dflt = " DEFAULT 0" + conn.execute(sa.text( + f"ALTER TABLE `{table}` MODIFY COLUMN `{col}` BIGINT{nn}{dflt}" + )) + + +def downgrade() -> None: + # 金额列不降级回 int(历史溢出风险,且不可逆恢复成本高) + pass diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index d1d6a7d..b8dac58 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -10,6 +10,7 @@ from __future__ import annotations from pydantic import BaseModel, Field from sqlalchemy import ( + BigInteger, Boolean, Float, ForeignKey, @@ -74,10 +75,10 @@ class User(Base): # ── 算力(引擎用户镜像,全局一致) ── compute_provisioned: Mapped[bool] = mapped_column(Boolean, default=False) # 是否已在算力引擎建号 compute_username: Mapped[str] = mapped_column(String, default="") # 引擎用户名(= username) - compute_quota: Mapped[int] = mapped_column(Integer, default=0) # 缓存引擎可用额度 - compute_used_quota: Mapped[int] = mapped_column(Integer, default=0) # 缓存引擎已用额度 + compute_quota: Mapped[int] = mapped_column(BigInteger, default=0) # 缓存引擎可用额度 + compute_used_quota: Mapped[int] = mapped_column(BigInteger, default=0) # 缓存引擎已用额度 # ── 个人算力余额(分;个人充值/企业转入,与 company_members.compute_balance 企业分配分离) ── - compute_personal_balance: Mapped[int] = mapped_column(Integer, default=0) # 个人充值/转入余额(分) + compute_personal_balance: Mapped[int] = mapped_column(BigInteger, default=0) # 个人充值/转入余额(分) # ── 状态与身份分类 ── certification_status: Mapped[str] = mapped_column(String, default="uncertified") # uncertified|pending|certified|rejected certification_time: Mapped[str] = mapped_column(String, default="") @@ -542,8 +543,8 @@ class InvestorPreference(Base): ) industries: Mapped[str] = mapped_column(String, default="[]") # JSON list stage: Mapped[str] = mapped_column(String, default="") # seed/angel/series_a/... - amount_min: Mapped[int] = mapped_column(Integer, default=0) - amount_max: Mapped[int] = mapped_column(Integer, default=0) + amount_min: Mapped[int] = mapped_column(BigInteger, default=0) + amount_max: Mapped[int] = mapped_column(BigInteger, default=0) region_id: Mapped[str | None] = mapped_column(ForeignKey("regions.id"), nullable=True) updated_at: Mapped[str] = mapped_column(String, default="") @@ -645,7 +646,7 @@ class SubsidyApplication(Base): opc_id: Mapped[str] = mapped_column(String, default="") opc_name: Mapped[str] = mapped_column(String, default="") title: Mapped[str] = mapped_column(String, default="") - amount: Mapped[int] = mapped_column(Integer, default=0) + amount: Mapped[int] = mapped_column(BigInteger, default=0) region_id: Mapped[str | None] = mapped_column(ForeignKey("regions.id"), nullable=True) status: Mapped[str] = mapped_column(String, default="applying") # applying/district_review/city_review/province_review/approved/paid/rejected comment: Mapped[str] = mapped_column(String, default="") @@ -714,7 +715,7 @@ class Escrow(Base): ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, ) task_title: Mapped[str] = mapped_column(String, default="") - amount: Mapped[int] = mapped_column(Integer, default=0) + amount: Mapped[int] = mapped_column(BigInteger, default=0) commission: Mapped[int] = mapped_column(Integer, default=0) # 平台佣金 status: Mapped[str] = mapped_column(String, default="deposited") # deposited/frozen/released/refunded # ── 微信服务商分账(资金托管)字段 ── @@ -854,7 +855,7 @@ class Invoice(Base): issuer_id: Mapped[str] = mapped_column(String, default="") # 开票方(接单人) issuer_name: Mapped[str] = mapped_column(String, default="") payer_name: Mapped[str] = mapped_column(String, default="") # 抬头(发包方) - amount: Mapped[int] = mapped_column(Integer, default=0) + amount: Mapped[int] = mapped_column(BigInteger, default=0) tax_rate: Mapped[float] = mapped_column(Float, default=0.01) invoice_type: Mapped[str] = mapped_column(String, default="personal") # personal/vat status: Mapped[str] = mapped_column(String, default="applied") # applied/issued/rejected @@ -927,7 +928,7 @@ class FinanceRecord(Base): ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ) category: Mapped[str] = mapped_column(String, default="income") # income | expense - amount: Mapped[int] = mapped_column(Integer, default=0) + amount: Mapped[int] = mapped_column(BigInteger, default=0) date: Mapped[str] = mapped_column(String, default="") # YYYY-MM-DD note: Mapped[str] = mapped_column(String, default="") created_at: Mapped[str] = mapped_column(String, default="") @@ -1189,13 +1190,13 @@ class ParkCompany(Base): emp_migrant: Mapped[int | None] = mapped_column(Integer, nullable=True) # 返乡农民工 # 企业算力(只增不减):折扣% off 模型价;配额为发放给成员的算力额度 compute_discount: Mapped[int] = mapped_column(Integer, default=0) # 折扣%(0-100,单调非降) - compute_quota: Mapped[int] = mapped_column(Integer, default=0) # 企业算力配额(单调非降/增量) - compute_quota_used: Mapped[int] = mapped_column(Integer, default=0) # 已分配/已用配额(统计) + compute_quota: Mapped[int] = mapped_column(BigInteger, default=0) # 企业算力配额(单调非降/增量) + compute_quota_used: Mapped[int] = mapped_column(BigInteger, default=0) # 已分配/已用配额(统计) engine_group: Mapped[str] = mapped_column(String, default="") # 引擎用户组名(折扣倍率载体) owner_user_id: Mapped[str] = mapped_column(String, default="") # 负责人/管理员账号id(必填) # ── 企业算力余额(企业管理员充值后分配给成员,0056) ── - compute_balance: Mapped[int] = mapped_column(Integer, default=0) # 企业算力余额(分) - compute_balance_used: Mapped[int] = mapped_column(Integer, default=0) # 已分配给成员的余额(分) + compute_balance: Mapped[int] = mapped_column(BigInteger, default=0) # 企业算力余额(分) + compute_balance_used: Mapped[int] = mapped_column(BigInteger, default=0) # 已分配给成员的余额(分) created_at: Mapped[str] = mapped_column(String, default="") @@ -1344,8 +1345,8 @@ class CompanyMember(Base): role: Mapped[str] = mapped_column(String, default="staff") # 企业内角色(admin/finance/hr/staff,0056) status: Mapped[str] = mapped_column(String, default="active") # active/left # ── 成员算力余额(从企业分配,0056) ── - compute_balance: Mapped[int] = mapped_column(Integer, default=0) # 成员个人余额(分) - compute_balance_used: Mapped[int] = mapped_column(Integer, default=0) # 已使用余额(分) + compute_balance: Mapped[int] = mapped_column(BigInteger, default=0) # 成员个人余额(分) + compute_balance_used: Mapped[int] = mapped_column(BigInteger, default=0) # 已使用余额(分) # ── 退出企业(0056) ── left_at: Mapped[str] = mapped_column(String, default="") # 退出时间 left_reason: Mapped[str] = mapped_column(String, default="") # 退出原因 @@ -1393,8 +1394,8 @@ class ComputeRecharge(Base): id: Mapped[str] = mapped_column(String, primary_key=True) company_id: Mapped[str] = mapped_column(String, default="", index=True) # 企业充值时填写 user_id: Mapped[str] = mapped_column(String, default="", index=True) # 个人充值时填写 - amount: Mapped[int] = mapped_column(Integer, default=0) # 充值金额(分) - compute_amount: Mapped[int] = mapped_column(Integer, default=0) # 到账算力(分) + amount: Mapped[int] = mapped_column(BigInteger, default=0) # 充值金额(分) + compute_amount: Mapped[int] = mapped_column(BigInteger, default=0) # 到账算力(分) payment_method: Mapped[str] = mapped_column(String, default="") payment_status: Mapped[str] = mapped_column(String, default="pending") # pending/paid/failed transaction_id: Mapped[str] = mapped_column(String, default="") @@ -1410,7 +1411,7 @@ class ComputeBalanceAllocation(Base): company_id: Mapped[str] = mapped_column(String, index=True) from_user_id: Mapped[str] = mapped_column(String, default="") # 分配人(企业管理员) to_user_id: Mapped[str] = mapped_column(String, index=True) # 接收成员 - amount: Mapped[int] = mapped_column(Integer, default=0) # 分配金额(分) + amount: Mapped[int] = mapped_column(BigInteger, default=0) # 分配金额(分) type: Mapped[str] = mapped_column(String, default="allocate") # allocate/reclaim(分配/回收) reason: Mapped[str] = mapped_column(Text, default="") created_at: Mapped[str] = mapped_column(String, default="") @@ -1424,10 +1425,10 @@ class ComputeUsageRecord(Base): company_id: Mapped[str] = mapped_column(String, default="", index=True) # 从企业余额扣费时填写 model: Mapped[str] = mapped_column(String, default="") token_count: Mapped[int] = mapped_column(Integer, default=0) - standard_price: Mapped[int] = mapped_column(Integer, default=0) # 标准价(分) + standard_price: Mapped[int] = mapped_column(BigInteger, default=0) # 标准价(分) discount: Mapped[int] = mapped_column(Integer, default=0) # 实际折扣(0-100) discount_source: Mapped[str] = mapped_column(String, default="standard") # standard/tenant/company/tenant_user - actual_amount: Mapped[int] = mapped_column(Integer, default=0) # 实际扣费(分) + actual_amount: Mapped[int] = mapped_column(BigInteger, default=0) # 实际扣费(分) balance_source: Mapped[str] = mapped_column(String, default="personal") # company/personal created_at: Mapped[str] = mapped_column(String, default="") @@ -1484,7 +1485,7 @@ class OpcService(Base): description: Mapped[str] = mapped_column(Text, default="") cover: Mapped[str] = mapped_column(String, default="") gallery_json: Mapped[str] = mapped_column(Text, default="[]") # 作品集 URL 数组 - price: Mapped[int] = mapped_column(Integer, default=0) # 定价(元,0=面议) + price: Mapped[int] = mapped_column(BigInteger, default=0) # 定价(元,0=面议) delivery_days: Mapped[int] = mapped_column(Integer, default=0) # 交付时限(天) tags: Mapped[str] = mapped_column(String, default="") status: Mapped[str] = mapped_column(String, default="pending") # pending/published/offline/hidden @@ -1618,7 +1619,7 @@ class MarketItem(Base): bundle_json: Mapped[str] = mapped_column(Text, default="{}") # 安装包元数据(skill 内嵌完整 SKILL.md;MySQL 已 ALTER 为 MEDIUMTEXT) bundle_url: Mapped[str] = mapped_column(String, default="") # OSS 对象路径(plugin/app zip) bundle_sha256: Mapped[str] = mapped_column(String, default="") - price_fen: Mapped[int] = mapped_column(Integer, default=0) # 定价(分);0=免费(skill 恒 0) + price_fen: Mapped[int] = mapped_column(BigInteger, default=0) # 定价(分);0=免费(skill 恒 0) stats_json: Mapped[str] = mapped_column(Text, default="{}") # {"downloads":0,"views":0,"likes":0} is_featured: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String, default="draft", index=True) # draft|pending|published|rejected|offline @@ -1663,7 +1664,7 @@ class MarketPurchaseOrder(Base): item_name: Mapped[str] = mapped_column(String, default="") user_id: Mapped[str] = mapped_column(String, default="", index=True) username: Mapped[str] = mapped_column(String, default="") - amount_fen: Mapped[int] = mapped_column(Integer, default=0) # 实付金额(分) + amount_fen: Mapped[int] = mapped_column(BigInteger, default=0) # 实付金额(分) client_type: Mapped[str] = mapped_column(String, default="mp") # mp(小程序码) | jsapi | native appid: Mapped[str] = mapped_column(String, default="") status: Mapped[str] = mapped_column(String, default="pending", index=True) @@ -1689,7 +1690,7 @@ class MarketPurchase(Base): user_id: Mapped[str] = mapped_column(String, default="", index=True) item_id: Mapped[str] = mapped_column(String, default="", index=True) item_type: Mapped[str] = mapped_column(String, default="plugin") - amount_fen: Mapped[int] = mapped_column(Integer, default=0) + amount_fen: Mapped[int] = mapped_column(BigInteger, default=0) order_no: Mapped[str] = mapped_column(String, default="") created_at: Mapped[str] = mapped_column(String, default="") @@ -1709,7 +1710,7 @@ class ServiceOrder(Base): buyer_name: Mapped[str] = mapped_column(String, default="") contact: Mapped[str] = mapped_column(String, default="") schedule_at: Mapped[str] = mapped_column(String, default="") # 预约时间 - price: Mapped[int] = mapped_column(Integer, default=0) # 成交价(元,0=按服务定价) + price: Mapped[int] = mapped_column(BigInteger, default=0) # 成交价(元,0=按服务定价) note: Mapped[str] = mapped_column(Text, default="") status: Mapped[str] = mapped_column(String, default="pending") accept_at: Mapped[str] = mapped_column(String, default="") @@ -1918,9 +1919,9 @@ class CertificationType(Base): category: Mapped[str] = mapped_column(String, default="personal") # personal/enterprise/opc_talent/skill name: Mapped[str] = mapped_column(String, default="") description: Mapped[str] = mapped_column(Text, default="") - fee: Mapped[int] = mapped_column(Integer, default=0) # 分,0=免费 + fee: Mapped[int] = mapped_column(BigInteger, default=0) # 分,0=免费 validity_days: Mapped[int] = mapped_column(Integer, default=0) # 0=永久 - credit_points: Mapped[int] = mapped_column(Integer, default=0) + credit_points: Mapped[int] = mapped_column(BigInteger, default=0) levels_json: Mapped[str] = mapped_column(Text, default="[]") # 等级配置JSON materials_schema: Mapped[str] = mapped_column(Text, default="{}") # 表单JSON Schema review_mode: Mapped[str] = mapped_column(String, default="manual") # auto/manual/hybrid @@ -1948,8 +1949,8 @@ class Certification(Base): # not_started/pending/approved/rejected/active/expiring/expired/revoked materials_json: Mapped[str] = mapped_column(Text, default="{}") # 提交材料(脱敏) score: Mapped[int] = mapped_column(Integer, default=0) # 认证分数(技能测试用) - credit_points: Mapped[int] = mapped_column(Integer, default=0) # 获得的信用加分 - fee_amount: Mapped[int] = mapped_column(Integer, default=0) # 费用(分) + credit_points: Mapped[int] = mapped_column(BigInteger, default=0) # 获得的信用加分 + fee_amount: Mapped[int] = mapped_column(BigInteger, default=0) # 费用(分) payment_id: Mapped[str] = mapped_column(String, default="") submitted_at: Mapped[str] = mapped_column(String, default="") approved_at: Mapped[str] = mapped_column(String, default="") @@ -2002,7 +2003,7 @@ class TrainingCourse(Base): # ui_design/graphic_design/frontend_dev/backend_dev/copywriting/translation/data_analysis/project_management level: Mapped[str] = mapped_column(String, default="beginner") # beginner/intermediate/advanced/expert - price: Mapped[int] = mapped_column(Integer, default=0) # 分,0=免费 + price: Mapped[int] = mapped_column(BigInteger, default=0) # 分,0=免费 duration_minutes: Mapped[int] = mapped_column(Integer, default=0) lesson_count: Mapped[int] = mapped_column(Integer, default=0) chapter_count: Mapped[int] = mapped_column(Integer, default=0) @@ -2069,7 +2070,7 @@ class TrainingCourseEnrollment(Base): total_watch_seconds: Mapped[int] = mapped_column(Integer, default=0) enrolled_at: Mapped[str] = mapped_column(String, default="") completed_at: Mapped[str] = mapped_column(String, default="") - price_paid: Mapped[int] = mapped_column(Integer, default=0) + price_paid: Mapped[int] = mapped_column(BigInteger, default=0) created_at: Mapped[str] = mapped_column(String, default="") @@ -2249,7 +2250,7 @@ class CreditLedger(Base): event_code: Mapped[str] = mapped_column(String, default="", index=True) dimension: Mapped[str] = mapped_column(String, default="system") delta: Mapped[int] = mapped_column(Integer, default=0) - balance: Mapped[int] = mapped_column(Integer, default=0) + balance: Mapped[int] = mapped_column(BigInteger, default=0) reason: Mapped[str] = mapped_column(String, default="") ref_type: Mapped[str] = mapped_column(String, default="") ref_id: Mapped[str] = mapped_column(String, default="") @@ -2288,7 +2289,7 @@ class Badge(Base): description: Mapped[str] = mapped_column(String, default="") condition: Mapped[str] = mapped_column(String, default="") grant_rule_json: Mapped[str] = mapped_column(Text, default="{}") - credit_bonus: Mapped[int] = mapped_column(Integer, default=0) + credit_bonus: Mapped[int] = mapped_column(BigInteger, default=0) lifetime: Mapped[int] = mapped_column(Integer, default=1) revocable: Mapped[int] = mapped_column(Integer, default=1) expiry_days: Mapped[int] = mapped_column(Integer, default=0) @@ -2492,4 +2493,4 @@ class TeamMember(Base): role: Mapped[str] = mapped_column(String, default="member") # owner/admin/member status: Mapped[str] = mapped_column(String, default="active") # active/left created_at: Mapped[str] = mapped_column(String, default="") - left_at: Mapped[str] = mapped_column(String, default="") + left_at: Mapped[str] = mapped_column(String, default="") \ No newline at end of file