chore:邀请码初步更改

This commit is contained in:
2026-08-03 14:19:21 +08:00
parent e01ba29604
commit cd91ff9d49
5 changed files with 386 additions and 14 deletions

View File

@@ -59,6 +59,11 @@ const localDevConfig = ({
domain: 'http://192.168.197.1:8050/',
// domain: 'http://localhost:8050/',
},
'192.168.8.118-1': { // 测试平台
uniacid: 1,
domain: 'http://192.168.8.118:8050/',
// domain: 'http://localhost:8050/',
},
'local-11': { // 测试平台
uniacid: 11,
domain: 'http://localhost:8050/',
@@ -164,7 +169,7 @@ const localDevConfig = ({
// })['2724']; // 生物菌肥
// })['2811']; // 选择要使用的环境配置
// })['local-11']; // 选择要使用的环境配置
})['248']; // 本地
})['192.168.8.118-1']; // 本地
// })['local-2']; // 本地
// })['local-11']; // 本地
// })['1-test']; // test

67
App.vue
View File

@@ -90,6 +90,43 @@
return fallback
}
// ============================================================
// ✅ 推荐码 / 邀请码(来源追踪)全局兜底
// 规则:只写非空值,不主动清空(diy-form 提交成功后自己清)
// ============================================================
const FORM_INVITE_KEY = 'form_invite_pending_code'
function _persistInviteCodeFromOptions(options) {
try {
var code = ''
// 1) onLaunch/onShow options.query:优先 recommend_code,次选 invite(历史兼容)
if (options && options.query) {
if (options.query.recommend_code) {
code = String(options.query.recommend_code)
} else if (options.query.invite) {
code = String(options.query.invite)
}
}
// 2) H5 兜底:从当前 location.search 取(同样优先 recommend_code)
if (!code) {
// #ifdef H5
try {
var sp = (new URL(window.location.href)).searchParams
var s1 = sp.get('recommend_code')
var s2 = sp.get('invite')
if (s1) code = String(s1)
else if (s2) code = String(s2)
} catch (e) {}
// #endif
}
// 3) 只写非空,不擦除已有暂存
if (code) {
uni.setStorageSync(FORM_INVITE_KEY, code)
}
} catch (e) {
// 极端失败静默,diy-form 仍有 P2/P3 兜底读取
}
}
// ============================================================
// 主题/语言启动
// ============================================================
@@ -508,6 +545,36 @@
// #ifdef MP-ALIPAY
if (options.query && options.query.m) uni.setStorageSync('source_member', options.query.m);
// #endif
// ✅ 邀请码(来源追踪):onLaunch 冷启动兜底解析 URL query → storage
_persistInviteCodeFromOptions(options);
},
onShow: function(options) {
// ✅ 邀请码(来源追踪):onShow 热启动/后台切回 兜底解析
// 注:onShow 的 options 仅部分平台(MP-WEIXIN, MP-HARMONY, MP-ALIPAY 等)提供 query
// 若 options 为空,则 helper 内部自动 fallback 到 H5 location.search 或已有 storage
_persistInviteCodeFromOptions(options || {});
// #ifdef MP
// 小程序端:每次回到前台 → 自动授权登录刷新会话 token + memberInfo(与行业模板一致)
if (typeof this.getAuthInfo === 'function') {
this.getAuthInfo();
}
if (this.$store && this.$store.state && this.$store.state.token && this.$api) {
this.$api.sendRequest({
url: '/api/member/info',
success: (res) => {
if (res && res.code >= 0 && res.data && this.$store && this.$store.commit) {
this.$store.commit('setMemberInfo', res.data);
}
}
});
}
// #endif
// #ifdef MP-ALIPAY
if (options && options.query && options.query.m) uni.setStorageSync('source_member', options.query.m);
// #endif
},
onHide: function() {},
methods: {

View File

@@ -76,6 +76,113 @@ function _getCurrentUniacid() {
return _pickCurrentUniacid(storageVal, configVal);
}
/**
* 从当前登录态读取 memberInfo → 返回后端接口真实生成的推荐码
* 读取优先级:storage.memberInfo.recommend_code > Vuex store.state.memberInfo.recommend_code
* 没登录 / 未同意授权 / 后端还没返回码 → 返回 ''
* @returns {string}
*/
function _getCurrentRecommendCode() {
// P1: storage 中 memberInfo.recommend_code(DIY 推荐码组件 _persistRecommendInfo 已写入)
try {
var info = null;
if (typeof uni !== 'undefined' && uni && typeof uni.getStorageSync === 'function') {
info = uni.getStorageSync('memberInfo');
}
if (info && typeof info === 'object') {
if (info.recommend_code && String(info.recommend_code).trim()) {
return String(info.recommend_code).trim();
}
// 兼容旧字段 invite_code(商家之前保存的配置)
if (info.invite_code && String(info.invite_code).trim()) {
return String(info.invite_code).trim();
}
}
} catch (e) {}
// P2: Vuex store.state.memberInfo(某些平台 storage 还没落地时,内存里可能已经好了)
try {
if (store && store.state && store.state.memberInfo && typeof store.state.memberInfo === 'object') {
var s = store.state.memberInfo;
if (s.recommend_code && String(s.recommend_code).trim()) {
return String(s.recommend_code).trim();
}
if (s.invite_code && String(s.invite_code).trim()) {
return String(s.invite_code).trim();
}
}
} catch (e) {}
return '';
}
/**
* 从 URL 中移除指定 query 参数(防止 URL 里已存在旧的 recommend_code=xxx 时,append 之后重复)
* - 仅移除 key,不影响其他参数
* - 同时处理 hash(xxx#yyy) 在 query 后面的情况(保留 hash 原位置)
* @param {string} url
* @param {string} key
* @returns {string}
*/
function _removeQueryParam(url, key) {
if (!url || !key) return url || '';
// 先拆 hash
var hashIdx = url.indexOf('#');
var hash = '';
var main = url;
if (hashIdx !== -1) {
hash = url.substring(hashIdx);
main = url.substring(0, hashIdx);
}
var qIdx = main.indexOf('?');
if (qIdx === -1) return url; // 没 query
var path = main.substring(0, qIdx);
var qs = main.substring(qIdx + 1);
if (!qs) return url;
var pairs = qs.split('&').filter(function(p) {
if (!p) return false;
var eq = p.indexOf('=');
var k = (eq === -1) ? p : p.substring(0, eq);
// 大小写不敏感(URL 参数通常大小写不敏感)
return String(k).toLowerCase() !== String(key).toLowerCase();
});
var newMain = pairs.length ? (path + '?' + pairs.join('&')) : path;
return newMain + hash;
}
/**
* 给 URL 追加当前登录会员的推荐码(方案 B 标准函数,推荐使用)
* 规则:
* - 当前登录且能拿到后端返回的 recommend_code → 移除 URL 里已存在的旧值
* → 同时追加 recommend_code + invite 两个 key(双参数兼容,后端收哪个都能用)
* - 未登录 / 推荐码为空 → 原样返回
* - 兼容内部相对路径 '/pages/xxx?x=1' 与 外部绝对 URL 'https://host/xx#yyy'
* @param {string} url
* @returns {string}
*/
function _appendRecommendCodeIfLoggedIn(url) {
if (!url) return '';
var code = _getCurrentRecommendCode();
if (!code) return url; // 没登录或没码:不动
// 1) 先清 URL 里已存在的旧参数(两个 key 都清,避免重复追加或旧码覆盖新码)
var cleaned = _removeQueryParam(url, 'recommend_code');
cleaned = _removeQueryParam(cleaned, 'invite');
// 2) 双 key 同时追加:标准名 recommend_code + 历史兼容名 invite
// 这样不管后端 parseCode 接口收哪个 key,都能正确命中
return _appendQueryParamsBase(cleaned, { recommend_code: code, invite: code });
}
/**
* 旧版函数别名(保持对 _appendInviteCodeIfLoggedIn 的调用兼容)
* 之前业务代码调用 _appendInviteCodeIfLoggedIn 的,现在全部委托给新的 _appendRecommendCodeIfLoggedIn
* 确保:一次改动,全链路统一使用后端真实码 + 双 key 兼容
*/
function _appendInviteCodeIfLoggedIn(url) {
return _appendRecommendCodeIfLoggedIn(url);
}
/**
* 把 URL 上的 page_params 还原成对象。是 _toParamValueString 的逆操作。
* 容错策略:任何解析失败都安全降级到 defaultValue,不抛异常。
@@ -1255,6 +1362,11 @@ export default {
let url = this.normalizeUrl(decodeEntity(link.wap_url));
url = this.appendQueryParams(url, params);
// ✅ 外链跳转自动拼邀请码:当前登录有邀请码 → 去 URL 旧 invite → 追加成新值
// H5: window.location.href 新页面打开时 location.search 能收到
// MP: webview src URL 打开目标 H5 页面时能收到
url = _appendInviteCodeIfLoggedIn(url);
// #ifdef H5
// console.log(`url ---->`, url, link)
window.location.href = url;
@@ -1367,6 +1479,16 @@ export default {
// resolveLinkUrl 已把 wap_url 中的 query 合并到 link.page_params
// 这里过滤掉 link 元数据字段, 避免 wap_url/name/parent/title 等被错误附加到目标 URL
// title/enable_set_title/page_params 仍需透传到目标 URL,page_params 放最后
// ✅ 内部页:把推荐码写进 page_params(最终会被拼成 URL query,diy-form onLoad options 能收到)
// 仅在当前登录已拿到推荐码时追加,不覆盖上游已有值;双写 recommend_code(老师标准)+ invite(历史兼容)
var _curRecommendCode = _getCurrentRecommendCode();
if (_curRecommendCode) {
if (!link.page_params || typeof link.page_params !== 'object') link.page_params = {};
if (!link.page_params.recommend_code) link.page_params.recommend_code = _curRecommendCode;
if (!link.page_params.invite) link.page_params.invite = _curRecommendCode;
}
const { wap_url, name, parent, type, appid, page, mobile, image, ...navParams } = link;
this.redirectTo(link.wap_url, {
...navParams,

View File

@@ -19,6 +19,12 @@
:formConfig="mergedFormConfig"
ref="nsNewform"
></ns-newform>
<!-- ✅ 邀请码归属提示(零感知:只有通过邀请链接进入才显示,纯灰色小字居中,防纠纷核对) -->
<view v-if="_inviteCodePending" class="form-invite-attribution">
<text class="attribution-text">本次提交推荐邀请码:{{ _inviteCodePending }}</text>
</view>
<!-- 提交按钮(外层 = form-bg + form-padding-x;按钮 = submit-bg + submit-color + input-height + input-radius)。
showSubmit=false 时由父级页面渲染自己的提交按钮并通过 ref 调用本组件的 verify()/getFormData()。 -->
<view v-if="showSubmit" class="form-submit-wrap" :style="submitWrapStyle">
@@ -57,11 +63,15 @@ export default {
mixins: [DiyMinx],
data() {
return {
formComponents: [] // 转换后的表单组件
formComponents: [], // 转换后的表单组件
// ✅ 表单提交邀请码(来源追踪):3 级读取 → 提交自动注入 → 成功清暂存
_inviteCodePending: ''
};
},
created() {
this.initForm();
// ✅ 初始化:从 URL query / storage / H5 location 读取邀请码暂存
this._initInviteCode();
},
watch: {
'value': {
@@ -296,22 +306,48 @@ export default {
// 若父级监听了 @submit,则把提交控制权交给父级(用于父级有自定义提交字段/反馈逻辑的页面,如 formdata.vue 传 uniacid)
if (this.$listeners && this.$listeners.submit) {
this.$emit('submit', this.getFormData());
// ✅ 父级自定义流程:把 recommend_code + invite_code 都透传给父级,双保险(formData 数组里 + 第二参数对象里)
var formDataArr = this.getFormData();
try {
if (this._inviteCodePending && Array.isArray(formDataArr)) {
formDataArr = formDataArr.slice();
formDataArr.push({
controller: 'hidden',
name: '_recommend_code',
value: this._inviteCodePending,
val: this._inviteCodePending
});
formDataArr.push({
controller: 'hidden',
name: '_invite_code',
value: this._inviteCodePending,
val: this._inviteCodePending
});
}
} catch (e) {}
this.$emit('submit', formDataArr, {
recommend_code: this._inviteCodePending,
invite_code: this._inviteCodePending
});
return;
}
// 获得保单数据
const submitFormData = this.getFormData();
var reqData = {
form_id: this.value.form_id || 0,
form_data: typeof submitFormData === 'string' ? submitFormData : JSON.stringify(submitFormData),
component_id: this.value.id || '',
form_name: this.value.form_name || '默认表单'
};
if (this._inviteCodePending) {
reqData.recommend_code = this._inviteCodePending; // 方案 B 标准
reqData.invite_code = this._inviteCodePending; // 历史兼容
}
// 提交到后端
this.$api.sendRequest({
url: '/form/api/form/create',
data: {
form_id: this.value.form_id || 0,
form_data: typeof submitFormData === 'string' ? submitFormData : JSON.stringify(submitFormData),
component_id: this.value.id || '',
form_name: this.value.form_name || '默认表单'
},
data: reqData,
success: function(res) {
if (res.code === 0) {
uni.showToast({
@@ -322,6 +358,8 @@ export default {
if (self.$refs.nsNewform && self.$refs.nsNewform.setFormData) {
self.$refs.nsNewform.setFormData();
}
// ✅ 提交成功后清暂存邀请码(防止后续重复提交归属错人)
self._clearInviteCode();
} else {
uni.showToast({
title: res.message || self.$lang('submitFail'),
@@ -330,6 +368,61 @@ export default {
}
}
});
},
/* ============================================================
✅ 邀请码(来源追踪)辅助方法
链路:分享链接 ?invite=XXX → 进入本页 3 级取码 → 暂存 data + storage
→ 提交时自动拼入 request → 成功清暂存
============================================================ */
FORM_INVITE_STORAGE_KEY: 'form_invite_pending_code',
_initInviteCode() {
var key = this.FORM_INVITE_STORAGE_KEY;
var code = '';
// P1:当前页面的 onLoad options(通过 getCurrentPages 顶页的 options 拿到)
try {
var pages = (typeof getCurrentPages === 'function') ? getCurrentPages() : [];
if (pages && pages.length) {
var top = pages[pages.length - 1];
if (top && top.options) {
if (top.options.recommend_code) {
code = String(top.options.recommend_code);
} else if (top.options.invite) {
code = String(top.options.invite);
}
}
}
} catch (e) {}
// P2:storage(App.vue onLaunch/onShow 提前写入,全局兜底)
if (!code) {
try { code = uni.getStorageSync(key) || ''; } catch (e) { code = ''; }
}
// P3:H5 冷启动从 location.search 兜底(同样优先 recommend_code)
if (!code) {
// #ifdef H5
try {
var sp = (new URL(window.location.href)).searchParams;
var s1 = sp.get('recommend_code');
var s2 = sp.get('invite');
if (s1) code = String(s1);
else if (s2) code = String(s2);
} catch (e) {}
// #endif
}
// 写回 data + storage 持久化(跨页面跳转也不丢)
if (code) {
this._inviteCodePending = code;
try { uni.setStorageSync(key, code); } catch (e) {}
}
},
// 清邀请码(提交成功后调用,避免串号)
_clearInviteCode() {
this._inviteCodePending = '';
try { uni.removeStorageSync(this.FORM_INVITE_STORAGE_KEY); } catch (e) {}
}
}
};
@@ -341,6 +434,20 @@ export default {
padding: 0;
border-radius: 10rpx;
/* ✅ 邀请码归属提示:灰色小字,零感知(只有通过邀请链接才显示) */
.form-invite-attribution {
width: 100%;
text-align: center;
box-sizing: border-box;
padding: 20rpx 30rpx 10rpx;
.attribution-text {
font-size: 22rpx;
color: #bbbbbb;
line-height: 1.5;
letter-spacing: 0.5rpx;
}
}
.form-banner {
width: 100%;
overflow: hidden;

View File

@@ -261,6 +261,22 @@
<diy-channel-list :value="item"></diy-channel-list>
</template>
<!-- ✅ 我的邀请码 / 推荐码(商家 DIY 自由放置:
1) 严格匹配:后端注册的 5 个常见名字
2) 宽松匹配:componentName 包含 Recommend / Invite 关键字(不区分大小写)—— 解决后端注册名和前端数组不一致导致的"加了组件但页面不显示"问题
后端只要把组件注册成任意含 Recommend 或 Invite 关键字的名字就能渲染出来,避免前端硬编码数组漏匹配 -->
<template v-if="(function(cn){
if (!cn) return false;
var s = String(cn).toLowerCase();
// 严格匹配
if (['MyInviteCode','RecommendCode','Recommend','InviteCode','RecommendCodeDiy'].indexOf(cn) !== -1) return true;
// 宽松匹配:含 recommend / invite 关键字
if (s.indexOf('recommend') !== -1 || s.indexOf('invite') !== -1) return true;
return false;
})(item.componentName)">
<components-recommend-diy :value="item"></components-recommend-diy>
</template>
<!-- 自定义扩展组件 -->
<diy-comp-extend :value="item"></diy-comp-extend>
</view>
@@ -270,9 +286,15 @@
<script>
// 组件组展示
import DiyMinx from './minx.js'
// ✅ 显式 import 我的邀请码 DIY 组件(同级文件,相对路径引入,不依赖 @ 别名)
import ComponentsRecommendDiy from './components-recommend-diy.vue'
export default {
name: 'diy-group',
components: {
// ✅ 显式注册:避免 easycom 没配置此目录时导致 "Unknown custom element"
ComponentsRecommendDiy
},
props: {
diyData: {
type: Object
@@ -336,6 +358,21 @@ export default {
? JSON.parse(this.$store.state.diyGroupShowModule)
: null;
// ✅ 调试日志:showModuleData 白名单如果存在,会严格过滤,推荐码组件没加进白名单就会被过滤掉 → 控制台能直接看到
try {
if (showModuleData && showModuleData.length) {
console.log('[DIY-GROUP][diyDataArray] showModuleData 白名单 = ', JSON.stringify(showModuleData));
var hasRecommendInWhite = false;
showModuleData.forEach(function(n){
var s = String(n || '').toLowerCase();
if (s.indexOf('recommend') !== -1 || s.indexOf('invite') !== -1) hasRecommendInWhite = true;
});
if (!hasRecommendInWhite) {
console.warn('[DIY-GROUP][diyDataArray] ⚠️ 白名单 showModuleData 里没有推荐码/邀请码组件名,组件会被过滤掉不显示!请在后台 DIY 编辑器里的"显示模块"中勾选上该组件');
}
}
} catch (e) {}
if (!showModuleData || !showModuleData.length) {
return this.processedData || [];
}
@@ -344,9 +381,25 @@ export default {
return [];
}
return (this.processedData || []).filter(item =>
showModuleData.includes(item.componentName)
);
// ✅ 关键修复:白名单过滤时,对推荐码组件也做宽松匹配(白名单里是 RecommendCode 但后端是 Recommend 也能过)
// 之前严格 includes(item.componentName) 会漏掉大小写/命名不一致的情况
var recKeys = ['myinvitecode','recommendcode','recommend','invitecode','recommendcodediy'];
return (this.processedData || []).filter(item => {
// 1) 严格命中白名单
if (showModuleData.includes(item.componentName)) return true;
// 2) 推荐码组件宽松命中:item 是推荐码类型 && 白名单里有任意推荐码关键字
var itemName = String(item.componentName || '').toLowerCase();
var isRecommendItem = (recKeys.indexOf(itemName) !== -1) || itemName.indexOf('recommend') !== -1 || itemName.indexOf('invite') !== -1;
if (isRecommendItem) {
var whiteHasRecommend = false;
showModuleData.forEach(function(n){
var s = String(n || '').toLowerCase();
if (recKeys.indexOf(s) !== -1 || s.indexOf('recommend') !== -1 || s.indexOf('invite') !== -1) whiteHasRecommend = true;
});
if (whiteHasRecommend) return true;
}
return false;
});
}
},
methods: {
@@ -363,7 +416,25 @@ export default {
this.processedData = [];
return;
}
// ✅ 调试日志:后台保存组件后,用户在控制台能看到 ALL componentName 列表
// 方便定位:"后端加了组件但前端不显示" → 要么 componentName 没匹配,要么 showModuleData 过滤掉了
try {
const names = this.diyGlobalData.value.map(function(it){ return it && it.componentName ? it.componentName : '(no name)'; });
console.log('[DIY-GROUP][processData] 所有DIY组件名列表 = ', JSON.stringify(names));
// 进一步打印哪些是推荐码相关的
var matched = [];
var recommendKeys = ['myinvitecode','recommendcode','recommend','invitecode','recommendcodediy','recommend','invite'];
this.diyGlobalData.value.forEach(function(it, idx){
if (!it || !it.componentName) return;
var s = String(it.componentName).toLowerCase();
if (recommendKeys.indexOf(s) !== -1 || s.indexOf('recommend') !== -1 || s.indexOf('invite') !== -1) {
matched.push('idx=' + idx + ', name=' + it.componentName);
}
});
if (matched.length) console.log('[DIY-GROUP][processData] 命中推荐码/邀请码组件 = ', matched.join('; '));
} catch (e) {}
this.processedData = this.diyGlobalData.value.map((item, index) => {
const newItem = this.$util.deepClone(item);