Files
qitongxue-api/internal/logic/migrate.go
2026-09-30 17:01:18 +08:00

601 lines
40 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='用户持有的优惠券(含券面快照 + 占用/核销记录)'"},
// ===== 软件开发服务域(迭代-2026-09-29)=====
{"goods", "CREATE TABLE IF NOT EXISTS `goods` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`category` tinyint NOT NULL DEFAULT 1 COMMENT '分类 1网站开发 2小程序开发 3App开发 4桌面程序开发'," +
"`title` varchar(128) NOT NULL DEFAULT '' COMMENT '服务名称'," +
"`summary` varchar(500) NOT NULL DEFAULT '' COMMENT '简介(列表卡片展示)'," +
"`content_md` mediumtext COMMENT '详细说明 Markdown 原文,小程序端解析渲染'," +
"`price_cents` bigint NOT NULL DEFAULT 0 COMMENT '定价(分),0=面议'," +
"`price_note` varchar(64) NOT NULL DEFAULT '' COMMENT '价格备注(如「起」「/项目」)'," +
"`cover_path` varchar(255) NOT NULL DEFAULT '' COMMENT '封面相对路径(/static/...),库内不存域名'," +
"`sort` int NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前'," +
"`is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '上架状态 0下架 1上架'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"`deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'," +
"PRIMARY KEY (`id`)," +
"KEY `idx_cat_sort` (`is_enabled`,`category`,`sort`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='软件开发服务商品'"},
{"attachments", "CREATE TABLE IF NOT EXISTS `attachments` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`scene` varchar(32) NOT NULL DEFAULT '' COMMENT '上传场景 avatar/cover/attachment'," +
"`file_name` varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名'," +
"`file_path` varchar(255) NOT NULL DEFAULT '' COMMENT '相对路径(/static/...),库内不存域名'," +
"`ext` varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名(小写含点)'," +
"`size_bytes` bigint NOT NULL DEFAULT 0 COMMENT '文件大小(字节)'," +
"`md5` char(32) NOT NULL DEFAULT '' COMMENT '内容 md5(落盘文件名)'," +
"`uploader_id` bigint NOT NULL DEFAULT 0 COMMENT '上传者 id(管理端或用户)'," +
"`uploader_type` varchar(16) NOT NULL DEFAULT 'admin' COMMENT '上传者类型 admin/user'," +
"`created_at` datetime DEFAULT NULL COMMENT '上传日期'," +
"`deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除(记录删除,文件本体按引用检查)'," +
"PRIMARY KEY (`id`)," +
"KEY `idx_created` (`created_at`)," +
"KEY `idx_path` (`file_path`)," +
"KEY `idx_scene` (`scene`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='上传附件登记(上传日期/上传者/大小,供后台附件管理)'"},
// ===== 软件开发服务域二期:介绍页模块 / 作品案例 / 报价申请 / 站内信(迭代-2026-09-30)=====
{"service_blocks", "CREATE TABLE IF NOT EXISTS `service_blocks` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`kind` varchar(16) NOT NULL DEFAULT 'rich' COMMENT '模块类型 hero/rich/image_text/image_list/developer/works/goods'," +
"`title` varchar(128) NOT NULL DEFAULT '' COMMENT '标题'," +
"`subtitle` varchar(255) NOT NULL DEFAULT '' COMMENT '副标题'," +
"`image_path` varchar(255) NOT NULL DEFAULT '' COMMENT '主图相对路径(hero/image_text/developer 头像),库内不存域名'," +
"`images` text COMMENT '图片列表 JSON 数组(相对路径,image_list 用)'," +
"`content` mediumtext COMMENT '正文(Markdown 原文,C 端解析渲染)'," +
"`sort` int NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前'," +
"`is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '启用 0停用 1启用'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_sort` (`is_enabled`,`sort`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='软件开发服务介绍页模块(后台可编辑,预设排版布局)'"},
{"service_works", "CREATE TABLE IF NOT EXISTS `service_works` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`category` tinyint NOT NULL DEFAULT 1 COMMENT '分类 1网站 2小程序 3App 4桌面(与 goods 对齐)'," +
"`title` varchar(128) NOT NULL DEFAULT '' COMMENT '作品名称'," +
"`summary` varchar(500) NOT NULL DEFAULT '' COMMENT '简介(列表卡片展示)'," +
"`content_md` mediumtext COMMENT '详情说明 Markdown 原文'," +
"`cover_path` varchar(255) NOT NULL DEFAULT '' COMMENT '封面相对路径'," +
"`images` text COMMENT '详情图集 JSON 数组(相对路径)'," +
"`price_cents` bigint NOT NULL DEFAULT 0 COMMENT '参考报价(分),0=面议'," +
"`price_note` varchar(64) NOT NULL DEFAULT '' COMMENT '报价备注(如「起」)'," +
"`sort` int NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前'," +
"`is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '上架状态 0下架 1上架'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"`deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'," +
"PRIMARY KEY (`id`)," +
"KEY `idx_cat_sort` (`is_enabled`,`category`,`sort`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='软件开发作品案例'"},
{"service_quotes", "CREATE TABLE IF NOT EXISTS `service_quotes` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`user_id` bigint NOT NULL," +
"`work_id` bigint NOT NULL DEFAULT 0 COMMENT '关联作品 id,0=通用申请'," +
"`work_title` varchar(128) NOT NULL DEFAULT '' COMMENT '作品名快照(作品改名/删除不影响申请单)'," +
"`contact` varchar(64) NOT NULL DEFAULT '' COMMENT '联系方式(微信/手机/QQ)'," +
"`requirement` varchar(1000) NOT NULL DEFAULT '' COMMENT '需求描述'," +
"`budget` varchar(64) NOT NULL DEFAULT '' COMMENT '预算范围(文案,如 5k-1w)'," +
"`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待处理 1已报价 2已成交 3已关闭'," +
"`quote_cents` bigint NOT NULL DEFAULT 0 COMMENT '报价金额(分),0=面议/未填'," +
"`quote_note` varchar(500) NOT NULL DEFAULT '' COMMENT '报价说明(用户端可见)'," +
"`handled_by` bigint NOT NULL DEFAULT 0 COMMENT '处理管理员 id,0=未处理'," +
"`handled_at` datetime DEFAULT NULL COMMENT '最近处理时间'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_user` (`user_id`,`id`)," +
"KEY `idx_status` (`status`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='软件开发报价申请'"},
{"user_messages", "CREATE TABLE IF NOT EXISTS `user_messages` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`user_id` bigint NOT NULL," +
"`type` varchar(32) NOT NULL DEFAULT 'system' COMMENT '类型 order_delivered/quote_submitted/quote_handled/feedback_replied/project_update/system'," +
"`title` varchar(128) NOT NULL DEFAULT '' COMMENT '消息标题'," +
"`content` varchar(500) NOT NULL DEFAULT '' COMMENT '消息正文'," +
"`link` varchar(255) NOT NULL DEFAULT '' COMMENT '跳转路径(小程序内路由,空=不跳转)'," +
"`is_read` tinyint NOT NULL DEFAULT 0 COMMENT '0未读 1已读'," +
"`created_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_user_read` (`user_id`,`is_read`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户站内信(会员开通/报价/反馈回复等通知)'"},
// ===== 软件开发域三期:开发项目(后台建单 → 用户扫码绑定 → 阶段/节点/交付物跟踪)=====
{"service_projects", "CREATE TABLE IF NOT EXISTS `service_projects` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`user_id` bigint NOT NULL DEFAULT 0 COMMENT '绑定用户 id,0=待扫码绑定'," +
"`bind_token` varchar(16) NOT NULL DEFAULT '' COMMENT '扫码绑定令牌(scene 携带,唯一)'," +
"`title` varchar(128) NOT NULL DEFAULT '' COMMENT '项目名称'," +
"`description` varchar(1000) NOT NULL DEFAULT '' COMMENT '需求描述'," +
"`price_cents` bigint NOT NULL DEFAULT 0 COMMENT '项目金额(分),0=面议'," +
"`price_note` varchar(64) NOT NULL DEFAULT '' COMMENT '金额备注'," +
"`due_note` varchar(64) NOT NULL DEFAULT '' COMMENT '交付周期文案(如「4 周」)'," +
"`status` tinyint NOT NULL DEFAULT 1 COMMENT '1进行中 2已交付 3已关闭'," +
"`code_url` varchar(255) NOT NULL DEFAULT '' COMMENT '绑定小程序码相对路径(/qrcode/...)'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"`deleted_at` datetime DEFAULT NULL COMMENT '软删除时间,NULL=未删除'," +
"PRIMARY KEY (`id`)," +
"UNIQUE KEY `uk_bind_token` (`bind_token`)," +
"KEY `idx_user` (`user_id`,`id`)," +
"KEY `idx_status` (`status`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='软件开发项目订单'"},
{"service_project_stages", "CREATE TABLE IF NOT EXISTS `service_project_stages` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`project_id` bigint NOT NULL," +
"`title` varchar(64) NOT NULL DEFAULT '' COMMENT '阶段名(如 原型设计/UI设计/开发/测试)'," +
"`status` tinyint NOT NULL DEFAULT 0 COMMENT '0未开始 1进行中 2已完成'," +
"`sort` int NOT NULL DEFAULT 0 COMMENT '排序值,越小越靠前'," +
"`is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '0隐藏 1显示'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_project` (`project_id`,`sort`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发项目阶段'"},
{"service_project_nodes", "CREATE TABLE IF NOT EXISTS `service_project_nodes` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`project_id` bigint NOT NULL," +
"`stage_id` bigint NOT NULL," +
"`title` varchar(64) NOT NULL DEFAULT '' COMMENT '小节点(可勾选事项)'," +
"`is_done` tinyint NOT NULL DEFAULT 0 COMMENT '0未完成 1已完成'," +
"`done_at` datetime DEFAULT NULL COMMENT '完成时间'," +
"`sort` int NOT NULL DEFAULT 0," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_stage` (`stage_id`,`sort`,`id`)," +
"KEY `idx_project` (`project_id`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发项目阶段小节点'"},
{"service_project_files", "CREATE TABLE IF NOT EXISTS `service_project_files` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`project_id` bigint NOT NULL," +
"`stage_id` bigint NOT NULL DEFAULT 0 COMMENT '所属阶段,0=项目级'," +
"`file_name` varchar(128) NOT NULL DEFAULT '' COMMENT '展示名(默认原始文件名)'," +
"`file_path` varchar(255) NOT NULL DEFAULT '' COMMENT '相对路径(/static/...),库内不存域名'," +
"`sort` int NOT NULL DEFAULT 0," +
"`created_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_stage` (`stage_id`,`sort`,`id`)," +
"KEY `idx_project` (`project_id`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='开发项目交付物(原型/UI 设计稿等图片)'"},
// ===== 软件开发域四期:维护模板 / 付款分期模板 / 项目分期(交付流程)=====
{"service_maintain_plans", "CREATE TABLE IF NOT EXISTS `service_maintain_plans` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`name` varchar(64) NOT NULL DEFAULT '' COMMENT '模板名(如 一年免费维护)'," +
"`free_years` int NOT NULL DEFAULT 0 COMMENT '免费维护年数,0=无免费'," +
"`yearly_fee_cents` bigint NOT NULL DEFAULT 0 COMMENT '后续维护费(元/年,分存储),0=免费无续费'," +
"`source_only` tinyint NOT NULL DEFAULT 0 COMMENT '1=仅交付源码无售后(不进维护期提醒)'," +
"`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`)," +
"KEY `idx_sort` (`is_enabled`,`sort`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维护模板(免费年限/年费/仅源码)'"},
{"service_pay_plans", "CREATE TABLE IF NOT EXISTS `service_pay_plans` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`name` varchar(64) NOT NULL DEFAULT '' COMMENT '模板名(如 两期·上线前结清)'," +
"`schedule` varchar(500) NOT NULL DEFAULT '' COMMENT '分期 JSON:[{seq,percent,trigger}],trigger=start/online_before/online_after/mid_demo'," +
"`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`)," +
"KEY `idx_sort` (`is_enabled`,`sort`,`id`)" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='付款分期模板(定金/尾款比例节点)'"},
{"service_project_pays", "CREATE TABLE IF NOT EXISTS `service_project_pays` (" +
"`id` bigint unsigned NOT NULL AUTO_INCREMENT," +
"`project_id` bigint NOT NULL," +
"`seq` int NOT NULL DEFAULT 1 COMMENT '第几期'," +
"`percent` int NOT NULL DEFAULT 0 COMMENT '付款比例(%)'," +
"`amount_cents` bigint NOT NULL DEFAULT 0 COMMENT '本期金额(分)= 项目金额 × 比例'," +
"`trigger` varchar(16) NOT NULL DEFAULT 'start' COMMENT '触发节点 start/online_before/online_after/mid_demo'," +
"`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付'," +
"`paid_at` datetime DEFAULT NULL," +
"`paid_note` varchar(64) NOT NULL DEFAULT '' COMMENT '收款备注(如 微信收款)'," +
"`created_at` datetime DEFAULT NULL," +
"`updated_at` datetime DEFAULT NULL," +
"PRIMARY KEY (`id`)," +
"KEY `idx_project` (`project_id`,`seq`,`id`)" +
") 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 '退款时间'"},
// ===== 真退款链路(调微信 /xpay/refund_order):退款单状态与金额落库 =====
// refund_no :商户退款单号(唯一;失败重发会换新号)
// refund_status :0未退款 1退款中 2退款成功 3退款失败(独立于订单 status)
// refund_amount :退款金额(分,全额退款 = paid_price_cents)
// refund_reason :退款原因(后台登记,iOS/手工退款必填)
// refund_finished_at:退款确认完成时间(回调 / 查单轮询 / 手工登记)
{"member_orders", "refund_no", "ALTER TABLE `member_orders` ADD COLUMN `refund_no` varchar(40) NOT NULL DEFAULT '' COMMENT '商户退款单号'"},
{"member_orders", "refund_status", "ALTER TABLE `member_orders` ADD COLUMN `refund_status` tinyint NOT NULL DEFAULT 0 COMMENT '退款状态:0未退款 1退款中 2退款成功 3退款失败'"},
{"member_orders", "refund_amount", "ALTER TABLE `member_orders` ADD COLUMN `refund_amount` bigint NOT NULL DEFAULT 0 COMMENT '退款金额(分)'"},
{"member_orders", "refund_reason", "ALTER TABLE `member_orders` ADD COLUMN `refund_reason` varchar(255) NOT NULL DEFAULT '' COMMENT '退款原因'"},
{"member_orders", "refund_finished_at", "ALTER TABLE `member_orders` ADD COLUMN `refund_finished_at` datetime DEFAULT NULL COMMENT '退款确认完成时间'"},
// ===== 反馈回复(官方回复 + 用户端未读提醒)=====
// reply_content:官方回复内容(后台「处理反馈」时填写,可多次更新)
// reply_by :回复管理员 id(审计留痕)
// reply_at :最近一次回复时间
// reply_read :用户端未读标记:0=有新回复未读(小红点) 1=已读
{"feedbacks", "reply_content", "ALTER TABLE `feedbacks` ADD COLUMN `reply_content` varchar(1000) NOT NULL DEFAULT '' COMMENT '官方回复内容'"},
{"feedbacks", "reply_by", "ALTER TABLE `feedbacks` ADD COLUMN `reply_by` bigint NOT NULL DEFAULT 0 COMMENT '回复管理员 id,0=无'"},
{"feedbacks", "reply_at", "ALTER TABLE `feedbacks` ADD COLUMN `reply_at` datetime DEFAULT NULL COMMENT '最近回复时间'"},
{"feedbacks", "reply_read", "ALTER TABLE `feedbacks` ADD COLUMN `reply_read` tinyint NOT NULL DEFAULT 0 COMMENT '用户端已读:0有新回复未读 1已读'"},
// ===== 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=不参与排座'"},
// ===== 开发项目四期:交付流程(维护模板 / 付款分期 / 验收维护期)=====
{"service_projects", "maintain_plan_id", "ALTER TABLE `service_projects` ADD COLUMN `maintain_plan_id` bigint NOT NULL DEFAULT 0 COMMENT '维护模板 id,0=未选择'"},
{"service_projects", "pay_plan_id", "ALTER TABLE `service_projects` ADD COLUMN `pay_plan_id` bigint NOT NULL DEFAULT 0 COMMENT '付款分期模板 id,0=未选择'"},
{"service_projects", "accepted", "ALTER TABLE `service_projects` ADD COLUMN `accepted` tinyint NOT NULL DEFAULT 0 COMMENT '0待验收 1已验收(用户手动确认)'"},
{"service_projects", "accepted_at", "ALTER TABLE `service_projects` ADD COLUMN `accepted_at` datetime DEFAULT NULL COMMENT '用户确认验收时间(维护期起点,后台可调 maintain_start_at)'"},
{"service_projects", "maintain_start_at", "ALTER TABLE `service_projects` ADD COLUMN `maintain_start_at` datetime DEFAULT NULL COMMENT '维护期起算日(默认=验收日,后台可调整)'"},
{"service_projects", "maintain_paid_until", "ALTER TABLE `service_projects` ADD COLUMN `maintain_paid_until` datetime DEFAULT NULL COMMENT '维护费已缴至(免费期=起点+免费年数;续费+1年)'"},
{"service_projects", "last_maintain_remind_at", "ALTER TABLE `service_projects` ADD COLUMN `last_maintain_remind_at` datetime DEFAULT NULL COMMENT '上次缴费提醒时间(隔 3 天一次的依据)'"},
}
}