Files
qitongxue-api/internal/logic/migrate.go
2026-09-29 10:57:01 +08:00

366 lines
22 KiB
Go
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.
package logic
import (
"context"
"github.com/gogf/gf/v2/frame/g"
)
// ============================================================================
// 启动幂等迁移
//
// 为什么不用迁移工具:本项目部署方式是 `go run ./main.go`,仓库里也没有 goose/flyway,
// 而 manifest/config 被 gitignore 后运维手上常常只有一份二进制 + 数据库地址。
// 把「建表 + 加列」做成启动时的幂等 SQL,可以让升级只需要重启服务,不必再手工敲 DDL。
//
// 安全约定:
// - 全部语句必须可重复执行(CREATE TABLE IF NOT EXISTS / 先查 information_schema 再 ALTER)
// - 失败只告警不阻断启动:缺表会让相关接口报错,但不该连带整个服务起不来
// ============================================================================
type migrateTable struct {
name string
sql string
}
type migrateColumn struct {
table string
column string
sql string
}
// Migrate 启动时执行幂等迁移;失败仅告警。
func Migrate(ctx context.Context) {
for _, t := range migrateTables() {
if _, err := g.DB().Exec(ctx, t.sql); err != nil {
g.Log().Warningf(ctx, "[migrate] 建表 %s 失败: %v", t.name, err)
}
}
for _, c := range migrateColumns() {
exists, err := columnExists(ctx, c.table, c.column)
if err != nil {
g.Log().Warningf(ctx, "[migrate] 检查列 %s.%s 失败: %v", c.table, c.column, err)
continue
}
if exists {
continue
}
if _, err = g.DB().Exec(ctx, c.sql); err != nil {
g.Log().Warningf(ctx, "[migrate] 加列 %s.%s 失败: %v", c.table, c.column, err)
continue
}
g.Log().Infof(ctx, "[migrate] 已新增列 %s.%s", c.table, c.column)
}
}
// columnExists 用 information_schema 判断列是否已存在(DATABASE() 取当前库,避免写死库名)
func columnExists(ctx context.Context, table, column string) (bool, error) {
value, err := g.DB().GetValue(ctx,
"SELECT COUNT(*) FROM information_schema.COLUMNS "+
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?",
table, column)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
func migrateTables() []migrateTable {
return []migrateTable{
{"quota_packs", "CREATE TABLE IF NOT EXISTS `quota_packs` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`pack_key` varchar(64) NOT NULL COMMENT '档位标识,跨端契约'," +
"`name` varchar(64) NOT NULL DEFAULT '' COMMENT '展示名'," +
"`times` int NOT NULL DEFAULT 0 COMMENT '可用次数'," +
"`price_cents` bigint NOT NULL DEFAULT 0 COMMENT '价格(分)'," +
"`product_id` varchar(64) NOT NULL DEFAULT '' COMMENT '微信虚拟支付道具 ID'," +
"`valid_days` int NOT NULL DEFAULT 365 COMMENT '有效期天数,默认一年'," +
"`tool_key` varchar(64) NOT NULL DEFAULT '' COMMENT '绑定工具,空=通用档位'," +
"`badge` varchar(32) NOT NULL DEFAULT '' COMMENT '角标文案'," +
"`sort` int NOT NULL DEFAULT 0," +
"`is_enabled` tinyint NOT NULL DEFAULT 1," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_pack_key` (`pack_key`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='次数包档位(付费额度包)'"},
{"user_tool_quota", "CREATE TABLE IF NOT EXISTS `user_tool_quota` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`user_id` bigint NOT NULL," +
"`tool_key` varchar(64) NOT NULL," +
"`times_left` int NOT NULL DEFAULT 0 COMMENT '剩余次数'," +
"`total_bought` int NOT NULL DEFAULT 0 COMMENT '累计购买次数'," +
"`expire_at` datetime DEFAULT NULL COMMENT '到期时间,NULL 视为已过期'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_user_tool` (`user_id`,`tool_key`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户工具付费额度余额'"},
{"user_quota_usage", "CREATE TABLE IF NOT EXISTS `user_quota_usage` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`user_id` bigint NOT NULL," +
"`tool_key` varchar(64) NOT NULL," +
"`period_start` datetime NOT NULL COMMENT '当前周期起点'," +
"`period_end` datetime NOT NULL COMMENT '当前周期终点'," +
"`free_used` int NOT NULL DEFAULT 0 COMMENT '本周期已用免费额度'," +
"`member_used` int NOT NULL DEFAULT 0 COMMENT '本周期已用会员额度'," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_user_tool` (`user_id`,`tool_key`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户工具额度周期用量'"},
{"settings", "CREATE TABLE IF NOT EXISTS `settings` (" +
"`k` varchar(64) NOT NULL," +
"`v` varchar(255) NOT NULL DEFAULT ''," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`k`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台可改的全局配置'"},
// ===== 笔记域 =====
{"notes", "CREATE TABLE IF NOT EXISTS `notes` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`user_id` bigint NOT NULL," +
"`title` varchar(128) NOT NULL DEFAULT '' COMMENT '标题'," +
"`content` text COMMENT '正文'," +
"`tags` varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔'," +
"`is_pinned` tinyint NOT NULL DEFAULT 0 COMMENT '置顶'," +
"`remind_at` datetime DEFAULT NULL COMMENT '提醒时间,NULL=未设置'," +
"`remind_status` tinyint NOT NULL DEFAULT 0 COMMENT '0未设置 1待提醒 2已提醒 3发送失败 4已过期'," +
"`remind_sent_at` datetime DEFAULT NULL," +
"`remind_attempts` int NOT NULL DEFAULT 0," +
"`subscribe_count` int NOT NULL DEFAULT 0 COMMENT '已授权可发送次数'," +
"`deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除(不占额度)'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_user_active` (`user_id`,`deleted_at`)," +
"KEY `idx_remind` (`remind_status`,`remind_at`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='笔记'"},
// ===== 年会域:企业 / 员工 / 活动 =====
{"enterprises", "CREATE TABLE IF NOT EXISTS `enterprises` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`name` varchar(128) NOT NULL," +
"`contact` varchar(64) NOT NULL DEFAULT ''," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`status` tinyint NOT NULL DEFAULT 1," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`),KEY `idx_name` (`name`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业'"},
{"employees", "CREATE TABLE IF NOT EXISTS `employees` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`enterprise_id` bigint NOT NULL," +
"`name` varchar(64) NOT NULL," +
"`phone` varchar(20) NOT NULL," +
"`dept` varchar(64) NOT NULL DEFAULT ''," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`status` tinyint NOT NULL DEFAULT 1," +
"`deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'," +
"`deleted_from` text COMMENT '软删前 phone 原值(JSON),便于恢复'," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_ent_phone` (`enterprise_id`,`phone`)," +
"KEY `idx_ent_name` (`enterprise_id`,`name`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业员工库(跨活动复用)'"},
{"annual_events", "CREATE TABLE IF NOT EXISTS `annual_events` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`enterprise_id` bigint NOT NULL," +
"`title` varchar(128) NOT NULL," +
"`venue` varchar(128) NOT NULL DEFAULT ''," +
"`event_time` datetime DEFAULT NULL," +
"`status` varchar(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/published/ended'," +
"`hall_layout` text COMMENT '画布/舞台 JSON'," +
"`code_url` varchar(512) NOT NULL DEFAULT ''," +
"`layout_version` int NOT NULL DEFAULT 0," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_ent_status` (`enterprise_id`,`status`),KEY `idx_status` (`status`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='年会活动'"},
// ===== 年会域:候场 / 桌 / 座(导入/审计表由 T09/T10 追加)=====
{"event_participants", "CREATE TABLE IF NOT EXISTS `event_participants` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`event_id` bigint NOT NULL,`employee_id` bigint NOT NULL," +
"`created_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_event_emp` (`event_id`,`employee_id`)," +
"KEY `idx_event` (`event_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动候场名单'"},
{"event_tables", "CREATE TABLE IF NOT EXISTS `event_tables` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`event_id` bigint NOT NULL," +
"`table_no` varchar(16) NOT NULL DEFAULT '',`name` varchar(64) NOT NULL DEFAULT ''," +
"`x` int NOT NULL DEFAULT 0,`y` int NOT NULL DEFAULT 0," +
"`capacity` int NOT NULL DEFAULT 10,`rotation` int NOT NULL DEFAULT 0," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`),KEY `idx_event` (`event_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='圆桌'"},
{"event_seats", "CREATE TABLE IF NOT EXISTS `event_seats` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`event_id` bigint NOT NULL,`table_id` bigint NOT NULL," +
"`seat_no` int NOT NULL DEFAULT 0,`is_manual` tinyint NOT NULL DEFAULT 0," +
"`employee_id` bigint DEFAULT NULL COMMENT 'NULL=空位'," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_evt_tbl_seat` (`event_id`,`table_id`,`seat_no`)," +
"KEY `idx_event_emp` (`event_id`,`employee_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='座位'"},
{"event_import_logs", "CREATE TABLE IF NOT EXISTS `event_import_logs` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`enterprise_id` bigint NOT NULL,`event_id` bigint NOT NULL DEFAULT 0," +
"`file_name` varchar(255) NOT NULL DEFAULT ''," +
"`status` varchar(16) NOT NULL DEFAULT 'draft' COMMENT 'draft/committed/cancelled'," +
"`token` varchar(64) NOT NULL DEFAULT ''," +
"`snapshot` mediumtext COMMENT '解析行快照 JSON'," +
"`total` int NOT NULL DEFAULT 0,`inserted` int NOT NULL DEFAULT 0," +
"`updated` int NOT NULL DEFAULT 0,`skipped` int NOT NULL DEFAULT 0," +
"`conflict` int NOT NULL DEFAULT 0,`conflicts` text COMMENT '裁决结果 JSON'," +
"`operator` varchar(64) NOT NULL DEFAULT ''," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`),UNIQUE KEY `uk_token` (`token`)," +
"KEY `idx_event` (`event_id`),KEY `idx_ent` (`enterprise_id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导入记录/审计'"},
{"event_seat_queries", "CREATE TABLE IF NOT EXISTS `event_seat_queries` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`event_id` bigint NOT NULL," +
"`openid` varchar(64) NOT NULL DEFAULT '',`ip` varchar(64) NOT NULL DEFAULT ''," +
"`phone_masked` varchar(20) NOT NULL DEFAULT ''," +
"`result` tinyint NOT NULL DEFAULT 0 COMMENT '0失败 1成功'," +
"`created_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_evt_openid_time` (`event_id`,`openid`,`created_at`)," +
"KEY `idx_evt_ip_time` (`event_id`,`ip`,`created_at`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查座审计与限流'"},
// ===== 后台权限与操作审计(T17)=====
{"admin_audit_logs", "CREATE TABLE IF NOT EXISTS `admin_audit_logs` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`admin_id` bigint NOT NULL DEFAULT 0," +
"`admin_account` varchar(64) NOT NULL DEFAULT ''," +
"`admin_name` varchar(64) NOT NULL DEFAULT ''," +
"`role_value` varchar(32) NOT NULL DEFAULT ''," +
"`action` varchar(64) NOT NULL DEFAULT '' COMMENT '如 user.set_level / event.status'," +
"`target_type` varchar(32) NOT NULL DEFAULT '' COMMENT 'user/event/table/admin...'," +
"`target_id` varchar(64) NOT NULL DEFAULT ''," +
"`before_json` text COMMENT '操作前值 JSON'," +
"`after_json` text COMMENT '操作后值 JSON'," +
"`result` tinyint NOT NULL DEFAULT 1 COMMENT '0失败 1成功'," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`ip` varchar(64) NOT NULL DEFAULT ''," +
"`created_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_admin_time` (`admin_id`,`created_at`)," +
"KEY `idx_action_time` (`action`,`created_at`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台操作审计'"},
// ===== 商业化域:优惠码 / 优惠券(追加到 migrateTables())=====
{"promo_codes", "CREATE TABLE IF NOT EXISTS `promo_codes` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`code` varchar(32) NOT NULL COMMENT '码值(唯一,大写字母+数字,排除易混字符)'," +
"`product_id` varchar(64) NOT NULL DEFAULT 'admin_1' COMMENT '绑定的支付道具 ID(须与 MP 后台逐字符一致)'," +
"`price_cents` bigint NOT NULL DEFAULT 1 COMMENT '该道具价格(分)'," +
"`scope` tinyint NOT NULL DEFAULT 0 COMMENT '0全部 1指定会员套餐 2指定次数包'," +
"`scope_keys` varchar(512) NOT NULL DEFAULT '' COMMENT '适用范围 key 的 JSON 数组'," +
"`max_uses` int NOT NULL DEFAULT 0 COMMENT '总使用上限,0=不限'," +
"`used_count` int NOT NULL DEFAULT 0 COMMENT '已用次数'," +
"`per_user_limit` int NOT NULL DEFAULT 1 COMMENT '单人限用次数'," +
"`valid_from` datetime DEFAULT NULL,`valid_to` datetime DEFAULT NULL," +
"`status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0停用'," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`created_by` varchar(64) NOT NULL DEFAULT ''," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`),UNIQUE KEY `uk_code` (`code`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠码(免单/固定价档位,独立于优惠券)'"},
{"coupons", "CREATE TABLE IF NOT EXISTS `coupons` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`title` varchar(64) NOT NULL COMMENT '券名'," +
"`sub_title` varchar(128) NOT NULL DEFAULT '' COMMENT '副标题'," +
"`coupon_type` tinyint NOT NULL DEFAULT 2 COMMENT '1免单券(走支付) 2展示券(仅展示,默认)'," +
"`kind` tinyint NOT NULL DEFAULT 2 COMMENT '1满减 2免单(面额语义/展示)'," +
"`value` int NOT NULL DEFAULT 0 COMMENT '面额(分,仅展示用途)'," +
"`threshold_cents` int NOT NULL DEFAULT 0 COMMENT '使用门槛(分,0=无门槛)'," +
"`scope` tinyint NOT NULL DEFAULT 0 COMMENT '0全部 1指定会员套餐 2指定次数包'," +
"`scope_keys` varchar(512) NOT NULL DEFAULT ''," +
"`total_limit` int NOT NULL DEFAULT 0 COMMENT '总发行上限,0=不限'," +
"`per_user_limit` int NOT NULL DEFAULT 1 COMMENT '单人限领'," +
"`valid_from` datetime DEFAULT NULL,`valid_to` datetime DEFAULT NULL," +
"`status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0停用'," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`),KEY `idx_type_status` (`coupon_type`,`status`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='优惠券模板'"},
{"user_coupons", "CREATE TABLE IF NOT EXISTS `user_coupons` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`coupon_id` bigint NOT NULL,`user_id` bigint NOT NULL," +
"`coupon_title` varchar(64) NOT NULL DEFAULT '' COMMENT '券名快照(模板改名不影响已发券)'," +
"`coupon_type` tinyint NOT NULL DEFAULT 2 COMMENT '券型快照(模板改型不影响已发券)'," +
"`kind` tinyint NOT NULL DEFAULT 2,`value` int NOT NULL DEFAULT 0," +
"`threshold_cents` int NOT NULL DEFAULT 0," +
"`status` tinyint NOT NULL DEFAULT 0 COMMENT '0未使用 1已使用 2已过期 3已作废'," +
"`source` tinyint NOT NULL DEFAULT 1 COMMENT '1后台发放 2活动领取 3系统赠送'," +
"`granted_by` varchar(64) NOT NULL DEFAULT '' COMMENT '发放人 admin id'," +
"`granted_at` datetime DEFAULT NULL,`used_at` datetime DEFAULT NULL," +
"`expire_at` datetime DEFAULT NULL," +
"`used_order_no` varchar(64) NOT NULL DEFAULT '' COMMENT '占用/核销对应订单号'," +
"`used_by` varchar(64) NOT NULL DEFAULT '' COMMENT 'system=订单占用;admin=人工核销'," +
"`lock_at` datetime DEFAULT NULL COMMENT '下单占用时间(超时释放依据)'," +
"`remark` varchar(255) NOT NULL DEFAULT ''," +
"`created_at` datetime DEFAULT NULL,`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_user_status` (`user_id`,`status`)," +
"KEY `idx_coupon` (`coupon_id`)," +
"KEY `idx_user_lock` (`user_id`,`used_order_no`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户持有的优惠券(含券面快照 + 占用/核销记录)'"},
}
}
func migrateColumns() []migrateColumn {
return []migrateColumn{
// 工具额度配置:默认值让老数据在升级后立刻可用(免费 3 次/周期、会员 100 次/周期、系数 1)
{"tools", "free_quota", "ALTER TABLE `tools` ADD COLUMN `free_quota` int NOT NULL DEFAULT 3 COMMENT '免费额度(每周期次数)'"},
{"tools", "member_quota", "ALTER TABLE `tools` ADD COLUMN `member_quota` int NOT NULL DEFAULT 100 COMMENT '会员额度(每周期次数)'"},
{"tools", "pay_cost", "ALTER TABLE `tools` ADD COLUMN `pay_cost` int NOT NULL DEFAULT 1 COMMENT '付费额度每次消耗系数'"},
// 额度周期:0 = 跟随全局设置
{"users", "quota_period_days", "ALTER TABLE `users` ADD COLUMN `quota_period_days` int NOT NULL DEFAULT 0 COMMENT '额度重置周期(天),0=跟随全局'"},
{"member_plans", "quota_period_days", "ALTER TABLE `member_plans` ADD COLUMN `quota_period_days` int NOT NULL DEFAULT 0 COMMENT '购买后生效的额度周期(天),0=不调整'"},
// 订单表承载两类商品
{"member_orders", "order_type", "ALTER TABLE `member_orders` ADD COLUMN `order_type` tinyint NOT NULL DEFAULT 1 COMMENT '1=会员套餐 2=次数包'"},
{"member_orders", "pack_key", "ALTER TABLE `member_orders` ADD COLUMN `pack_key` varchar(64) NOT NULL DEFAULT '' COMMENT '次数包档位'"},
{"member_orders", "tool_key", "ALTER TABLE `member_orders` ADD COLUMN `tool_key` varchar(64) NOT NULL DEFAULT '' COMMENT '次数包绑定工具'"},
{"member_orders", "times", "ALTER TABLE `member_orders` ADD COLUMN `times` int NOT NULL DEFAULT 0 COMMENT '次数包次数'"},
// T17:管理员启停状态(默认启用,老数据升级后立即可用)
{"admin_users", "status", "ALTER TABLE `admin_users` ADD COLUMN `status` tinyint NOT NULL DEFAULT 1 COMMENT '0停用 1启用'"},
// ===== T18 软删除(deleted_at / deleted_from)=====
// users:唯一键 uk_openid / uk_username,软删时墓碑化并保留原值
{"users", "deleted_at", "ALTER TABLE `users` ADD COLUMN `deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'"},
{"users", "deleted_from", "ALTER TABLE `users` ADD COLUMN `deleted_from` text COMMENT '软删前唯一键原值(JSON),便于恢复'"},
// tools:软删仅置 deleted_at(不墓碑化 tool_key,见 admin_batch.go 说明)
{"tools", "deleted_at", "ALTER TABLE `tools` ADD COLUMN `deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'"},
// employees:唯一键 uk_ent_phone,软删时墓碑化 phone 并保留原值
{"employees", "deleted_at", "ALTER TABLE `employees` ADD COLUMN `deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'"},
{"employees", "deleted_from", "ALTER TABLE `employees` ADD COLUMN `deleted_from` text COMMENT '软删前 phone 原值(JSON),便于恢复'"},
// ===== member_orders 加列(追加到 migrateColumns();不加进 attach,依据约定 C4)=====
{"member_orders", "origin_price_cents", "ALTER TABLE `member_orders` ADD COLUMN `origin_price_cents` bigint NOT NULL DEFAULT 0 COMMENT '原价(分)'"},
{"member_orders", "paid_price_cents", "ALTER TABLE `member_orders` ADD COLUMN `paid_price_cents` bigint NOT NULL DEFAULT 0 COMMENT '实付(分)'"},
{"member_orders", "promo_code_id", "ALTER TABLE `member_orders` ADD COLUMN `promo_code_id` bigint NOT NULL DEFAULT 0 COMMENT '使用的优惠码 id,0=未使用'"},
{"member_orders", "promo_code", "ALTER TABLE `member_orders` ADD COLUMN `promo_code` varchar(32) NOT NULL DEFAULT '' COMMENT '优惠码值快照'"},
{"member_orders", "user_coupon_id", "ALTER TABLE `member_orders` ADD COLUMN `user_coupon_id` bigint NOT NULL DEFAULT 0 COMMENT '使用的免单券 user_coupons.id,0=未使用'"},
{"member_orders", "discount_cents", "ALTER TABLE `member_orders` ADD COLUMN `discount_cents` bigint NOT NULL DEFAULT 0 COMMENT '优惠金额(分)'"},
{"member_orders", "promo_kind", "ALTER TABLE `member_orders` ADD COLUMN `promo_kind` tinyint NOT NULL DEFAULT 0 COMMENT '使用凭证类型:0无 1优惠码 2免单券'"},
// ===== 订单退款/回收所购服务(退款能力)=====
// service_revoked:退款时是否已回收所购服务(用于列表展示 / 审计留痕 / 幂等判重)。
// refunded_at :退款时间(列表展示「退款时间」)。
{"member_orders", "service_revoked", "ALTER TABLE `member_orders` ADD COLUMN `service_revoked` tinyint NOT NULL DEFAULT 0 COMMENT '退款时是否已回收所购服务:0未回收 1已回收'"},
{"member_orders", "refunded_at", "ALTER TABLE `member_orders` ADD COLUMN `refunded_at` datetime DEFAULT NULL COMMENT '退款时间'"},
// ===== T04 座次:桌型 / 备注 / 排除(N4)=====
// 全部幂等加列;老活动无这些属性时回退默认值(普通桌 / 空备注 / 参与),保证老画布与自动排座不报错(S14)。
{"event_tables", "table_type", "ALTER TABLE `event_tables` ADD COLUMN `table_type` tinyint NOT NULL DEFAULT 0 COMMENT '桌型:0普通 1主桌 2签到台 3媒体席 4备用桌 9其他'"},
{"event_tables", "remark", "ALTER TABLE `event_tables` ADD COLUMN `remark` varchar(64) NOT NULL DEFAULT '' COMMENT '桌备注'"},
{"event_tables", "exclude_auto", "ALTER TABLE `event_tables` ADD COLUMN `exclude_auto` tinyint NOT NULL DEFAULT 0 COMMENT '1=不参与自动排序'"},
{"event_participants", "exclude_assign", "ALTER TABLE `event_participants` ADD COLUMN `exclude_assign` tinyint NOT NULL DEFAULT 0 COMMENT '1=不参与排座'"},
}
}