feat(spec-3): complete section 3.1/3.2/3.3 activity verify settlement, role_flags, and distribution_relations
这个提交包含在:
@@ -267,12 +267,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import AppTabBar from '@/components/CustomTabBar.vue';
|
||||
import SkeletonScreen from '@/components/SkeletonScreen.vue';
|
||||
import PrivacyModal from '@/components/PrivacyModal.vue';
|
||||
import HomePopupModal from '@/components/HomePopupModal.vue';
|
||||
import { getCompanions, getBanners, getNavCategories } from '@/common/api.js';
|
||||
import { getCompanions, getBanners, getNavCategories, bindInviteCode } from '@/common/api.js';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
@@ -403,6 +403,20 @@ const onThemeChanged = (key) => {
|
||||
applyThemeToNativeUI(key);
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
const rawInvite = options && (options.inviter_id || options.inviterId || options.code || options.scene);
|
||||
if (rawInvite) {
|
||||
const decoded = decodeURIComponent(String(rawInvite));
|
||||
uni.setStorageSync('PENDING_INVITER_CODE', decoded);
|
||||
const uid = uni.getStorageSync('userId');
|
||||
if (uid) {
|
||||
bindInviteCode(uid, decoded).then(() => {
|
||||
uni.removeStorageSync('PENDING_INVITER_CODE');
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
navInfo.value = getNavBarInfo();
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
|
||||
@@ -390,7 +390,7 @@ import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, setTheme, THEME_KEYS, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getCurrentRole, setCurrentRole, ROLE_TYPES, toggleRole as toggleUserRole, isExpertCertified, getExpertCertStatus, setExpertCertStatus, CERT_STATUS } from '@/common/role.js';
|
||||
import { getNavBarInfo } from '@/common/nav.js';
|
||||
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert, getUserWithdrawals, getFavorites, removeFavorite, getDistributionTeam, getDistributionRecords, getMyReports } from '@/common/api.js';
|
||||
import { getUserProfile, rechargeBalance, withdrawCommission, getUserFinanceRecords, updateUserProfile, uploadImage, getRechargeTiers, getPlatformConfigs, applyExpertCert, getUserWithdrawals, getFavorites, removeFavorite, getDistributionTeam, getDistributionRecords, getMyReports, bindInviteCode } from '@/common/api.js';
|
||||
|
||||
const DEFAULT_GUEST_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
|
||||
const isLoggedIn = ref(true);
|
||||
@@ -624,6 +624,20 @@ const loadUserProfile = async (targetId) => {
|
||||
...(uni.getStorageSync('PW_USER_INFO') || {}),
|
||||
...user.value
|
||||
});
|
||||
// 若通过小程序太阳码海报进入带有待绑定的推荐人标识且当前尚未绑定上级,首次登录自动建立父子关联
|
||||
const pendingInviter = uni.getStorageSync('PENDING_INVITER_CODE');
|
||||
if (pendingInviter && !profile.inviterId) {
|
||||
bindInviteCode(uid, pendingInviter)
|
||||
.then((res) => {
|
||||
uni.removeStorageSync('PENDING_INVITER_CODE');
|
||||
if (res && res.inviterId) {
|
||||
user.value.inviterId = res.inviterId;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
uni.removeStorageSync('PENDING_INVITER_CODE');
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[User] 加载用户信息异常:', err.message);
|
||||
|
||||
@@ -458,12 +458,16 @@ export async function initDatabase(): Promise<void> {
|
||||
try {
|
||||
await conn.query('ALTER TABLE users ADD COLUMN is_expert TINYINT DEFAULT 0');
|
||||
} catch (_) {}
|
||||
try {
|
||||
await conn.query('ALTER TABLE users ADD COLUMN role_flags TINYINT DEFAULT 1');
|
||||
await conn.query('UPDATE users SET role_flags = 3 WHERE is_expert = 1 OR role = "认证大神"');
|
||||
} catch (_) {}
|
||||
// 确保存在 demo 账号 101 (金牌陪伴师/认证大神体验账号)
|
||||
try {
|
||||
await conn.query(`
|
||||
INSERT INTO users (id, user_no, nickname, avatar, phone, gender, role, balance, commission, invited_count, order_count, activity_count, status, wechat_openid, signature, is_expert)
|
||||
VALUES (101, 'U101', '糖心小糯米', 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200', '138****0101', '女', '认证大神', 2580.00, 128.50, 6, 88, 16, '正常', 'wx_openid_101', '人美声甜,擅长各类同城陪伴与开黑互动', 1)
|
||||
ON DUPLICATE KEY UPDATE nickname = VALUES(nickname), is_expert = 1
|
||||
INSERT INTO users (id, user_no, nickname, avatar, phone, gender, role, balance, commission, invited_count, order_count, activity_count, status, wechat_openid, signature, is_expert, role_flags)
|
||||
VALUES (101, 'U101', '糖心小糯米', 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200', '138****0101', '女', '认证大神', 2580.00, 128.50, 6, 88, 16, '正常', 'wx_openid_101', '人美声甜,擅长各类同城陪伴与开黑互动', 1, 3)
|
||||
ON DUPLICATE KEY UPDATE nickname = VALUES(nickname), is_expert = 1, role_flags = 3
|
||||
`);
|
||||
} catch (_) {}
|
||||
|
||||
@@ -517,11 +521,24 @@ export async function initDatabase(): Promise<void> {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
// 23. 分销体系扩展 (下级团队与佣金订单记录)
|
||||
// 23. 分销体系扩展 (父子终身绑定关系表、下级团队与佣金订单记录)
|
||||
try {
|
||||
await conn.query('ALTER TABLE users ADD COLUMN inviter_id INT DEFAULT NULL');
|
||||
} catch (_) {}
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS distribution_relations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNIQUE NOT NULL,
|
||||
inviter_id INT NOT NULL,
|
||||
level2_inviter_id INT DEFAULT NULL,
|
||||
bind_source VARCHAR(64) DEFAULT 'INVITE_CODE',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_inviter (inviter_id),
|
||||
INDEX idx_level2_inviter (level2_inviter_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS distribution_records (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
|
||||
@@ -120,6 +120,84 @@ export class MysqlRepository {
|
||||
return this.getActivityById(id);
|
||||
}
|
||||
|
||||
private verifyCodeCache = new Map<string, { participantId: string; userId: number; expireAt: number }>();
|
||||
|
||||
private async settleActivityCheckinAndReferral(
|
||||
activityId: string,
|
||||
creatorId: number,
|
||||
participantId: string,
|
||||
participantUserId: number,
|
||||
payAmountCents: number
|
||||
): Promise<void> {
|
||||
const feeYuan = Number((Number(payAmountCents || 0) / 100).toFixed(2));
|
||||
if (feeYuan <= 0) return;
|
||||
|
||||
try {
|
||||
const ts = Date.now();
|
||||
const rand = () => Math.floor(100 + Math.random() * 900);
|
||||
|
||||
// 1. 触发活动费用清算逻辑:将核销款项划入活动发起人账户并记录清算流水
|
||||
if (creatorId > 0) {
|
||||
await pool.query(
|
||||
'UPDATE users SET balance = COALESCE(balance, 0) + ?, activity_count = COALESCE(activity_count, 0) + 1 WHERE id = ?',
|
||||
[feeYuan, creatorId]
|
||||
);
|
||||
await pool.query(
|
||||
`INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id, created_at)
|
||||
VALUES (?, ?, ?, '活动核销费用清算', ?, 'IN', ?, ?, NOW())`,
|
||||
[
|
||||
`fin_act_${ts}_${rand()}`,
|
||||
`AC${ts}${rand()}`,
|
||||
creatorId,
|
||||
feeYuan,
|
||||
`参与搭子 ${participantUserId} (凭证 ${participantId})`,
|
||||
activityId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 付费活动完成后触发一级(5%) / 二级(2%)分销返佣
|
||||
const cfg = await this.getFinanceConfig();
|
||||
const firstRefPct = Number(cfg.firstReferralPercent ?? 5);
|
||||
const secondRefPct = Number(cfg.secondReferralPercent ?? 2);
|
||||
|
||||
const [buyerRows]: any = await pool.query(
|
||||
'SELECT id, nickname, inviter_id FROM users WHERE id = ? LIMIT 1',
|
||||
[participantUserId]
|
||||
);
|
||||
const buyer = buyerRows && buyerRows[0];
|
||||
const fromUserName = buyer?.nickname || `搭子_${participantUserId}`;
|
||||
|
||||
if (buyer && buyer.inviter_id && firstRefPct > 0) {
|
||||
const lv1UserId = Number(buyer.inviter_id);
|
||||
const comm1 = Number(((feeYuan * firstRefPct) / 100).toFixed(2));
|
||||
if (comm1 > 0 && lv1UserId > 0 && lv1UserId !== participantUserId) {
|
||||
await pool.query('UPDATE users SET commission = COALESCE(commission, 0) + ? WHERE id = ?', [comm1, lv1UserId]);
|
||||
await pool.query(
|
||||
`INSERT INTO distribution_records (id, user_id, from_user_id, from_user_name, order_no, order_amount, commission, level, rate_percent, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, '已入账', NOW())`,
|
||||
[`dst_act1_${ts}_${rand()}`, lv1UserId, participantUserId, fromUserName, activityId, feeYuan, comm1, firstRefPct]
|
||||
);
|
||||
}
|
||||
|
||||
const [lv1Rows]: any = await pool.query('SELECT id, inviter_id FROM users WHERE id = ? LIMIT 1', [lv1UserId]);
|
||||
const lv1User = lv1Rows && lv1Rows[0];
|
||||
if (lv1User && lv1User.inviter_id && secondRefPct > 0) {
|
||||
const lv2UserId = Number(lv1User.inviter_id);
|
||||
const comm2 = Number(((feeYuan * secondRefPct) / 100).toFixed(2));
|
||||
if (comm2 > 0 && lv2UserId > 0 && lv2UserId !== participantUserId && lv2UserId !== lv1UserId) {
|
||||
await pool.query('UPDATE users SET commission = COALESCE(commission, 0) + ? WHERE id = ?', [comm2, lv2UserId]);
|
||||
await pool.query(
|
||||
`INSERT INTO distribution_records (id, user_id, from_user_id, from_user_name, order_no, order_amount, commission, level, rate_percent, status, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 2, ?, '已入账', NOW())`,
|
||||
[`dst_act2_${ts}_${rand()}`, lv2UserId, participantUserId, fromUserName, activityId, feeYuan, comm2, secondRefPct]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async joinActivity(activityId: string, params: { userId: number; payAmountCents: number }): Promise<any> {
|
||||
const act = await this.getActivityById(activityId);
|
||||
if (!act) throw new Error('活动不存在');
|
||||
@@ -148,6 +226,14 @@ export class MysqlRepository {
|
||||
VALUES (?, ?, ?, ?, ?, 0)
|
||||
`, [ptId, activityId, params.userId, params.payAmountCents, verifyCode]);
|
||||
|
||||
// 建立核销码缓存索引 act:verify:{activity_id}:{verify_code} -> participant_id (TTL 24h)
|
||||
const cacheKey = `act:verify:${activityId}:${verifyCode}`;
|
||||
this.verifyCodeCache.set(cacheKey, {
|
||||
participantId: ptId,
|
||||
userId: params.userId,
|
||||
expireAt: Date.now() + 24 * 3600 * 1000
|
||||
});
|
||||
|
||||
// 写入一条财务流水
|
||||
const finId = `fin_${Date.now()}`;
|
||||
await pool.query(`
|
||||
@@ -161,6 +247,7 @@ export class MysqlRepository {
|
||||
userId: params.userId,
|
||||
payAmountCents: params.payAmountCents,
|
||||
verifyCode,
|
||||
cacheKey,
|
||||
checkinStatus: false,
|
||||
};
|
||||
}
|
||||
@@ -185,18 +272,36 @@ export class MysqlRepository {
|
||||
throw new Error('该核销码已核销过,不可重复核销(防重校验已生效)');
|
||||
}
|
||||
|
||||
await pool.query(`
|
||||
// CAS 原子状态跃迁校验:防止高并发重复核销
|
||||
const [updateRes]: any = await pool.query(`
|
||||
UPDATE participants
|
||||
SET checkin_status = 1, verified_at = NOW(), verified_by = ?
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND checkin_status = 0
|
||||
`, [operatorId, pt.id]);
|
||||
|
||||
if (!updateRes || updateRes.affectedRows === 0) {
|
||||
throw new Error('该核销码已核销过,不可重复核销(防重校验已生效)');
|
||||
}
|
||||
|
||||
const cacheKey = `act:verify:${activityId}:${verifyCode}`;
|
||||
this.verifyCodeCache.delete(cacheKey);
|
||||
|
||||
// 触发活动费用清算逻辑(划入发起人账户)及二级分销返佣
|
||||
await this.settleActivityCheckinAndReferral(
|
||||
activityId,
|
||||
Number(act.creatorId),
|
||||
pt.id,
|
||||
Number(pt.user_id),
|
||||
Number(pt.pay_amount_cents || 0)
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
participantId: pt.id,
|
||||
activityId,
|
||||
userId: pt.user_id,
|
||||
verifyCode,
|
||||
settledFeeYuan: Number((Number(pt.pay_amount_cents || 0) / 100).toFixed(2)),
|
||||
verifiedAt: new Date(),
|
||||
operatorId,
|
||||
};
|
||||
@@ -209,7 +314,7 @@ export class MysqlRepository {
|
||||
p.id as participantId, p.activity_id as activityId, p.user_id as userId,
|
||||
p.pay_amount_cents as payAmountCents, p.verify_code as verifyCode,
|
||||
p.checkin_status as checkinStatus,
|
||||
a.title as activityTitle, a.game_category as gameCategory
|
||||
a.title as activityTitle, a.game_category as gameCategory, a.creator_id as creatorId
|
||||
FROM participants p
|
||||
LEFT JOIN activities a ON p.activity_id = a.id
|
||||
WHERE p.verify_code = ?
|
||||
@@ -222,6 +327,9 @@ export class MysqlRepository {
|
||||
type: 'ACTIVITY',
|
||||
id: pt.participantId,
|
||||
relatedId: pt.activityId,
|
||||
userId: pt.userId,
|
||||
creatorId: pt.creatorId,
|
||||
payAmountCents: pt.payAmountCents,
|
||||
nickname: `搭子_${pt.userId}`,
|
||||
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
|
||||
title: pt.activityTitle || '同城组局活动',
|
||||
@@ -267,11 +375,22 @@ export class MysqlRepository {
|
||||
}
|
||||
|
||||
if (info.type === 'ACTIVITY') {
|
||||
await pool.query(`
|
||||
const [updateRes]: any = await pool.query(`
|
||||
UPDATE participants
|
||||
SET checkin_status = 1, verified_at = NOW(), verified_by = ?
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND checkin_status = 0
|
||||
`, [operatorId, info.id]);
|
||||
if (!updateRes || updateRes.affectedRows === 0) {
|
||||
throw new Error(`核销码 [${verifyCode}] 已完成现场签到,不可重复核销(防重校验已生效)`);
|
||||
}
|
||||
this.verifyCodeCache.delete(`act:verify:${info.relatedId}:${verifyCode}`);
|
||||
await this.settleActivityCheckinAndReferral(
|
||||
String(info.relatedId),
|
||||
Number(info.creatorId || operatorId),
|
||||
String(info.id),
|
||||
Number(info.userId || 0),
|
||||
Number(info.payAmountCents || Math.round(Number(info.fee || 0) * 100))
|
||||
);
|
||||
} else if (info.type === 'ORDER') {
|
||||
await pool.query(`
|
||||
UPDATE orders
|
||||
@@ -669,15 +788,17 @@ export class MysqlRepository {
|
||||
const [rows]: any = await pool.query('SELECT * FROM companions WHERE id = ?', [id]);
|
||||
const comp = rows && rows[0];
|
||||
|
||||
// 联动更新关联用户的角色与大神状态
|
||||
// 联动更新关联用户的角色与大神状态及双身份标识 role_flags (玩家:1, 大神:2, 双身份:3)
|
||||
if (comp && comp.user_id) {
|
||||
const uId = Number(comp.user_id);
|
||||
if (uId > 0) {
|
||||
try {
|
||||
if (action === 'approve') {
|
||||
await pool.query('UPDATE users SET role = "认证大神", is_expert = 1 WHERE id = ?', [uId]);
|
||||
await pool.query('UPDATE users SET role_flags = 3 WHERE id = ?', [uId]).catch(() => {});
|
||||
} else {
|
||||
await pool.query('UPDATE users SET role = "普通用户", is_expert = 0 WHERE id = ?', [uId]);
|
||||
await pool.query('UPDATE users SET role_flags = 1 WHERE id = ?', [uId]).catch(() => {});
|
||||
}
|
||||
} catch (_) {
|
||||
if (action === 'approve') {
|
||||
@@ -1034,6 +1155,12 @@ export class MysqlRepository {
|
||||
}
|
||||
|
||||
async claimOrder(orderId: string, expertData: { expertId: number; companionName: string; expertAvatar?: string }): Promise<any> {
|
||||
if (expertData.expertId) {
|
||||
const comp = await this.resolveCompanionRow(expertData.expertId);
|
||||
if (comp && comp.audit_status && comp.audit_status !== '已通过') {
|
||||
throw new Error('大神档案未通过实名认证审核,无法接单');
|
||||
}
|
||||
}
|
||||
await pool.query(`
|
||||
UPDATE orders
|
||||
SET status = '履约中', expert_id = ?, companion_name = ?, expert_avatar = ?
|
||||
@@ -2480,6 +2607,9 @@ export class MysqlRepository {
|
||||
const rejectReason = companionRow?.reject_reason || '';
|
||||
const companionStatus = companionRow ? (companionRow.status || '接单中') : (certStatus === 2 ? '接单中' : '');
|
||||
|
||||
const isCertExpert = certStatus === 2 || roleHasExpert;
|
||||
const roleFlags = isCertExpert ? 3 : 1;
|
||||
|
||||
return {
|
||||
id: u.id,
|
||||
userNo: u.userNo,
|
||||
@@ -2489,8 +2619,10 @@ export class MysqlRepository {
|
||||
gender: u.gender,
|
||||
signature: u.signature || '',
|
||||
role: u.role,
|
||||
roleFlags,
|
||||
balance: Number(u.balance || 0).toFixed(2),
|
||||
commission: Number(u.commission || 0).toFixed(2),
|
||||
commissionBalance: Number(u.commission || 0).toFixed(2),
|
||||
invitedCount: Number(u.invitedCount || 0),
|
||||
inviterId: u.inviterId ? Number(u.inviterId) : null,
|
||||
orderCount: Number(u.orderCount || 0),
|
||||
@@ -2500,7 +2632,7 @@ export class MysqlRepository {
|
||||
certStatus,
|
||||
rejectReason,
|
||||
companionStatus,
|
||||
isExpert: certStatus === 2 || roleHasExpert,
|
||||
isExpert: isCertExpert,
|
||||
status: u.status,
|
||||
};
|
||||
}
|
||||
@@ -3348,23 +3480,48 @@ export class MysqlRepository {
|
||||
if (inviterId === uid) {
|
||||
throw new Error('不能绑定自己的邀请码');
|
||||
}
|
||||
const [invRows]: any = await pool.query('SELECT id, nickname FROM users WHERE id = ? LIMIT 1', [inviterId]);
|
||||
const [invRows]: any = await pool.query('SELECT id, nickname, inviter_id FROM users WHERE id = ? LIMIT 1', [inviterId]);
|
||||
if (!invRows || invRows.length === 0) {
|
||||
throw new Error('邀请码对应的推荐人不存在');
|
||||
}
|
||||
const inviter = invRows[0];
|
||||
const level2InviterId = inviter.inviter_id ? Number(inviter.inviter_id) : null;
|
||||
const [curRows]: any = await pool.query('SELECT id, inviter_id FROM users WHERE id = ? LIMIT 1', [uid]);
|
||||
if (!curRows || curRows.length === 0) {
|
||||
throw new Error('当前用户不存在');
|
||||
}
|
||||
if (curRows[0].inviter_id && Number(curRows[0].inviter_id) === inviterId) {
|
||||
return { userId: uid, inviterId, inviterName: inviter.nickname, message: `已绑定推荐人:${inviter.nickname}` };
|
||||
if (curRows[0].inviter_id) {
|
||||
if (Number(curRows[0].inviter_id) === inviterId) {
|
||||
return { userId: uid, inviterId, level2InviterId, inviterName: inviter.nickname, message: `已绑定推荐人:${inviter.nickname}` };
|
||||
}
|
||||
throw new Error('当前账号已终身绑定上级推荐人,不可重复修改绑定');
|
||||
}
|
||||
await pool.query('UPDATE users SET inviter_id = ? WHERE id = ?', [inviterId, uid]);
|
||||
await pool.query('UPDATE users SET invited_count = COALESCE(invited_count, 0) + 1 WHERE id = ?', [inviterId]);
|
||||
try {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS distribution_relations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNIQUE NOT NULL,
|
||||
inviter_id INT NOT NULL,
|
||||
level2_inviter_id INT DEFAULT NULL,
|
||||
bind_source VARCHAR(64) DEFAULT 'INVITE_CODE',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_inviter (inviter_id),
|
||||
INDEX idx_level2_inviter (level2_inviter_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
await pool.query(
|
||||
`INSERT INTO distribution_relations (user_id, inviter_id, level2_inviter_id, bind_source, created_at)
|
||||
VALUES (?, ?, ?, 'SUNCODE_OR_INVITE', NOW())
|
||||
ON DUPLICATE KEY UPDATE inviter_id = VALUES(inviter_id), level2_inviter_id = VALUES(level2_inviter_id)`,
|
||||
[uid, inviterId, level2InviterId]
|
||||
);
|
||||
} catch (_) {}
|
||||
return {
|
||||
userId: uid,
|
||||
inviterId,
|
||||
level2InviterId,
|
||||
inviterName: inviter.nickname,
|
||||
message: `成功绑定推荐人:${inviter.nickname}`
|
||||
};
|
||||
|
||||
@@ -452,6 +452,42 @@ describe('ClientFeatures (TDD 测试套件: 用户端功能闭环)', () => {
|
||||
expect(wsContent).toContain('updateCompanionProfile');
|
||||
expect(wsContent).toContain('completeOrderApi');
|
||||
});
|
||||
|
||||
it('3.1 活动 4 位核销码必须建立 act:verify 缓存索引、执行 CAS 原子防重核销,并在核销后触发发起人费用清算与付费活动二级分润', () => {
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||||
|
||||
expect(content).toContain('act:verify:');
|
||||
expect(content).toContain('WHERE id = ? AND checkin_status = 0');
|
||||
expect(content).toContain('settleActivityCheckinAndReferral');
|
||||
expect(content).toContain('活动核销费用清算');
|
||||
});
|
||||
|
||||
it('3.2 双身份架构必须维护 role_flags (1:玩家, 2:大神, 3:双身份) 并在抢单接口拦截未通过实名认证的大神档案', () => {
|
||||
const mysqlPath = path.join(serverSrcDir, 'db', 'mysql.ts');
|
||||
const mysqlContent = fs.readFileSync(mysqlPath, 'utf-8');
|
||||
expect(mysqlContent).toContain('role_flags');
|
||||
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const repoContent = fs.readFileSync(repoPath, 'utf-8');
|
||||
expect(repoContent).toContain('roleFlags');
|
||||
expect(repoContent).toContain('大神档案未通过实名认证审核,无法接单');
|
||||
});
|
||||
|
||||
it('3.3 分销裂变必须建立 distribution_relations 父子终身关联表,并支持小程序扫码/带参进入自动锁粉绑定', () => {
|
||||
const mysqlPath = path.join(serverSrcDir, 'db', 'mysql.ts');
|
||||
const mysqlContent = fs.readFileSync(mysqlPath, 'utf-8');
|
||||
expect(mysqlContent).toContain('CREATE TABLE IF NOT EXISTS distribution_relations');
|
||||
|
||||
const repoPath = path.join(serverSrcDir, 'db', 'repository.ts');
|
||||
const repoContent = fs.readFileSync(repoPath, 'utf-8');
|
||||
expect(repoContent).toContain('INSERT INTO distribution_relations');
|
||||
expect(repoContent).toContain('当前账号已终身绑定上级推荐人,不可重复修改绑定');
|
||||
|
||||
const indexVuePath = path.join(uniPagesDir, 'index', 'index.vue');
|
||||
const indexContent = fs.readFileSync(indexVuePath, 'utf-8');
|
||||
expect(indexContent).toContain('PENDING_INVITER_CODE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户