跳转至

线上环境固定数据同步检查清单

本文用于上线前核对本地或测试环境中哪些“固定数据/运营数据”需要同步到线上环境。这里的固定数据指:应用运行时从数据库或仓库文件读取、但不属于用户业务数据的配置,例如套餐、首购优惠、兑换码、Prompt 模板和 Prompt 场景绑定。

本文基于 2026-05-21 对本地 .env.dev 数据库的只读检查,以及当前代码仓库结构整理。

结论

上线前必须重点同步:

  1. system_configs:首购折扣相关配置。
  2. subscription_plans:商城套餐定义。
  3. prompt_templates + prompt_template_versions:线上运行时 Prompt 模板。
  4. prompt_scene_bindings:Prompt 场景到模板的绑定规则。

需要按活动策略决定是否同步:

  1. redeem_codes:内部兑换码/促销码。本地当前没有可用兑换码,不建议把已使用或测试码直接带到线上。

通常不需要单独同步:

  1. app/prompts/** 文件:属于代码发布内容,随镜像/代码上线。
  2. PROMPT_SCENE_DEFINITIONS 场景枚举:代码常量,随代码上线。
  3. 用户、订单、标书、知识库、产品库等业务数据:不属于本清单范围。

必须同步的数据

1. 系统配置:system_configs

运行时用途:

  • first_purchase_discount:首购折扣率,当前本地值 0.85
  • first_purchase_plan_code:首购适用套餐,当前本地值 monthly_1
  • first_purchase_discount_code:首购折扣审计码,当前本地值 SEEMCODE_a0c076cf

本地当前快照:

key value 说明
first_purchase_discount 0.85 首购折扣率
first_purchase_plan_code monthly_1 首购适用套餐
first_purchase_discount_code SEEMCODE_a0c076cf 首购折扣审计/记录码

同步建议:

  • 线上如果已经有运营确认过的值,以线上为准,不要被本地覆盖。
  • 新环境初始化时,需要至少写入这 3 个 key。

校验 SQL:

SELECT key, value, description, updated_at
FROM system_configs
WHERE key IN (
  'first_purchase_discount',
  'first_purchase_plan_code',
  'first_purchase_discount_code'
)
ORDER BY key;

幂等 upsert 示例:

INSERT INTO system_configs (key, value, description, created_at, updated_at)
VALUES
  ('first_purchase_discount', '0.85', '首购折扣(0-1之间的小数)', now(), now()),
  ('first_purchase_plan_code', 'monthly_1', '首购适用的套餐 plan_code', now(), now()),
  ('first_purchase_discount_code', 'SEEMCODE_a0c076cf', '首购折扣码,用于审计/记录', now(), now())
ON CONFLICT (key) DO UPDATE SET
  value = EXCLUDED.value,
  description = EXCLUDED.description,
  updated_at = now();

2. 套餐定义:subscription_plans

运行时用途:

  • 商城套餐列表。
  • 下单金额、配额发放、有效期、是否允许叠加。
  • 新人注册福利逻辑会按 newcomer_welfare 查询;当前本地库没有该套餐,代码 seed 脚本中有。

本地当前快照:

plan_code name type price_cents original_price_cents quota period_days monthly_count active sort
monthly_1 月套餐1 MONTHLY 8800 12800 1000000 30 true 1
monthly_2 月套餐2 MONTHLY 18800 25800 3000000 30 true 2
monthly_3 月套餐3 MONTHLY 28800 38800 不限 30 true 3
yearly_1 年套餐1 YEARLY 88800 108000 3000000 365 12 true 4
yearly_2 年套餐2 YEARLY 138800 246900 5000000 365 12 true 5
yearly_3 年套餐3 YEARLY 188800 320000 不限 365 12 true 6
topup_1 字数套餐1 TOPUP 8800 10000 500000 永久 true 7

同步建议:

  • 新环境初始化或线上缺失套餐时,按 plan_code 幂等 upsert。
  • 现有线上环境若已经有真实价格/活动价,需要先由运营确认,不要盲目用本地覆盖。
  • exector/insert_subscription_plans.sql 是幂等 upsert,但文件中的 original_price_cents 与本地当前库不完全一致;上线前需要选择一个权威来源。
  • 建议补齐或确认 newcomer_welfare。代码 app/services/subscription/service.py 会查询该 plan_code 发放新人福利。

校验 SQL:

SELECT plan_code, name, plan_type, distribution_type, price_cents,
       original_price_cents, character_quota_per_period, period_days,
       monthly_distribution_count, allow_stacking, is_active,
       sort_order, tag, features, updated_at
FROM subscription_plans
ORDER BY sort_order, plan_code;

3. Prompt 模板:prompt_templates / prompt_template_versions

运行时用途:

  • document_parse:页级/全文解析使用。
  • outline_skeleton:一二级目录骨架生成使用,但当前本地模板 key 是 outline_l1_l2,需注意代码实际读取 key。
  • writing_hint:章节编写思路与三四级目录生成使用。
  • section_content:章节正文生成使用。

本地当前模板快照(记录运行时版本的正文长度和 SHA-256,便于和线上比对):

key version status body_chars body_sha256
document_parse 1 published 1890 338026896d4329fe4db5022ec89f633bacb435b5b046c37d70ad42573035d0d9
outline_l1_l2 1 published 866 c1be38cefd6e3534131369f71c1af9aac036477b1c92b6de9b2e089b514ba3d6
project_overview 1 published 1134 62e7632934daf4612ad05484a3fd64e3791b77747997da3d5344a89d17dc2ee3
scoring_criteria 1 published 909 d5d4731ba82cd19f81b7e457bb397ca14d6f44f4a5026d95185aecfc8b329536
section_content_engineering 1 published 1452 17e9bd9aadda9cb4d0254effffcad2cbc4734d023a41da65a73d1c0dbc7dddaa
section_content_goods_distributor 1 published 1403 ffa3dfd3200409829ad66030efaae5f48ed1dd101bb31028875f5e0c52265bee
section_content_goods_manufacturer 1 published 1443 7d4161465f68b339fc3bfd9b32c899e1281e5fa214fc57d643c8c58231f8e788
section_content_service 1 published 2723 7e95de18749aed4c0382dec7b64951527d2325ffccb6430c0eb0e001e934c401
writing_hint_engineering 1 published 3145 38dc48e3adb4d0a8ad848041bf8acfefa5eec30e35ee6b80ba6ea4eb4851860b
writing_hint_goods_distributor 1 published 3198 713a57a0682c931561aff8ca172b652bf3b9a49645bfe7c513ad699c4d2ac71e
writing_hint_goods_manufacturer 1 published 3035 77d3990e2b0587e08433a09fd37cd751c5284001e5d54d7126f874202a72d823
writing_hint_service 1 published 2608 abd37981cf43c431c6a3c18c8ba607c866f9175f913e112f2d8114218635132e

同步建议:

  • Prompt 模板是数据库运行时数据,不会因为部署代码自动同步。
  • 需要同步 prompt_templates 和对应 prompt_template_versions 的已发布版本。
  • 同步时优先按 prompt_templates.key 作为稳定身份,不要依赖本地 UUID。
  • 每个模板线上至少要有一个 published 版本;服务会优先取最新 published,否则退回最新版本,但生产环境不建议依赖草稿回退。

校验 SQL:

CREATE EXTENSION IF NOT EXISTS pgcrypto;

SELECT t.key,
       t.display_name,
       t.description,
       v.version,
       v.status,
       v.published_at,
       length(v.body) AS body_chars,
       encode(digest(v.body, 'sha256'), 'hex') AS body_sha256,
       greatest(t.updated_at, v.updated_at) AS updated_at
FROM prompt_templates t
LEFT JOIN LATERAL (
  SELECT *
  FROM prompt_template_versions v
  WHERE v.template_id = t.id
  ORDER BY CASE WHEN v.status = 'published' THEN 0 ELSE 1 END,
           v.version DESC
  LIMIT 1
) v ON true
ORDER BY t.key;

导出建议:

COPY (
  SELECT jsonb_pretty(jsonb_agg(row_to_json(x)::jsonb))
  FROM (
    SELECT t.key, t.display_name, t.description,
           v.version, v.body, v.status, v.change_note
    FROM prompt_templates t
    JOIN prompt_template_versions v ON v.template_id = t.id
    WHERE v.status = 'published'
    ORDER BY t.key, v.version
  ) x
) TO STDOUT;

4. Prompt 场景绑定:prompt_scene_bindings

运行时用途:

  • 根据 scene_key + tender.industry + tender.goods_role 解析应使用哪个 Prompt 模板。
  • 匹配优先级:industry+goods_role 优先,其次 industry,再其次 goods_role,最后默认绑定;同层级 priority 数字越小越优先。

本地当前绑定快照:

scene_key industry goods_role template_key priority active
section_content SERVICE section_content_service 100 true
section_content GOODS MANUFACTURER section_content_goods_manufacturer 100 true
section_content GOODS DISTRIBUTOR section_content_goods_distributor 100 true
section_content ENGINEERING section_content_engineering 100 true
writing_hint SERVICE writing_hint_service 100 true
writing_hint GOODS MANUFACTURER writing_hint_goods_manufacturer 100 true
writing_hint GOODS DISTRIBUTOR writing_hint_goods_distributor 100 true
writing_hint ENGINEERING writing_hint_engineering 100 true

同步建议:

  • 必须在模板导入后再导入绑定,因为绑定需要引用 prompt_templates.id
  • 导入时应通过 template_key 在目标环境查出 template_id,不要直接复制本地 template_id
  • 当前没有 document_parse/project_overview/scoring_criteria/outline_skeleton 的场景绑定;其中 document_parse 是按模板 key 直接读取,不需要绑定。

校验 SQL:

SELECT b.scene_key,
       b.industry::text,
       b.goods_role::text,
       t.key AS template_key,
       b.priority,
       b.is_active,
       b.remark,
       b.updated_at
FROM prompt_scene_bindings b
JOIN prompt_templates t ON t.id = b.template_id
ORDER BY b.scene_key, b.priority, b.industry NULLS FIRST, b.goods_role NULLS FIRST;

按策略同步的数据

兑换码/促销码:redeem_codes

运行时用途:

  • 下单时传入 redeem_code,命中后订单金额改为 amount_cents
  • 每个 code 只能核销一次,核销后写入 user_id/order_id/used_at

本地当前状态:

  • 当前本地没有 is_active = true AND used_at IS NULL 的可用兑换码。

同步建议:

  • 不建议直接同步本地 redeem_codes 全表,避免带入测试码、已使用码或测试用户绑定关系。
  • 若线上要发放内部码,应在上线前单独生成一批新 code,只同步 code/is_active/amount_cents,不要同步 user_id/order_id/used_at
  • code 格式受约束:12 位大写字母/数字,当前工具会避免容易混淆字符。

校验 SQL:

SELECT code, is_active, amount_cents, used_at, user_id, order_id, updated_at
FROM redeem_codes
ORDER BY updated_at DESC, code;

只看可用码:

SELECT code, amount_cents, updated_at
FROM redeem_codes
WHERE is_active = true AND used_at IS NULL
ORDER BY updated_at DESC, code;

代码随发布同步的数据

这些数据不需要手工从本地数据库同步,但需要确认线上镜像/代码包含最新文件。

app/prompts/**

仓库当前包含的 Prompt 文件会随代码发布,例如:

注意:

  • 当前部分运行时已经改为从 DB 表 prompt_templates 读取,仓库文件不等价于线上运行时 Prompt。
  • app/core/extraction/extractor.pydocument_parse 从 DB 读取模板 key:document_parse

Prompt 场景枚举

app/schemas/prompt_scene_binding.py 中的 PROMPT_SCENE_DEFINITIONS 随代码发布,不读库:

  • document_parse
  • project_overview
  • scoring_criteria
  • outline_skeleton
  • writing_hint
  • section_content

推荐上线流程

已整理一份可执行的幂等同步脚本:

exector/sync_fixed_data_to_prod.sql

脚本范围:system_configssubscription_plansprompt_templatesprompt_template_versionsprompt_scene_bindings。脚本不包含 redeem_codes

  1. 线上先跑结构迁移:
ENV_FILE=.env.prod uv run alembic upgrade head
  1. 在线上只读导出当前固定数据快照,保存备份:
CREATE EXTENSION IF NOT EXISTS pgcrypto;

SELECT * FROM system_configs;
SELECT * FROM subscription_plans ORDER BY sort_order, plan_code;
SELECT t.key, v.version, v.status, length(v.body), encode(digest(v.body, 'sha256'), 'hex')
FROM prompt_templates t
JOIN prompt_template_versions v ON v.template_id = t.id
ORDER BY t.key, v.version;
SELECT b.*, t.key AS template_key
FROM prompt_scene_bindings b
JOIN prompt_templates t ON t.id = b.template_id;
  1. 对比本文快照与线上快照,确认哪些以本地为准、哪些以线上运营配置为准。

  2. 先同步 system_configssubscription_plans

  3. 再同步 prompt_templates / prompt_template_versions

  4. 最后同步 prompt_scene_bindings,通过目标环境的 template_key -> template_id 解析外键。

  5. 兑换码只按活动需要新建,不做整表复制。

  6. 上线后跑 smoke test:

  7. 套餐列表能返回 7 个可售套餐,且价格、原价、字数、排序正确。

  8. 新用户首购 monthly_1 能正确计算首购价。
  9. 解析任务能读取 document_parse Prompt。
  10. 三四级目录、编写思路、章节正文分别覆盖 SERVICE/ENGINEERING/GOODS + MANUFACTURER/DISTRIBUTOR
  11. 有兑换码时,下单金额按 redeem_codes.amount_cents 生效且只能使用一次。

已发现的同步/代码风险

  1. exector/insert_subscription_plans.sql 是幂等脚本,但其中 monthly_3/yearly_1/yearly_2/yearly_3/topup_1original_price_cents 与本地库当前值不一致。
  2. app/services/billing/order_pricing_service.py 当前读取首购适用套餐时疑似使用了 FIRST_PURCHASE_DISCOUNT_KEY,导致拿到 0.85 后与 plan.plan_code 比较;这会让首购优惠无法命中。同步数据前后都需要单独修复或验证该逻辑。
  3. Prompt 模板表的 published_at 当前本地多为 NULLstatus = published;如果线上要依赖发布时间排序或审计,需要补齐发布动作或统一修复数据。