|
|
@@ -37,6 +37,8 @@ type ActivityItem struct {
|
|
|
// "`signed_add` 把活动金额抄进 `signed.activity_money`" —— 种 0 的话
|
|
|
// "没抄"和"抄了 0"长得一样,那条断言就是空过。
|
|
|
MoneyCents int64
|
|
|
+ // ActivityMoneyUsedCents 种进 `signed.activity_money_used`(W2 的"额度已用尽"要靠它)。
|
|
|
+ ActivityMoneyUsedCents int64
|
|
|
// ResponseUserID >0 时给该报名行写 `response_userid`("已审核")。
|
|
|
//
|
|
|
// SC-2 要验 `type∈{2,3} 且 response_userid>0 → LEAVE`,这条守卫**只能**靠这个值触发。
|
|
|
@@ -58,6 +60,13 @@ type ActivitySeed struct {
|
|
|
// legacy 的 `t_courses_student.id` 就是旧 UUID,target 的 `id` 是 bigint 自增、
|
|
|
// 真正对应旧 UUID 的是 `biz_no`。用 biz_no 做锚点,两侧才指向同一行。
|
|
|
//
|
|
|
+ // UsedLogs 是本 case 要种的用量流水行(W2 的冲回路径靠它定位)。
|
|
|
+ //
|
|
|
+ // 为什么要种:`sp_courses_activity_refund_activity_recharge` 的游标是
|
|
|
+ // `WHERE orderid=? AND activity_money_used<0 ORDER BY id DESC` ——
|
|
|
+ // 没有这些行,冲回**根本进不去循环**,那条 case 就成了"两侧都 no-op"的空过。
|
|
|
+ UsedLogs []UsedLogSeed
|
|
|
+
|
|
|
// Items 非 nil 时**整体替换** `C1ActivityPlan` 的默认矩阵(case 自定义构型)。
|
|
|
//
|
|
|
// `ID` 留 0 即可:由 `buildPlan` 按 `base+i` 分配。**case 里不许写死 id** ——
|
|
|
@@ -108,6 +117,17 @@ func C1ActivityPlan(base int64, otherUsers map[int64]int32) *ActivityPlan {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// UsedLogSeed 是一条要种的用量流水。
|
|
|
+//
|
|
|
+// `OrderNo` 两侧写**同一个字符串**(legacy 写 `orderid`、target 写 `order_no`),
|
|
|
+// 这样两侧的游标才会定位到同一批行。
|
|
|
+type UsedLogSeed struct {
|
|
|
+ ActivityIndex int // plan.Items 的下标(id 在运行期才知道)
|
|
|
+ OrderNo string
|
|
|
+ MoneyCents int64 // 负数 = 消耗;正数 = 冲回
|
|
|
+ Memo string
|
|
|
+}
|
|
|
+
|
|
|
// buildPlan 按 case 的声明构造实际 plan(id 由 base 决定)。
|
|
|
//
|
|
|
// 两条路径:
|
|
|
@@ -280,12 +300,22 @@ VALUES (?, ?, ?, ?, ?, 1, ?, NOW())`,
|
|
|
// `activity_money` 与 `response_userid` 是活动链要验的两列:
|
|
|
// 前者验"`signed_add` 把活动金额抄进来",后者是"已审核不可取消"那条守卫的唯一触发器。
|
|
|
if err := tx.Exec(`
|
|
|
-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 {
|
|
|
+INSERT INTO t_courses_activity_signed (activityid, userid, status, activity_money, activity_money_used, response_userid, create_time)
|
|
|
+VALUES (?, ?, ?, ?, ?, ?, NOW())`,
|
|
|
+ it.ID, uid, it.SignedStatus, yuan(it.MoneyCents), yuan(it.ActivityMoneyUsedCents), it.ResponseUserID).Error; err != nil {
|
|
|
return fmt.Errorf("种子 legacy 报名 %d 失败:%w", it.ID, err)
|
|
|
}
|
|
|
}
|
|
|
+ // 用量流水(W2 冲回路径的定位依据)。两侧 `orderid` / `order_no` 写同一个字符串。
|
|
|
+ for _, ul := range c.Activity.UsedLogs {
|
|
|
+ aid := p.Items[ul.ActivityIndex].ID
|
|
|
+ if err := tx.Exec(`
|
|
|
+INSERT INTO t_courses_activity_used_log (activityid, userid, recommend_userid, orderid, activity_money_used, create_time, memo)
|
|
|
+VALUES (?, ?, 0, ?, ?, NOW(), ?)`,
|
|
|
+ aid, uid, firstOr(ul.OrderNo, c.StuCourseID), yuan(ul.MoneyCents), ul.Memo).Error; err != nil {
|
|
|
+ return fmt.Errorf("种子 legacy 用量流水失败:%w", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
for otherID, st := range p.OtherUsers {
|
|
|
// 其他用户报名在"应被失效"的活动上 —— 验证 SP 的 `WHERE userid=inputUserid` 不会波及旁人。
|
|
|
if err := tx.Exec(`
|
|
|
@@ -326,12 +356,23 @@ VALUES (?, ?, ?, ?, ?, 1, ?, NOW(3), NOW(3))`,
|
|
|
respUID := sqlNullUID(it.ResponseUserID)
|
|
|
if err := tx.Exec(`
|
|
|
INSERT INTO t_courses_activity_signed
|
|
|
- (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 {
|
|
|
+ (activity_id, user_id, institution_id, campus_id, status, activity_money, activity_money_used, response_user_id, create_time)
|
|
|
+VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(3))`,
|
|
|
+ it.ID, uid, it.InstitutionID, it.CampusID, it.SignedStatus,
|
|
|
+ yuan(it.MoneyCents), yuan(it.ActivityMoneyUsedCents), respUID).Error; err != nil {
|
|
|
return fmt.Errorf("种子 target 报名 %d 失败:%w", it.ID, err)
|
|
|
}
|
|
|
}
|
|
|
+ // 用量流水:`order_no` 与 legacy 的 `orderid` 同一个字符串(两侧游标靠它定位)。
|
|
|
+ for _, ul := range c.Activity.UsedLogs {
|
|
|
+ aid := p.Items[ul.ActivityIndex].ID
|
|
|
+ if err := tx.Exec(`
|
|
|
+INSERT INTO t_courses_activity_used_log (activity_id, user_id, order_no, activity_money_used, create_time, memo)
|
|
|
+VALUES (?, ?, ?, ?, NOW(3), ?)`,
|
|
|
+ aid, uid, firstOr(ul.OrderNo, fmt.Sprint(c.StuCourseNumericID)), yuan(ul.MoneyCents), ul.Memo).Error; err != nil {
|
|
|
+ return fmt.Errorf("种子 target 用量流水失败:%w", err)
|
|
|
+ }
|
|
|
+ }
|
|
|
for otherID, st := range p.OtherUsers {
|
|
|
otherUID := CanonicalUIDFor(otherID)
|
|
|
// 其他用户也需要 t_user 行(t_courses_activity_signed 的逻辑 FK)
|