429 lines
21 KiB
Go
429 lines
21 KiB
Go
package host
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/xerr"
|
||
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||
)
|
||
|
||
// commandResult 读取写命令流水。
|
||
// 命中表示前一次业务事务已经提交,调用方必须按持久化结果重建返回值。
|
||
func commandResult(ctx context.Context, q sqlQueryer, commandID string) (hostdomain.CommandResult, bool, error) {
|
||
var result hostdomain.CommandResult
|
||
err := q.QueryRowContext(ctx, `
|
||
SELECT command_id, command_type, result_type, result_id, created_at_ms
|
||
FROM host_command_results
|
||
WHERE app_code = ? AND command_id = ?
|
||
`, appcode.FromContext(ctx), commandID).Scan(&result.CommandID, &result.CommandType, &result.ResultType, &result.ResultID, &result.CreatedAtMs)
|
||
if err == sql.ErrNoRows {
|
||
return hostdomain.CommandResult{}, false, nil
|
||
}
|
||
if err != nil {
|
||
return hostdomain.CommandResult{}, false, err
|
||
}
|
||
return result, true, nil
|
||
}
|
||
|
||
// insertCommandResult 必须放在每个状态机成功之后写入。
|
||
// 它必须和领域事实共用同一事务,避免重试看到半提交命令。
|
||
func insertCommandResult(ctx context.Context, tx *sql.Tx, result hostdomain.CommandResult) error {
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO host_command_results (app_code, command_id, command_type, result_type, result_id, created_at_ms)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), result.CommandID, result.CommandType, result.ResultType, result.ResultID, result.CreatedAtMs)
|
||
return err
|
||
}
|
||
|
||
// insertHostOutbox 在产生事实的同一事务里记录可靠集成事件。
|
||
// 消费者可以延迟或重建;真正的事实来源仍是和本行一起提交的领域表。
|
||
func insertHostOutbox(ctx context.Context, tx *sql.Tx, eventID int64, eventType string, aggregateType string, aggregateID int64, nowMs int64) error {
|
||
payload, err := json.Marshal(map[string]any{
|
||
"event_type": eventType,
|
||
"app_code": appcode.FromContext(ctx),
|
||
"aggregate_type": aggregateType,
|
||
"aggregate_id": aggregateID,
|
||
})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
_, err = tx.ExecContext(ctx, `
|
||
INSERT INTO host_outbox (app_code, event_id, event_type, aggregate_type, aggregate_id, status, payload_json, created_at_ms)
|
||
VALUES (?, ?, ?, ?, ?, 'pending', CAST(? AS JSON), ?)
|
||
`, appcode.FromContext(ctx), eventID, eventType, aggregateType, fmt.Sprintf("%d", aggregateID), string(payload), nowMs)
|
||
return err
|
||
}
|
||
|
||
func ensureHostProfileForMembership(ctx context.Context, tx *sql.Tx, userID int64, regionID int64, membershipID int64, agencyID int64, source string, nowMs int64) (hostdomain.HostProfile, bool, error) {
|
||
// host_profile 对每个用户只有一行,并且会跨 Agency 成员关系变化长期保留。
|
||
// FOR UPDATE 保证“创建身份或挂载当前成员关系”对同一用户是原子的。
|
||
profile, err := scanHostProfile(tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles WHERE app_code = ? AND user_id = ? FOR UPDATE`, hostProfileColumns), appcode.FromContext(ctx), userID))
|
||
if err == sql.ErrNoRows {
|
||
// 第一次通过申请时创建长期 Host 身份,并记录 first_became_host_at_ms。
|
||
profile = hostdomain.HostProfile{
|
||
UserID: userID,
|
||
Status: hostdomain.HostStatusActive,
|
||
RegionID: regionID,
|
||
CurrentAgencyID: agencyID,
|
||
CurrentMembershipID: membershipID,
|
||
Source: source,
|
||
FirstBecameHostAtMs: nowMs,
|
||
CreatedAtMs: nowMs,
|
||
UpdatedAtMs: nowMs,
|
||
}
|
||
return profile, true, insertHostProfile(ctx, tx, profile)
|
||
}
|
||
if err != nil {
|
||
return hostdomain.HostProfile{}, false, err
|
||
}
|
||
if profile.Status != hostdomain.HostStatusActive {
|
||
// disabled 不再算有效 Host 权限;当用户通过邀请或后台开通重新获得 Host 身份时,
|
||
// 复用原主键行并挂到本次新 membership,避免因为历史停用记录导致身份补齐失败。
|
||
profile.Status = hostdomain.HostStatusActive
|
||
profile.RegionID = regionID
|
||
profile.CurrentAgencyID = agencyID
|
||
profile.CurrentMembershipID = membershipID
|
||
profile.UpdatedAtMs = nowMs
|
||
return profile, true, updateHostProfileForMembership(ctx, tx, profile)
|
||
}
|
||
if profile.CurrentMembershipID > 0 {
|
||
// current_membership 是为“我的 Agency”快速读取做的冗余;非 0 表示已经挂载。
|
||
return hostdomain.HostProfile{}, false, xerr.New(xerr.Conflict, "host already has active agency membership")
|
||
}
|
||
// 复用已有身份行时不能重置最早成为主播的时间和来源。
|
||
profile.RegionID = regionID
|
||
profile.CurrentAgencyID = agencyID
|
||
profile.CurrentMembershipID = membershipID
|
||
profile.UpdatedAtMs = nowMs
|
||
return profile, false, updateHostCurrentMembership(ctx, tx, profile)
|
||
}
|
||
|
||
func ensureHostProfileForExistingMembership(ctx context.Context, tx *sql.Tx, userID int64, regionID int64, membership hostdomain.AgencyMembership, source string, nowMs int64) (hostdomain.HostProfile, bool, error) {
|
||
profile, ok, err := queryHostProfileByUserMaybe(ctx, tx, userID, true)
|
||
if err != nil {
|
||
return hostdomain.HostProfile{}, false, err
|
||
}
|
||
if !ok {
|
||
// 极端历史数据可能只有 active membership 没有 host_profile;这里补回 Host 身份,
|
||
// 但指针仍指向已有 membership,绝不把用户重新绑定到新创建的 Agency。
|
||
profile = hostdomain.HostProfile{
|
||
UserID: userID,
|
||
Status: hostdomain.HostStatusActive,
|
||
RegionID: regionID,
|
||
CurrentAgencyID: membership.AgencyID,
|
||
CurrentMembershipID: membership.MembershipID,
|
||
Source: source,
|
||
FirstBecameHostAtMs: nowMs,
|
||
CreatedAtMs: nowMs,
|
||
UpdatedAtMs: nowMs,
|
||
}
|
||
return profile, true, insertHostProfile(ctx, tx, profile)
|
||
}
|
||
changed := profile.Status != hostdomain.HostStatusActive || profile.CurrentAgencyID != membership.AgencyID || profile.CurrentMembershipID != membership.MembershipID
|
||
profile.Status = hostdomain.HostStatusActive
|
||
profile.RegionID = regionID
|
||
profile.CurrentAgencyID = membership.AgencyID
|
||
profile.CurrentMembershipID = membership.MembershipID
|
||
profile.UpdatedAtMs = nowMs
|
||
if !changed {
|
||
return profile, false, nil
|
||
}
|
||
return profile, true, updateHostProfileForMembership(ctx, tx, profile)
|
||
}
|
||
|
||
func insertHostProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.HostProfile) error {
|
||
// 这里只用于首次创建身份;成员关系变化走 updateHostCurrentMembership。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO host_profiles (
|
||
app_code, user_id, status, current_agency_id, current_membership_id, source,
|
||
first_became_host_at_ms, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), profile.UserID, profile.Status, nullableRegionID(profile.CurrentAgencyID), nullableRegionID(profile.CurrentMembershipID), profile.Source, profile.FirstBecameHostAtMs, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func updateHostCurrentMembership(ctx context.Context, tx *sql.Tx, profile hostdomain.HostProfile) error {
|
||
// 这里只更新可变的“当前成员关系指针”字段。
|
||
// 状态、来源、首次成为主播时间属于身份历史,不在这里重写。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE host_profiles
|
||
SET current_agency_id = ?, current_membership_id = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?
|
||
`, nullableRegionID(profile.CurrentAgencyID), nullableRegionID(profile.CurrentMembershipID), profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func disableHostProfileAfterAgencyKick(ctx context.Context, tx *sql.Tx, profile hostdomain.HostProfile) error {
|
||
// Agency 移除普通 Host 的产品语义是收回主播身份;历史行保留给审计和后续重新开通复用。
|
||
// 当前归属必须同步清空,否则 App 入口和送礼结算会继续把 ended membership 当作有效关系。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE host_profiles
|
||
SET status = ?, current_agency_id = NULL, current_membership_id = NULL, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?
|
||
`, profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func updateHostProfileForMembership(ctx context.Context, tx *sql.Tx, profile hostdomain.HostProfile) error {
|
||
// 恢复 disabled Host 或修复历史指针时,需要同时写 status 和当前归属;
|
||
// source/first_became_host_at_ms 仍然保留首次成为 Host 的历史语义。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE host_profiles
|
||
SET status = ?, current_agency_id = ?, current_membership_id = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?
|
||
`, profile.Status, nullableRegionID(profile.CurrentAgencyID), nullableRegionID(profile.CurrentMembershipID), profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func updateUserRegion(ctx context.Context, tx *sql.Tx, userID int64, regionID int64, nowMs int64) error {
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE users
|
||
SET region_id = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?
|
||
`, nullableRegionID(regionID), nowMs, appcode.FromContext(ctx), userID)
|
||
return err
|
||
}
|
||
|
||
func insertAgency(ctx context.Context, tx *sql.Tx, agency hostdomain.Agency) error {
|
||
// 第一阶段只有接受 Agency 邀请时会创建 Agency。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO agencies (
|
||
app_code, agency_id, owner_user_id, parent_bd_user_id, name, avatar, status,
|
||
join_enabled, max_hosts, created_by_user_id, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), agency.AgencyID, agency.OwnerUserID, agency.ParentBDUserID, agency.Name, agency.Avatar, agency.Status, agency.JoinEnabled, agency.MaxHosts, agency.CreatedByUserID, agency.CreatedAtMs, agency.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func updateAgencyAdminState(ctx context.Context, tx *sql.Tx, agency hostdomain.Agency) error {
|
||
// 后台 Phase 2 只允许调整 Agency 状态和入会开关;owner/parent_bd/region 不在这里迁移。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE agencies
|
||
SET status = ?, join_enabled = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND agency_id = ?
|
||
`, agency.Status, agency.JoinEnabled, agency.UpdatedAtMs, appcode.FromContext(ctx), agency.AgencyID)
|
||
return err
|
||
}
|
||
|
||
func updateAgencyProfile(ctx context.Context, tx *sql.Tx, agency hostdomain.Agency) error {
|
||
// Agency 资料是独立组织资料;这里绝不更新 users 表,也不从 owner 用户资料重新派生。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE agencies
|
||
SET name = ?, avatar = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND agency_id = ?
|
||
`, agency.Name, agency.Avatar, agency.UpdatedAtMs, appcode.FromContext(ctx), agency.AgencyID)
|
||
return err
|
||
}
|
||
|
||
func insertBDProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||
// 普通 BD 身份行和 host_profile 相互独立;BD 用户不会自动成为 Host,也不会隐式拥有 BD Leader 权限。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO bd_profiles (
|
||
app_code, user_id, role, parent_leader_user_id, status,
|
||
created_by_user_id, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), profile.UserID, profile.Role, nullableRegionID(profile.ParentLeaderUserID), profile.Status, profile.CreatedByUserID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func insertBDLeaderProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||
// BD Leader 使用独立表承载负责人身份,避免和普通 BD 工资、状态、团队归属混成同一条事实。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO bd_leader_profiles (
|
||
app_code, user_id, status,
|
||
created_by_user_id, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), profile.UserID, profile.Status, profile.CreatedByUserID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func updateBDProfileStatus(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||
// 状态变更只更新 status/updated_at_ms,不改角色、区域和上级 Leader 这些身份历史字段。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE bd_profiles
|
||
SET status = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ? AND role = 'bd'
|
||
`, profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func updateBDProfileForActivation(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||
// disabled 普通 BD 被重新授予权限时才允许重写父级 Leader;
|
||
// active BD 的 parent 代表既有业务关系,任何补齐逻辑都不能覆盖它。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE bd_profiles
|
||
SET parent_leader_user_id = ?, status = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ? AND role = 'bd'
|
||
`, nullableRegionID(profile.ParentLeaderUserID), profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func updateBDLeaderProfileStatus(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||
// Leader 状态只影响负责人权限和 ADMIN_SALARY_USD 钱包入口,不影响同用户可能存在的普通 BD 身份。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE bd_leader_profiles
|
||
SET status = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?
|
||
`, profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func ensureActiveBDLeaderProfile(ctx context.Context, tx *sql.Tx, userID int64, regionID int64, createdByUserID int64, nowMs int64) (hostdomain.BDProfile, string, error) {
|
||
profile, ok, err := queryBDLeaderProfileByUserMaybe(ctx, tx, userID, true)
|
||
if err != nil {
|
||
return hostdomain.BDProfile{}, "", err
|
||
}
|
||
if !ok {
|
||
// 直开 BD Leader 时,负责人身份先成为 active 事实;普通 BD 钱包/工资身份由后续 helper 补齐。
|
||
profile = hostdomain.BDProfile{
|
||
UserID: userID,
|
||
Role: hostdomain.BDRoleLeader,
|
||
RegionID: regionID,
|
||
Status: hostdomain.BDStatusActive,
|
||
CreatedByUserID: createdByUserID,
|
||
CreatedAtMs: nowMs,
|
||
UpdatedAtMs: nowMs,
|
||
}
|
||
if err := insertBDLeaderProfile(ctx, tx, profile); err != nil {
|
||
return hostdomain.BDProfile{}, "", mapHostDuplicateError(err)
|
||
}
|
||
return profile, "BDCreated", nil
|
||
}
|
||
if profile.Status == hostdomain.BDStatusActive {
|
||
// 已经是 active Leader 时只回放当前事实,不重写 created_by,也不重复发状态事件。
|
||
return profile, "", nil
|
||
}
|
||
profile.Status = hostdomain.BDStatusActive
|
||
profile.UpdatedAtMs = nowMs
|
||
if err := updateBDLeaderProfileStatus(ctx, tx, profile); err != nil {
|
||
return hostdomain.BDProfile{}, "", err
|
||
}
|
||
return profile, "BDStatusChanged", nil
|
||
}
|
||
|
||
func ensureSelfBDProfileForLeader(ctx context.Context, tx *sql.Tx, userID int64, regionID int64, createdByUserID int64, nowMs int64) (hostdomain.BDProfile, string, error) {
|
||
profile, ok, err := queryBDProfileByUserMaybe(ctx, tx, userID, true)
|
||
if err != nil {
|
||
return hostdomain.BDProfile{}, "", err
|
||
}
|
||
if !ok {
|
||
// BD Leader 需要普通 BD 身份承载下级 Agency/工资等 BD 体系能力;
|
||
// 首次补齐时把 parent_leader_user_id 指向自己,形成自有 BD Leader 体系。
|
||
profile = hostdomain.BDProfile{
|
||
UserID: userID,
|
||
Role: hostdomain.BDRoleBD,
|
||
RegionID: regionID,
|
||
ParentLeaderUserID: userID,
|
||
Status: hostdomain.BDStatusActive,
|
||
CreatedByUserID: createdByUserID,
|
||
CreatedAtMs: nowMs,
|
||
UpdatedAtMs: nowMs,
|
||
}
|
||
if err := insertBDProfile(ctx, tx, profile); err != nil {
|
||
return hostdomain.BDProfile{}, "", mapHostDuplicateError(err)
|
||
}
|
||
return profile, "BDCreated", nil
|
||
}
|
||
if profile.Status == hostdomain.BDStatusActive {
|
||
// 已有 active 普通 BD 说明用户已经在某个 BD 关系里;开通 Leader 不能覆盖原 parent。
|
||
return profile, "", nil
|
||
}
|
||
profile.ParentLeaderUserID = userID
|
||
profile.Status = hostdomain.BDStatusActive
|
||
profile.UpdatedAtMs = nowMs
|
||
if err := updateBDProfileForActivation(ctx, tx, profile); err != nil {
|
||
return hostdomain.BDProfile{}, "", err
|
||
}
|
||
return profile, "BDStatusChanged", nil
|
||
}
|
||
|
||
func insertCoinSellerProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.CoinSellerProfile) error {
|
||
// 币商身份是独立角色事实;账户余额属于 wallet-service,不在此表冗余。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO coin_seller_profiles (
|
||
app_code, user_id, status, merchant_asset_type,
|
||
created_by_user_id, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), profile.UserID, profile.Status, profile.MerchantAssetType, profile.CreatedByUserID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func updateCoinSellerProfileStatus(ctx context.Context, tx *sql.Tx, profile hostdomain.CoinSellerProfile) error {
|
||
// 启停币商身份只改状态;merchant_asset_type 是账户边界,创建后不能静默迁移。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE coin_seller_profiles
|
||
SET status = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND user_id = ?
|
||
`, profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||
return err
|
||
}
|
||
|
||
func insertAgencyMembership(ctx context.Context, tx *sql.Tx, membership hostdomain.AgencyMembership) error {
|
||
// 成员关系是 Agency 关系历史行;有效且当前的关系也会冗余到身份表。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO agency_memberships (
|
||
app_code, membership_id, agency_id, host_user_id, membership_type, status,
|
||
joined_at_ms, ended_at_ms, ended_by_user_id, ended_reason, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), membership.MembershipID, membership.AgencyID, membership.HostUserID, membership.MembershipType, membership.Status, membership.JoinedAtMs, nullableRegionID(membership.EndedAtMs), nullableRegionID(membership.EndedByUserID), membership.EndedReason, membership.CreatedAtMs, membership.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func endAgencyMembership(ctx context.Context, tx *sql.Tx, membership hostdomain.AgencyMembership) error {
|
||
// 结束成员关系只改状态,不删除历史行,便于审计和列表回看。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE agency_memberships
|
||
SET status = ?, ended_at_ms = ?, ended_by_user_id = ?, ended_reason = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND membership_id = ?
|
||
`, membership.Status, nullableRegionID(membership.EndedAtMs), nullableRegionID(membership.EndedByUserID), membership.EndedReason, membership.UpdatedAtMs, appcode.FromContext(ctx), membership.MembershipID)
|
||
return err
|
||
}
|
||
|
||
func insertAgencyApplication(ctx context.Context, tx *sql.Tx, application hostdomain.AgencyApplication) error {
|
||
// 申请记录只保留申请人和 Agency;区域每次读取时都从申请人 users 行投影。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO agency_applications (
|
||
app_code, application_id, command_id, applicant_user_id, agency_id, status,
|
||
reviewed_by_user_id, review_reason, reviewed_at_ms, created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), application.ApplicationID, application.CommandID, application.ApplicantUserID, application.AgencyID, application.Status, nullableRegionID(application.ReviewedByUserID), application.ReviewReason, nullableRegionID(application.ReviewedAtMs), application.CreatedAtMs, application.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func updateAgencyApplicationReview(ctx context.Context, tx *sql.Tx, application hostdomain.AgencyApplication) error {
|
||
// 审核更新是申请终态的唯一写入口。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE agency_applications
|
||
SET status = ?, reviewed_by_user_id = ?, review_reason = ?, reviewed_at_ms = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND application_id = ?
|
||
`, application.Status, nullableRegionID(application.ReviewedByUserID), application.ReviewReason, nullableRegionID(application.ReviewedAtMs), application.UpdatedAtMs, appcode.FromContext(ctx), application.ApplicationID)
|
||
return err
|
||
}
|
||
|
||
func insertRoleInvitation(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation) error {
|
||
// 邀请记录保留足够的上级 BD 信息,接受时不需要重新推导组织链路。
|
||
_, err := tx.ExecContext(ctx, `
|
||
INSERT INTO role_invitations (
|
||
app_code, invitation_id, command_id, invitation_type, status, inviter_user_id,
|
||
inviter_bd_user_id, target_user_id, agency_name, parent_bd_user_id,
|
||
parent_leader_user_id, processed_by_user_id, process_reason, processed_at_ms,
|
||
created_at_ms, updated_at_ms
|
||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, appcode.FromContext(ctx), invitation.InvitationID, invitation.CommandID, invitation.InvitationType, invitation.Status, invitation.InviterUserID, invitation.InviterBDUserID, invitation.TargetUserID, invitation.AgencyName, invitation.ParentBDUserID, nullableRegionID(invitation.ParentLeaderUserID), nullableRegionID(invitation.ProcessedByUserID), invitation.ProcessReason, nullableRegionID(invitation.ProcessedAtMs), invitation.CreatedAtMs, invitation.UpdatedAtMs)
|
||
return err
|
||
}
|
||
|
||
func updateRoleInvitationProcessed(ctx context.Context, tx *sql.Tx, invitation hostdomain.RoleInvitation) error {
|
||
// 处理邀请时统一记录处理人、原因和时间,覆盖接受、拒绝和取消。
|
||
_, err := tx.ExecContext(ctx, `
|
||
UPDATE role_invitations
|
||
SET status = ?, processed_by_user_id = ?, process_reason = ?, processed_at_ms = ?, updated_at_ms = ?
|
||
WHERE app_code = ? AND invitation_id = ?
|
||
`, invitation.Status, nullableRegionID(invitation.ProcessedByUserID), invitation.ProcessReason, nullableRegionID(invitation.ProcessedAtMs), invitation.UpdatedAtMs, appcode.FromContext(ctx), invitation.InvitationID)
|
||
return err
|
||
}
|