Files
junhong_cmp_fiber/scripts/migration/lib/shop_sql.py
break 370fd3e67f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m49s
update
2026-09-03 09:28:28 +08:00

186 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""店铺批量导入 SQL 构造。"""
from __future__ import annotations
from datetime import datetime
from hashlib import sha256
from .shop_import import ShopImportPlan, ShopImportRow, initial_password_hash
def build_shop_sql(plan: ShopImportPlan) -> str:
"""生成包含店铺初始事实的单事务 SQL。"""
lines = [
"-- 店铺批量导入 SQL执行前必须审核结果清单与摘要。",
f"-- 批次: {plan.config.batch_id}",
f"-- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"-- 建议执行: psql -1 -v ON_ERROR_STOP=1 -f <此文件>",
"",
"-- psql -1 开启的事务内先执行以下守卫。",
"",
*_preflight_sql(plan),
]
for row in plan.rows:
lines.extend(_row_sql(row, plan))
lines.append("")
return "\n".join(lines)
def _preflight_sql(plan: ShopImportPlan) -> list[str]:
config = plan.config
mapped_salespeople = []
seen_salespeople: set[str] = set()
for row in plan.rows:
if row.salesperson not in seen_salespeople:
seen_salespeople.add(row.salesperson)
mapped_salespeople.append((row.line_no, row.salesperson, config.salesperson_account_ids[row.salesperson]))
target_rows = ",\n ".join(
f"({row.line_no}, {_sql_str(row.shop_code)}, {_sql_str(row.username)}, {_sql_str(row.phone)})"
for row in plan.rows
)
salesperson_rows = ",\n ".join(
f"({line_no}, {_sql_str(name)}, {account_id})" for line_no, name, account_id in mapped_salespeople
)
return [
"-- 所有目标库守卫必须在业务写入前完成。",
"DO $shop_import_guard$",
"DECLARE",
" v_row record;",
"BEGIN",
" IF NOT EXISTS (SELECT 1 FROM tb_role WHERE id = " + str(config.default_role_id) + " AND role_type = 2 AND status = 1 AND deleted_at IS NULL) THEN",
f" RAISE EXCEPTION '导入配置错误: default_role_id={config.default_role_id} 不是启用的客户角色';",
" END IF;",
" IF NOT EXISTS (SELECT 1 FROM tb_account WHERE id = " + str(config.operator_id) + " AND user_type = 1 AND status = 1 AND deleted_at IS NULL) THEN",
f" RAISE EXCEPTION '导入配置错误: operator_id={config.operator_id} 不是启用的超级管理员';",
" END IF;",
" FOR v_row IN",
" VALUES",
f" {salesperson_rows}",
" LOOP",
" IF NOT EXISTS (SELECT 1 FROM tb_account WHERE id = v_row.column3 AND user_type = 2 AND status = 1 AND deleted_at IS NULL) THEN",
" RAISE EXCEPTION 'CSV 第 % 行业务员映射无效: % (账号 ID=%)', v_row.column1, v_row.column2, v_row.column3;",
" END IF;",
" END LOOP;",
" FOR v_row IN",
" VALUES",
f" {target_rows}",
" LOOP",
" IF EXISTS (SELECT 1 FROM tb_shop WHERE shop_code = v_row.column2 AND deleted_at IS NULL) THEN",
" RAISE EXCEPTION 'CSV 第 % 行店铺编号已存在: %', v_row.column1, v_row.column2;",
" END IF;",
" IF EXISTS (SELECT 1 FROM tb_account WHERE username = v_row.column3 AND deleted_at IS NULL) THEN",
" RAISE EXCEPTION 'CSV 第 % 行用户名已存在: %', v_row.column1, v_row.column3;",
" END IF;",
" IF EXISTS (SELECT 1 FROM tb_account WHERE phone = v_row.column4 AND deleted_at IS NULL) THEN",
" RAISE EXCEPTION 'CSV 第 % 行手机号已存在: %', v_row.column1, v_row.column4;",
" END IF;",
" END LOOP;",
"END;",
"$shop_import_guard$;",
"",
]
def _row_sql(row: ShopImportRow, plan: ShopImportPlan) -> list[str]:
config = plan.config
parent_expr = "NULL" if not row.parent_shop_code else f"(SELECT id FROM tb_shop WHERE shop_code = {_sql_str(row.parent_shop_code)} AND deleted_at IS NULL)"
password_hash = initial_password_hash(row.phone)
return [
f"-- CSV 第 {row.line_no} 行: {_sql_str(row.shop_name)}",
"DO $shop_import$",
"DECLARE",
" v_shop_id bigint;",
" v_account_id bigint;",
" v_event_id bigint;",
" v_parent_id bigint;",
" v_operator_name varchar;",
"BEGIN",
f" SELECT username INTO v_operator_name FROM tb_account WHERE id = {config.operator_id};",
" INSERT INTO tb_shop (creator, updater, shop_name, shop_code, parent_id, business_owner_account_id, level, contact_name, contact_phone, province, city, district, address, status, client_login_disabled, created_at, updated_at)",
" VALUES ("
f"{config.operator_id}, {config.operator_id}, {_sql_str(row.shop_name)}, {_sql_str(row.shop_code)}, {parent_expr}, "
f"{config.salesperson_account_ids[row.salesperson]}, {row.level}, {_sql_str(row.agent_name)}, {_sql_str(row.phone)}, '', '', '', '', 1, FALSE, NOW(), NOW())",
" RETURNING id, parent_id INTO v_shop_id, v_parent_id;",
"",
" INSERT INTO tb_account (creator, updater, username, phone, password, user_type, shop_id, is_primary, status, created_at, updated_at)",
" VALUES ("
f"{config.operator_id}, {config.operator_id}, {_sql_str(row.username)}, {_sql_str(row.phone)}, {_sql_str(password_hash)}, 3, v_shop_id, TRUE, 1, NOW(), NOW())",
" RETURNING id INTO v_account_id;",
"",
" INSERT INTO tb_account_role (account_id, role_id, status, creator, updater, created_at, updated_at)",
f" VALUES (v_account_id, {config.default_role_id}, 1, {config.operator_id}, {config.operator_id}, NOW(), NOW());",
"",
" INSERT INTO tb_shop_role (shop_id, role_id, status, creator, updater, created_at, updated_at)",
f" VALUES (v_shop_id, {config.default_role_id}, 1, {config.operator_id}, {config.operator_id}, NOW(), NOW());",
"",
" INSERT INTO tb_agent_wallet (shop_id, wallet_type, balance, frozen_balance, credit_enabled, credit_limit, currency, status, version, shop_id_tag)",
" SELECT v_shop_id, 'main', 0, 0, default_credit_enabled, default_credit_limit, 'CNY', 1, 0, v_shop_id",
f" FROM tb_role WHERE id = {config.default_role_id} AND deleted_at IS NULL;",
"",
" INSERT INTO tb_agent_wallet (shop_id, wallet_type, balance, frozen_balance, credit_enabled, credit_limit, currency, status, version, shop_id_tag)",
" VALUES (v_shop_id, 'commission', 0, 0, FALSE, 0, 'CNY', 1, 0, v_shop_id);",
*_audit_sql(row, plan),
"END;",
"$shop_import$;",
"",
]
def _audit_sql(row: ShopImportRow, plan: ShopImportPlan) -> list[str]:
config = plan.config
owner_id = config.salesperson_account_ids[row.salesperson]
created_event_id, created_hash = _audit_ids(config.batch_id, row.line_no, "shop.create")
owner_event_id, owner_hash = _audit_ids(config.batch_id, row.line_no, "shop.update_business_owner")
identity = (
"jsonb_build_object('id', v_shop_id, 'shop_code', " + _sql_str(row.shop_code) + ", 'shop_name', " + _sql_str(row.shop_name) + ", "
"'parent_id', v_parent_id, 'level', " + str(row.level) + ")"
)
after = (
"jsonb_build_object('shop_name', " + _sql_str(row.shop_name) + ", 'contact_name', " + _sql_str(row.agent_name) + ", "
"'contact_phone', " + _sql_str(row.phone) + ", 'province', '', 'city', '', 'district', '', 'address', '', "
"'shop_code', " + _sql_str(row.shop_code) + ", 'parent_id', v_parent_id, 'level', " + str(row.level) + ")"
)
parent_resource = [
" IF v_parent_id IS NOT NULL THEN",
" INSERT INTO tb_audit_event_resource (audit_event_id, resource_type, resource_id, resource_key, display_name, relation, role, identity_snapshot, before_data, after_data, subject_visibility, subject_summary, subject_data, sort_order)",
" SELECT v_event_id, 'shop', id::text, id::text, shop_name, 'reference', 'shop_parent',",
" jsonb_build_object('id', id, 'shop_code', shop_code, 'shop_name', shop_name, 'parent_id', parent_id, 'level', level),",
" '{}'::jsonb, '{}'::jsonb, 'internal_only', '', '{}'::jsonb, 0",
" FROM tb_shop WHERE id = v_parent_id;",
" END IF;",
]
return [
"",
" INSERT INTO tb_audit_event (event_id, occurred_at, category, action_code, action_name, summary, actor_kind, actor_id, actor_name, source, scope_type, result, risk_level, metadata, content_hash)",
" VALUES ("
f"{_sql_str(created_event_id)}, NOW(), 'business', 'shop.create', '创建店铺', '创建店铺', 'account', '{config.operator_id}', v_operator_name, "
f"'admin_api', 'platform', 'success', 'normal', jsonb_build_object('batch_id', {_sql_str(config.batch_id)}, 'csv_line_no', {row.line_no}), {_sql_str(created_hash)})",
" RETURNING id INTO v_event_id;",
" INSERT INTO tb_audit_event_resource (audit_event_id, resource_type, resource_id, resource_key, display_name, relation, role, identity_snapshot, before_data, after_data, subject_visibility, subject_summary, subject_data, sort_order)",
" VALUES (v_event_id, 'shop', v_shop_id::text, v_shop_id::text, " + _sql_str(row.shop_name) + ", 'primary', 'shop_target', " + identity + ", '{}'::jsonb, " + after + ", 'internal_only', '', '{}'::jsonb, 0);",
*parent_resource,
"",
" INSERT INTO tb_audit_event (event_id, occurred_at, category, action_code, action_name, summary, actor_kind, actor_id, actor_name, source, scope_type, result, risk_level, metadata, content_hash)",
" VALUES ("
f"{_sql_str(owner_event_id)}, NOW(), 'business', 'shop.update_business_owner', '更新店铺业务员归属', '设置店铺业务员归属', 'account', '{config.operator_id}', v_operator_name, "
f"'admin_api', 'platform', 'success', 'normal', jsonb_build_object('batch_id', {_sql_str(config.batch_id)}, 'csv_line_no', {row.line_no}), {_sql_str(owner_hash)})",
" RETURNING id INTO v_event_id;",
" INSERT INTO tb_audit_event_resource (audit_event_id, resource_type, resource_id, resource_key, display_name, relation, role, identity_snapshot, before_data, after_data, subject_visibility, subject_summary, subject_data, sort_order)",
" VALUES (v_event_id, 'shop', v_shop_id::text, v_shop_id::text, " + _sql_str(row.shop_name) + ", 'primary', 'shop_target', " + identity + ", '{}'::jsonb, " + f"jsonb_build_object('business_owner_account_id', {owner_id})" + ", 'subject_result', '店铺业务员归属已设置', '{}'::jsonb, 0);",
*parent_resource,
" INSERT INTO tb_audit_event_resource (audit_event_id, resource_type, resource_id, resource_key, display_name, relation, role, identity_snapshot, before_data, after_data, subject_visibility, subject_summary, subject_data, sort_order)",
" SELECT v_event_id, 'account', id::text, id::text, username, 'reference', 'shop_business_owner',",
" jsonb_build_object('id', id, 'username', username, 'phone', phone, 'user_type', user_type, 'shop_id', shop_id, 'enterprise_id', enterprise_id, 'wecom_userid', wecom_userid, 'wecom_name', wecom_name),",
" jsonb_build_object('assigned', FALSE), jsonb_build_object('assigned', TRUE), 'internal_only', '', '{}'::jsonb, 1",
f" FROM tb_account WHERE id = {owner_id};",
]
def _audit_ids(batch_id: str, line_no: int, action: str) -> tuple[str, str]:
seed = f"{batch_id}:{line_no}:{action}"
digest = sha256(seed.encode()).hexdigest()
return f"evt_{digest[:32]}", digest
def _sql_str(value: str) -> str:
return "'" + value.replace("'", "''") + "'"