|
@@ -31,6 +31,16 @@ type ActivityItem struct {
|
|
|
InstitutionID int64 // 旧 schoolid
|
|
InstitutionID int64 // 旧 schoolid
|
|
|
CampusID int64 // 0 = 全机构池(所有自营校区)
|
|
CampusID int64 // 0 = 全机构池(所有自营校区)
|
|
|
SignedStatus int32 // 该用户在本次活动上的报名状态(只对"本 case 用户"有意义)
|
|
SignedStatus int32 // 该用户在本次活动上的报名状态(只对"本 case 用户"有意义)
|
|
|
|
|
+ // MoneyCents 是活动金额(`t_courses_activity.money`,元→分)。
|
|
|
|
|
+ //
|
|
|
|
|
+ // 之前所有活动都种 0.00(C1/C2-10 不关心它),但活动链的 W1 要验
|
|
|
|
|
+ // "`signed_add` 把活动金额抄进 `signed.activity_money`" —— 种 0 的话
|
|
|
|
|
+ // "没抄"和"抄了 0"长得一样,那条断言就是空过。
|
|
|
|
|
+ MoneyCents int64
|
|
|
|
|
+ // ResponseUserID >0 时给该报名行写 `response_userid`("已审核")。
|
|
|
|
|
+ //
|
|
|
|
|
+ // SC-2 要验 `type∈{2,3} 且 response_userid>0 → LEAVE`,这条守卫**只能**靠这个值触发。
|
|
|
|
|
+ ResponseUserID int64
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ActivitySeed 是"这个 case 要建活动种子"的声明。
|
|
// ActivitySeed 是"这个 case 要建活动种子"的声明。
|
|
@@ -48,6 +58,12 @@ type ActivitySeed struct {
|
|
|
// legacy 的 `t_courses_student.id` 就是旧 UUID,target 的 `id` 是 bigint 自增、
|
|
// legacy 的 `t_courses_student.id` 就是旧 UUID,target 的 `id` 是 bigint 自增、
|
|
|
// 真正对应旧 UUID 的是 `biz_no`。用 biz_no 做锚点,两侧才指向同一行。
|
|
// 真正对应旧 UUID 的是 `biz_no`。用 biz_no 做锚点,两侧才指向同一行。
|
|
|
//
|
|
//
|
|
|
|
|
+ // Items 非 nil 时**整体替换** `C1ActivityPlan` 的默认矩阵(case 自定义构型)。
|
|
|
|
|
+ //
|
|
|
|
|
+ // `ID` 留 0 即可:由 `buildPlan` 按 `base+i` 分配。**case 里不许写死 id** ——
|
|
|
|
|
+ // 写死会把自增水位永久推高,而且两侧会指向不同的活动行。
|
|
|
|
|
+ Items []ActivityItem
|
|
|
|
|
+
|
|
|
// 只有 `use_activity_recommend`(W4)需要它:它嵌调两次
|
|
// 只有 `use_activity_recommend`(W4)需要它:它嵌调两次
|
|
|
// `sp_courses_student_counteract_arrears_course`,写的是 `t_courses_student` 的
|
|
// `sp_courses_student_counteract_arrears_course`,写的是 `t_courses_student` 的
|
|
|
// `net_receipts` / `remainder_receivable`(第三个域)。
|
|
// `net_receipts` / `remainder_receivable`(第三个域)。
|
|
@@ -92,6 +108,23 @@ func C1ActivityPlan(base int64, otherUsers map[int64]int32) *ActivityPlan {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// buildPlan 按 case 的声明构造实际 plan(id 由 base 决定)。
|
|
|
|
|
+//
|
|
|
|
|
+// 两条路径:
|
|
|
|
|
+// - `Items == nil` → 用 C1 的标准矩阵(C1/C2-10 的行为**一字不改**)
|
|
|
|
|
+// - `Items != nil` → 用 case 给的项,`ID` 按 `base+i` 覆盖
|
|
|
|
|
+func (s *ActivitySeed) buildPlan(base int64) *ActivityPlan {
|
|
|
|
|
+ if len(s.Items) == 0 {
|
|
|
|
|
+ return C1ActivityPlan(base, s.OtherUsers)
|
|
|
|
|
+ }
|
|
|
|
|
+ items := make([]ActivityItem, len(s.Items))
|
|
|
|
|
+ for i, it := range s.Items {
|
|
|
|
|
+ it.ID = base + int64(i)
|
|
|
|
|
+ items[i] = it
|
|
|
|
|
+ }
|
|
|
|
|
+ return &ActivityPlan{BaseID: base, Items: items, OtherUsers: s.OtherUsers}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ActivityIDs 返回本 plan 全部活动 id(升序)。
|
|
// ActivityIDs 返回本 plan 全部活动 id(升序)。
|
|
|
func (p *ActivityPlan) ActivityIDs() []int64 {
|
|
func (p *ActivityPlan) ActivityIDs() []int64 {
|
|
|
if p == nil {
|
|
if p == nil {
|
|
@@ -238,15 +271,18 @@ func seedLegacyActivities(ctx context.Context, tx *gorm.DB, c Case, uid int64) e
|
|
|
if err := tx.Exec(`
|
|
if err := tx.Exec(`
|
|
|
INSERT INTO t_courses_activity (id, type, schoolid, campusid, money, status, create_time)
|
|
INSERT INTO t_courses_activity (id, type, schoolid, campusid, money, status, create_time)
|
|
|
VALUES (?, ?, ?, ?, ?, 1, NOW())`,
|
|
VALUES (?, ?, ?, ?, ?, 1, NOW())`,
|
|
|
- it.ID, it.Type, it.InstitutionID, it.CampusID, "0.00").Error; err != nil {
|
|
|
|
|
|
|
+ it.ID, it.Type, it.InstitutionID, it.CampusID, yuan(it.MoneyCents)).Error; err != nil {
|
|
|
return fmt.Errorf("种子 legacy 活动 %d 失败:%w", it.ID, err)
|
|
return fmt.Errorf("种子 legacy 活动 %d 失败:%w", it.ID, err)
|
|
|
}
|
|
}
|
|
|
if it.SignedStatus < 0 {
|
|
if it.SignedStatus < 0 {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
+ // `activity_money` 与 `response_userid` 是活动链要验的两列:
|
|
|
|
|
+ // 前者验"`signed_add` 把活动金额抄进来",后者是"已审核不可取消"那条守卫的唯一触发器。
|
|
|
if err := tx.Exec(`
|
|
if err := tx.Exec(`
|
|
|
-INSERT INTO t_courses_activity_signed (activityid, userid, status, create_time)
|
|
|
|
|
-VALUES (?, ?, ?, NOW())`, it.ID, uid, it.SignedStatus).Error; err != nil {
|
|
|
|
|
|
|
+INSERT INTO t_courses_activity_signed (activityid, userid, status, activity_money, response_userid, create_time)
|
|
|
|
|
+VALUES (?, ?, ?, ?, ?, NOW())`,
|
|
|
|
|
+ it.ID, uid, it.SignedStatus, yuan(it.MoneyCents), it.ResponseUserID).Error; err != nil {
|
|
|
return fmt.Errorf("种子 legacy 报名 %d 失败:%w", it.ID, err)
|
|
return fmt.Errorf("种子 legacy 报名 %d 失败:%w", it.ID, err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -278,17 +314,21 @@ func seedTargetActivities(ctx context.Context, tx *gorm.DB, c Case, uid string)
|
|
|
if err := tx.Exec(`
|
|
if err := tx.Exec(`
|
|
|
INSERT INTO t_courses_activity (id, type, institution_id, campus_id, money, status, create_time, updated_at)
|
|
INSERT INTO t_courses_activity (id, type, institution_id, campus_id, money, status, create_time, updated_at)
|
|
|
VALUES (?, ?, ?, ?, ?, 1, NOW(3), NOW(3))`,
|
|
VALUES (?, ?, ?, ?, ?, 1, NOW(3), NOW(3))`,
|
|
|
- it.ID, it.Type, it.InstitutionID, it.CampusID, "0.00").Error; err != nil {
|
|
|
|
|
|
|
+ it.ID, it.Type, it.InstitutionID, it.CampusID, yuan(it.MoneyCents)).Error; err != nil {
|
|
|
return fmt.Errorf("种子 target 活动 %d 失败:%w", it.ID, err)
|
|
return fmt.Errorf("种子 target 活动 %d 失败:%w", it.ID, err)
|
|
|
}
|
|
}
|
|
|
if it.SignedStatus < 0 {
|
|
if it.SignedStatus < 0 {
|
|
|
continue
|
|
continue
|
|
|
}
|
|
}
|
|
|
|
|
+ // `recommend_user_id` / `response_user_id` 在 target 是 `varchar(26) NULL`:
|
|
|
|
|
+ // 0 必须写成 **NULL**(不是 '0',也不是空串)—— 迁移口径里 0 与 NULL 同义,
|
|
|
|
|
+ // 写成空串会让 `assertIdentityZeroNull` 报出一条假差异。
|
|
|
|
|
+ respUID := sqlNullUID(it.ResponseUserID)
|
|
|
if err := tx.Exec(`
|
|
if err := tx.Exec(`
|
|
|
INSERT INTO t_courses_activity_signed
|
|
INSERT INTO t_courses_activity_signed
|
|
|
- (activity_id, user_id, institution_id, campus_id, status, create_time)
|
|
|
|
|
-VALUES (?, ?, ?, ?, ?, NOW(3))`,
|
|
|
|
|
- it.ID, uid, it.InstitutionID, it.CampusID, it.SignedStatus).Error; err != nil {
|
|
|
|
|
|
|
+ (activity_id, user_id, institution_id, campus_id, status, activity_money, response_user_id, create_time)
|
|
|
|
|
+VALUES (?, ?, ?, ?, ?, ?, ?, NOW(3))`,
|
|
|
|
|
+ it.ID, uid, it.InstitutionID, it.CampusID, it.SignedStatus, yuan(it.MoneyCents), respUID).Error; err != nil {
|
|
|
return fmt.Errorf("种子 target 报名 %d 失败:%w", it.ID, err)
|
|
return fmt.Errorf("种子 target 报名 %d 失败:%w", it.ID, err)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -447,3 +487,12 @@ SELECT COUNT(*) FROM t_courses_activity_signed s
|
|
|
}
|
|
}
|
|
|
return n, nil
|
|
return n, nil
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// sqlNullUID 把 legacy int identity 转成 target 的 `varchar(26) NULL`:
|
|
|
|
|
+// `0` ⇒ NULL(迁移口径:0 与 NULL 同义),>0 ⇒ ULID。
|
|
|
|
|
+func sqlNullUID(legacyID int64) any {
|
|
|
|
|
+ if legacyID <= 0 {
|
|
|
|
|
+ return nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return CanonicalUIDFor(legacyID)
|
|
|
|
|
+}
|