晚上邀请逻辑.

This commit is contained in:
18631081161 2025-12-19 15:30:29 +08:00
parent 18b5304119
commit 3257210c39
22 changed files with 963 additions and 2101 deletions

View File

@ -1,13 +1,55 @@
const express = require('express');
const router = express.Router();
const axios = require('axios');
const { authenticateUser } = require('../middleware/auth');
const User = require('../models/User');
const env = require('../config/env');
// 缓存 access_token
let accessTokenCache = {
token: null,
expiresAt: 0
};
/**
* 获取微信 access_token
*/
async function getAccessToken() {
const now = Date.now();
// 如果缓存有效,直接返回
if (accessTokenCache.token && accessTokenCache.expiresAt > now) {
return accessTokenCache.token;
}
// 调用微信API获取 access_token
const url = 'https://api.weixin.qq.com/cgi-bin/token';
const response = await axios.get(url, {
params: {
grant_type: 'client_credential',
appid: env.wechat.appId,
secret: env.wechat.appSecret
}
});
if (response.data.errcode) {
throw new Error(`获取access_token失败: ${response.data.errmsg}`);
}
// 缓存 token提前5分钟过期
accessTokenCache = {
token: response.data.access_token,
expiresAt: now + (response.data.expires_in - 300) * 1000
};
return accessTokenCache.token;
}
/**
* @swagger
* /api/v1/qrcode/miniprogram:
* post:
* summary: 获取小程序码信息
* summary: 获取小程序码
* tags: [QRCode]
* security:
* - bearerAuth: []
@ -17,18 +59,17 @@ const User = require('../models/User');
* schema:
* type: object
* properties:
* path:
* type: string
* width:
* type: number
* description: 二维码宽度默认430
* responses:
* 200:
* description: 成功
* description: 成功返回小程序码图片的base64
*/
router.post('/miniprogram', authenticateUser, async (req, res) => {
try {
const userId = req.user.id;
const { path, width } = req.body;
const { width = 430 } = req.body;
// 获取用户邀请码
const user = await User.findByPk(userId, {
@ -37,8 +78,9 @@ router.post('/miniprogram', authenticateUser, async (req, res) => {
if (!user) {
return res.status(404).json({
success: false,
error: { code: 'USER_NOT_FOUND', message: 'User not found' }
code: 404,
message: 'User not found',
data: null
});
}
@ -49,20 +91,52 @@ router.post('/miniprogram', authenticateUser, async (req, res) => {
await user.update({ invitationCode });
}
// 目前返回邀请码信息,前端可以用 canvas 绘制
// 获取 access_token
const accessToken = await getAccessToken();
// 调用微信API生成小程序码使用 getUnlimited 接口,无数量限制)
const wxUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken}`;
const wxResponse = await axios.post(wxUrl, {
scene: `inviteCode=${invitationCode}`, // scene 参数最长32字符
page: 'pages/index/index', // 必须是已发布的小程序页面
width: width,
auto_color: false,
line_color: { r: 0, g: 0, b: 0 },
is_hyaline: false
}, {
responseType: 'arraybuffer' // 返回的是图片二进制数据
});
// 检查是否返回错误错误时返回JSON
const contentType = wxResponse.headers['content-type'];
if (contentType && contentType.includes('application/json')) {
const errorData = JSON.parse(wxResponse.data.toString());
console.error('微信小程序码生成失败:', errorData);
return res.status(200).json({
code: 500,
message: errorData.errmsg || '生成小程序码失败',
data: null
});
}
// 转换为 base64
const base64Image = Buffer.from(wxResponse.data).toString('base64');
const dataUrl = `data:image/png;base64,${base64Image}`;
return res.status(200).json({
success: true,
code: 0,
message: 'success',
data: {
invitationCode,
path: path || `pages/index/index?inviteCode=${invitationCode}`,
message: '请使用邀请码分享给好友'
qrcodeUrl: dataUrl // base64格式的小程序码图片
}
});
} catch (error) {
console.error('Get miniprogram qrcode error:', error);
return res.status(500).json({
success: false,
error: { code: 'QRCODE_ERROR', message: error.message }
return res.status(200).json({
code: 500,
message: error.message || '生成小程序码失败',
data: null
});
}
});

View File

@ -44,8 +44,10 @@ const _sfc_main = {
user: null,
shouldRefresh: false,
loginTime: 0,
config: null
config: null,
// 应用配置(从后端获取)
inviteCode: ""
// 邀请码(从分享链接获取)
},
onLaunch: function() {
console.log("App Launch");

File diff suppressed because it is too large Load Diff

View File

@ -185,9 +185,13 @@ AppServer.prototype.deleteData = async function(url) {
return err;
});
};
AppServer.prototype.WechatLogin = async function(code) {
AppServer.prototype.WechatLogin = async function(code, invitationCode) {
var url = serverConfig.apiUrl_Auth_WechatLogin;
return this.postData(url, { code }).then((data) => {
var postData = { code };
if (invitationCode) {
postData.invitationCode = invitationCode;
}
return this.postData(url, postData).then((data) => {
return data;
});
};

View File

@ -1,363 +0,0 @@
"use strict";
const common_vendor = require("../../../../common/vendor.js");
let qrcode;
const _sfc_main = {
name: "u-qrcode",
props: {
cid: {
type: String,
default: () => `u-qrcode-canvas${Math.floor(Math.random() * 1e6)}`
},
size: {
type: Number,
default: 200
},
unit: {
type: String,
default: "px"
},
show: {
type: Boolean,
default: true
},
val: {
type: String,
default: ""
},
background: {
type: String,
default: "#ffffff"
},
foreground: {
type: String,
default: "#000000"
},
pdground: {
type: String,
default: "#000000"
},
icon: {
type: String,
default: ""
},
iconSize: {
type: Number,
default: 40
},
lv: {
type: Number,
default: 3
},
onval: {
type: Boolean,
default: true
},
loadMake: {
type: Boolean,
default: true
},
usingComponents: {
type: Boolean,
default: true
},
showLoading: {
type: Boolean,
default: true
},
loadingText: {
type: String,
default: "生成中"
},
allowPreview: {
type: Boolean,
default: false
},
// 是否使用根节点宽高
useRootHeightAndWidth: {
type: Boolean,
default: () => false
}
},
emits: ["result", "longpressCallback"],
data() {
return {
loading: false,
result: "",
popupShow: false,
list: [
{
name: "保存二维码"
}
],
rootId: `rootId${Number(Math.random() * 100).toFixed(0)}`,
ganvas: null,
context: "",
canvasObj: {},
sizeLocal: this.size,
ctx: null,
// ctx 在new Qrcode 时js文件内部设置
canvas: null
// ctx 在new Qrcode 时js文件内部设置
};
},
async mounted() {
if (this.useRootHeightAndWidth) {
await this.setNewSize();
}
if (this.loadMake) {
if (!this._empty(this.val)) {
setTimeout(() => {
setTimeout(() => {
this._makeCode();
});
}, 0);
}
}
},
methods: {
_makeCode() {
let that2 = this;
if (!this._empty(this.val)) {
this.loading = true;
qrcode = new common_vendor.QRCode({
context: that2,
// 上下文环境
canvasId: that2.cid,
// canvas-id
nvueContext: that2.context,
usingComponents: that2.usingComponents,
// 是否是自定义组件
showLoading: false,
// 是否显示loading
loadingText: that2.loadingText,
// loading文字
text: that2.val,
// 生成内容
size: that2.sizeLocal,
// 二维码大小
background: that2.background,
// 背景色
foreground: that2.foreground,
// 前景色
pdground: that2.pdground,
// 定位角点颜色
correctLevel: that2.lv,
// 容错级别
image: that2.icon,
// 二维码图标
imageSize: that2.iconSize,
// 二维码图标大小
cbResult: function(res) {
that2._result(res);
}
});
} else {
common_vendor.index.showToast({
title: "二维码内容不能为空",
icon: "none",
duration: 2e3
});
}
},
_clearCode() {
this._result("");
qrcode.clear();
},
_saveCode() {
let that2 = this;
if (this.result != "") {
common_vendor.index.saveImageToPhotosAlbum({
filePath: that2.result,
success: function() {
common_vendor.index.showToast({
title: "二维码保存成功",
icon: "success",
duration: 2e3
});
}
});
}
},
preview(e) {
if (this.allowPreview) {
common_vendor.index.previewImage({
urls: [this.result],
longPressActions: {
itemList: ["保存二维码图片"],
success: function(data) {
switch (data.tapIndex) {
case 0:
that._saveCode();
break;
}
},
fail: function(err) {
console.log(err.errMsg);
}
}
});
}
this.$emit("preview", {
url: this.result
}, e);
},
async toTempFilePath({ success, fail }) {
if (this.context) {
this.ctx.toTempFilePath(
0,
0,
this.sizeLocal,
this.sizeLocal,
this.sizeLocal,
this.sizeLocal,
"",
1,
(res) => {
success(res);
}
);
} else {
const canvas = await this.getNode(this.cid, true);
common_vendor.index.canvasToTempFilePath(
{
canvas,
success: (res) => {
success(res);
},
fail
},
this
);
}
},
async longpress() {
this.toTempFilePath({
success: (res) => {
this.$emit("longpressCallback", res.tempFilePath);
},
fail: (err) => {
}
});
},
/**
* 使用根节点宽高 设置新的size
* @return {Promise<void>}
*/
async setNewSize() {
const rootNode = await this.getNode(this.rootId, false);
const { width, height } = rootNode;
if (width > height) {
this.sizeLocal = height;
} else {
this.sizeLocal = width;
}
},
/**
* 获取节点
* @param id 节点id
* @param isCanvas 是否为Canvas节点
* @return {Promise<unknown>}
*/
async getNode(id, isCanvas) {
return new Promise((resolve, reject) => {
try {
const query = common_vendor.index.createSelectorQuery().in(this);
query.select(`#${id}`).fields({ node: true, size: true }).exec((res) => {
if (isCanvas) {
resolve(res[0].node);
} else {
resolve(res[0]);
}
});
} catch (e) {
console.error("获取节点失败", e);
}
});
},
selectClick(index) {
switch (index) {
case 0:
alert("保存二维码");
this._saveCode();
break;
}
},
_result(res) {
this.loading = false;
this.result = res;
this.$emit("result", res);
},
_empty(v) {
let tp = typeof v, rt = false;
if (tp == "number" && String(v) == "") {
rt = true;
} else if (tp == "undefined") {
rt = true;
} else if (tp == "object") {
if (JSON.stringify(v) == "{}" || JSON.stringify(v) == "[]" || v == null)
rt = true;
} else if (tp == "string") {
if (v == "" || v == "undefined" || v == "null" || v == "{}" || v == "[]")
rt = true;
} else if (tp == "function") {
rt = false;
}
return rt;
}
},
watch: {
size: function(n, o) {
if (n != o && !this._empty(n)) {
this.cSize = n;
if (!this._empty(this.val)) {
setTimeout(() => {
this._makeCode();
}, 100);
}
}
},
val: function(n, o) {
if (this.onval) {
if (n != o && !this._empty(n)) {
setTimeout(() => {
this._makeCode();
}, 0);
}
}
}
},
computed: {}
};
if (!Array) {
const _easycom_up_loading_icon2 = common_vendor.resolveComponent("up-loading-icon");
_easycom_up_loading_icon2();
}
const _easycom_up_loading_icon = () => "../u-loading-icon/u-loading-icon.js";
if (!Math) {
_easycom_up_loading_icon();
}
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
a: $props.cid,
b: $props.cid,
c: $data.sizeLocal + $props.unit,
d: $data.sizeLocal + $props.unit,
e: $props.showLoading && $data.loading
}, $props.showLoading && $data.loading ? {
f: common_vendor.p({
vertical: true,
text: $props.loadingText,
textSize: "14px"
}),
g: $data.sizeLocal + $props.unit,
h: $data.sizeLocal + $props.unit
} : {}, {
i: common_vendor.o((...args) => $options.preview && $options.preview(...args)),
j: $data.rootId,
k: $props.useRootHeightAndWidth ? "100%" : "auto",
l: $props.useRootHeightAndWidth ? "100%" : "auto",
m: common_vendor.o((...args) => $options.longpress && $options.longpress(...args))
});
}
const Component = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-444ebaa9"], ["__file", "F:/gitCode/uniapp/appointment_system/miniprogram/node_modules/uview-plus/components/u-qrcode/u-qrcode.vue"]]);
wx.createComponent(Component);

View File

@ -1,6 +0,0 @@
{
"component": true,
"usingComponents": {
"up-loading-icon": "../u-loading-icon/u-loading-icon"
}
}

View File

@ -1 +0,0 @@
<view class="u-qrcode data-v-444ebaa9" id="{{j}}" style="{{'width:' + k + ';' + ('height:' + l)}}" bindlongpress="{{m}}"><view class="u-qrcode__content data-v-444ebaa9" bindtap="{{i}}"><block wx:if="{{r0}}"><canvas class="u-qrcode__canvas data-v-444ebaa9" id="{{a}}" canvas-id="{{b}}" type="2d" style="{{'width:' + c + ';' + ('height:' + d)}}"/></block><view wx:if="{{e}}" class="u-qrcode__loading data-v-444ebaa9" style="{{'width:' + g + ';' + ('height:' + h)}}"><up-loading-icon wx:if="{{f}}" class="data-v-444ebaa9" u-i="444ebaa9-0" bind:__l="__l" u-p="{{f}}"></up-loading-icon></view></view></view>

View File

@ -1,45 +0,0 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
.u-qrcode__loading.data-v-444ebaa9 {
display: flex;
justify-content: center;
align-items: center;
background-color: #f7f7f7;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.u-qrcode__content.data-v-444ebaa9 {
position: relative;
}
.u-qrcode__content__canvas.data-v-444ebaa9 {
position: fixed;
top: -99999rpx;
left: -99999rpx;
z-index: -99999;
}

View File

@ -17,7 +17,33 @@ const _sfc_main = {
// 应用名称
};
},
onLoad() {
onLoad(options) {
let inviteCode = "";
if (options && options.inviteCode) {
inviteCode = options.inviteCode;
}
if (options && options.scene) {
const scene = decodeURIComponent(options.scene);
console.log("小程序码 scene:", scene);
const params = {};
scene.split("&").forEach((item) => {
const [key, value] = item.split("=");
if (key && value) {
params[key] = value;
}
});
if (params.inviteCode) {
inviteCode = params.inviteCode;
}
}
if (inviteCode) {
console.log("收到邀请码:", inviteCode);
const app = getApp();
if (app && app.globalData) {
app.globalData.inviteCode = inviteCode;
}
common_vendor.index.setStorageSync("inviteCode", inviteCode);
}
this.loadConfig();
this.loadBanners();
this.loadHotServices();

View File

@ -47,8 +47,18 @@ const _sfc_main = {
this.isLoading = false;
return;
}
let invitationCode = "";
const app = getApp();
if (app && app.globalData && app.globalData.inviteCode) {
invitationCode = app.globalData.inviteCode;
} else {
invitationCode = common_vendor.index.getStorageSync("inviteCode") || "";
}
if (invitationCode) {
console.log("使用邀请码登录:", invitationCode);
}
const appserver = new modules_api_AppServer.AppServer();
const data = await appserver.WechatLogin(loginRes.code);
const data = await appserver.WechatLogin(loginRes.code, invitationCode);
console.log("登录接口返回:", data);
if (!data) {
console.error("登录接口无响应");
@ -80,6 +90,14 @@ const _sfc_main = {
console.log("登录成功,保存认证信息");
const token = "Bearer " + data.data.token;
utils_auth.saveAuthData(token, data.data.refreshToken, data.data.user);
if (invitationCode) {
const app2 = getApp();
if (app2 && app2.globalData) {
app2.globalData.inviteCode = "";
}
common_vendor.index.removeStorageSync("inviteCode");
console.log("邀请码已清除");
}
common_vendor.index.showToast({
title: this.$t("login.loginSuccess") || "登录成功",
icon: "success",

View File

@ -16,6 +16,12 @@ const _sfc_main = {
qrcodeDataUrl: "",
qrcodeValue: "",
// 二维码内容
wxQrcodeImage: "",
// 微信小程序码图片base64
qrcodeLoading: false,
// 小程序码加载状态
qrcodeError: "",
// 小程序码错误信息
appLogo: "",
// 应用logo用于分享图片
applyStep: 1,
@ -124,7 +130,7 @@ const _sfc_main = {
this.commissionStats = res.data;
this.updateWithdrawDisplay();
const inviteCode = res.data.invitationCode || "";
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`;
this.qrcodeValue = `/pages/index/index?inviteCode=${inviteCode}`;
}
} catch (error) {
console.error("获取佣金统计失败:", error);
@ -205,21 +211,30 @@ const _sfc_main = {
showCancel: false
});
},
generateQRCode() {
const inviteCode = this.commissionStats.invitationCode || "";
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`;
this.showQRCodeContent = false;
async generateQRCode() {
if (this.wxQrcodeImage) {
this.showQRCodeModal = true;
return;
}
this.showQRCodeModal = true;
},
onQRCodePopupOpen() {
setTimeout(() => {
this.showQRCodeContent = true;
}, 100);
this.qrcodeLoading = true;
this.qrcodeError = "";
try {
const res = await appServer.GetMiniProgramQRCode({ width: 430 });
if (res && res.code === 0 && res.data && res.data.qrcodeUrl) {
this.wxQrcodeImage = res.data.qrcodeUrl;
} else {
this.qrcodeError = (res == null ? void 0 : res.message) || "生成小程序码失败";
}
} catch (error) {
console.error("生成小程序码失败:", error);
this.qrcodeError = "网络错误,请重试";
} finally {
this.qrcodeLoading = false;
}
},
closeQRCodeModal() {
this.showQRCodeContent = false;
this.showQRCodeModal = false;
this.qrcodeDataUrl = "";
},
shareQRCodeToFriend() {
common_vendor.index.showToast({
@ -228,52 +243,58 @@ const _sfc_main = {
});
},
saveQRCodeImage() {
if (this.$refs.uQrcode) {
this.$refs.uQrcode.toTempFilePath({
success: (res) => {
common_vendor.index.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
common_vendor.index.showToast({
title: this.$t("invite.saveSuccess") || "保存成功",
icon: "success"
});
},
fail: (err) => {
console.error("保存图片失败:", err);
if (err.errMsg && err.errMsg.includes("auth deny")) {
common_vendor.index.showModal({
title: "提示",
content: "需要您授权保存图片到相册",
success: (modalRes) => {
if (modalRes.confirm) {
common_vendor.index.openSetting();
}
}
});
} else {
common_vendor.index.showToast({
title: "保存失败",
icon: "none"
});
}
}
});
},
fail: (err) => {
console.error("获取二维码图片失败:", err);
common_vendor.index.showToast({
title: "获取二维码失败",
icon: "none"
});
}
});
} else {
if (!this.wxQrcodeImage) {
common_vendor.index.showToast({
title: "二维码生成中,请稍候",
title: "小程序码加载中,请稍候",
icon: "none"
});
return;
}
const base64Data = this.wxQrcodeImage.replace(/^data:image\/\w+;base64,/, "");
const filePath = `${common_vendor.wx$1.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`;
const fs = common_vendor.index.getFileSystemManager();
fs.writeFile({
filePath,
data: base64Data,
encoding: "base64",
success: () => {
common_vendor.index.saveImageToPhotosAlbum({
filePath,
success: () => {
common_vendor.index.showToast({
title: this.$t("invite.saveSuccess") || "保存成功",
icon: "success"
});
},
fail: (err) => {
console.error("保存图片失败:", err);
if (err.errMsg && err.errMsg.includes("auth deny")) {
common_vendor.index.showModal({
title: "提示",
content: "需要您授权保存图片到相册",
success: (modalRes) => {
if (modalRes.confirm) {
common_vendor.index.openSetting();
}
}
});
} else {
common_vendor.index.showToast({
title: "保存失败",
icon: "none"
});
}
}
});
},
fail: (err) => {
console.error("写入文件失败:", err);
common_vendor.index.showToast({
title: "保存失败",
icon: "none"
});
}
});
},
shareToFriend() {
common_vendor.index.share({
@ -439,13 +460,11 @@ const _sfc_main = {
};
if (!Array) {
const _easycom_up_popup2 = common_vendor.resolveComponent("up-popup");
const _easycom_u_qrcode2 = common_vendor.resolveComponent("u-qrcode");
(_easycom_up_popup2 + _easycom_u_qrcode2)();
_easycom_up_popup2();
}
const _easycom_up_popup = () => "../../node-modules/uview-plus/components/u-popup/u-popup.js";
const _easycom_u_qrcode = () => "../../node-modules/uview-plus/components/u-qrcode/u-qrcode.js";
if (!Math) {
(_easycom_up_popup + _easycom_u_qrcode)();
_easycom_up_popup();
}
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return common_vendor.e({
@ -521,22 +540,17 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
bgColor: "#ffffff"
}),
N: common_vendor.t(_ctx.$t("invite.qrcodeTitle") || "邀请好友,赢现金"),
O: $data.showQRCodeContent
}, $data.showQRCodeContent ? {
P: common_vendor.sr("uQrcode", "830ba560-2,830ba560-1"),
Q: common_vendor.p({
val: $data.qrcodeValue,
size: 200,
unit: "px",
["load-make"]: true,
["using-components"]: true
})
} : {}, {
R: common_vendor.t(_ctx.$t("invite.shareToFriend") || "分享给好友"),
S: common_vendor.t(_ctx.$t("invite.saveImage") || "保存图片"),
T: common_vendor.o((...args) => $options.saveQRCodeImage && $options.saveQRCodeImage(...args)),
U: common_vendor.o($options.closeQRCodeModal),
V: common_vendor.o($options.onQRCodePopupOpen),
O: $data.wxQrcodeImage
}, $data.wxQrcodeImage ? {
P: $data.wxQrcodeImage
} : $data.qrcodeLoading ? {} : {
R: common_vendor.t($data.qrcodeError || "加载失败")
}, {
Q: $data.qrcodeLoading,
S: common_vendor.t(_ctx.$t("invite.shareToFriend") || "分享给好友"),
T: common_vendor.t(_ctx.$t("invite.saveImage") || "保存图片"),
U: common_vendor.o((...args) => $options.saveQRCodeImage && $options.saveQRCodeImage(...args)),
V: common_vendor.o($options.closeQRCodeModal),
W: common_vendor.p({
show: $data.showQRCodeModal,
mode: "center",

View File

@ -2,7 +2,6 @@
"navigationBarTitleText": "",
"navigationStyle": "custom",
"usingComponents": {
"up-popup": "../../node-modules/uview-plus/components/u-popup/u-popup",
"u-qrcode": "../../node-modules/uview-plus/components/u-qrcode/u-qrcode"
"up-popup": "../../node-modules/uview-plus/components/u-popup/u-popup"
}
}

File diff suppressed because one or more lines are too long

View File

@ -480,19 +480,23 @@
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: relative;
overflow: visible;
overflow: hidden;
}
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode {
position: static !important;
.qrcode-modal-content .qrcode-box .wx-qrcode-image.data-v-830ba560 {
width: 400rpx;
height: 400rpx;
}
.qrcode-modal-content .qrcode-box .qrcode-loading.data-v-830ba560 {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 28rpx;
}
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode__content {
position: static !important;
display: flex;
align-items: center;
justify-content: center;
.qrcode-modal-content .qrcode-box .qrcode-error.data-v-830ba560 {
color: #ff4d4f;
font-size: 26rpx;
text-align: center;
}
.qrcode-modal-content .qrcode-box.data-v-830ba560 .u-qrcode__canvas {
position: static !important;

View File

@ -14,7 +14,7 @@
"compileType": "miniprogram",
"libVersion": "",
"appid": "wx34e8a031d0d78b81",
"projectname": "appointment_system",
"projectname": "一起飞Go Mundo",
"condition": {
"search": {
"current": -1,

View File

@ -7,7 +7,8 @@
user: null,
shouldRefresh: false,
loginTime: 0,
config: null //
config: null, //
inviteCode: '' //
},
onLaunch: function() {
console.log('App Launch')

View File

@ -1,5 +1,5 @@
{
"name" : "appointment_system",
"name" : "一起飞Go Mundo",
"appid" : "__UNI__C6C43F9",
"description" : "",
"versionName" : "1.0.0",

View File

@ -269,10 +269,15 @@ AppServer.prototype.deleteData = async function(url) {
/**
* 微信登录
* @param {String} code - 微信登录code
* @param {String} invitationCode - 邀请码可选
*/
AppServer.prototype.WechatLogin = async function(code) {
AppServer.prototype.WechatLogin = async function(code, invitationCode) {
var url = serverConfig.apiUrl_Auth_WechatLogin
return this.postData(url, { code }).then((data) => {
var postData = { code }
if (invitationCode) {
postData.invitationCode = invitationCode
}
return this.postData(url, postData).then((data) => {
return data;
})
}

View File

@ -70,7 +70,45 @@
appName: '' //
}
},
onLoad() {
onLoad(options) {
//
let inviteCode = ''
// 1 options.inviteCode
if (options && options.inviteCode) {
inviteCode = options.inviteCode
}
// 2 scene
// scene "inviteCode=XXXXXX"
if (options && options.scene) {
const scene = decodeURIComponent(options.scene)
console.log('小程序码 scene:', scene)
// scene
const params = {}
scene.split('&').forEach(item => {
const [key, value] = item.split('=')
if (key && value) {
params[key] = value
}
})
if (params.inviteCode) {
inviteCode = params.inviteCode
}
}
//
if (inviteCode) {
console.log('收到邀请码:', inviteCode)
// 使
const app = getApp()
if (app && app.globalData) {
app.globalData.inviteCode = inviteCode
}
//
uni.setStorageSync('inviteCode', inviteCode)
}
//
this.loadConfig()
this.loadBanners()

View File

@ -104,9 +104,22 @@
return
}
//
//
let invitationCode = ''
const app = getApp()
if (app && app.globalData && app.globalData.inviteCode) {
invitationCode = app.globalData.inviteCode
} else {
invitationCode = uni.getStorageSync('inviteCode') || ''
}
if (invitationCode) {
console.log('使用邀请码登录:', invitationCode)
}
//
const appserver = new AppServer();
const data = await appserver.WechatLogin(loginRes.code);
const data = await appserver.WechatLogin(loginRes.code, invitationCode);
console.log('登录接口返回:', data);
@ -148,6 +161,16 @@
//
const token = "Bearer " + data.data.token
saveAuthData(token, data.data.refreshToken, data.data.user)
// 使
if (invitationCode) {
const app = getApp()
if (app && app.globalData) {
app.globalData.inviteCode = ''
}
uni.removeStorageSync('inviteCode')
console.log('邀请码已清除')
}
//
uni.showToast({

View File

@ -0,0 +1,179 @@
# 邀请新人系统文档
## 功能概述
用户可以通过分享邀请链接或小程序码邀请新用户注册,新用户注册后会自动绑定邀请关系,邀请人可获得佣金奖励。
## 完整流程
```
邀请者分享 → 新用户点击/扫码 → 进入小程序 → 保存邀请码 → 登录注册 → 绑定邀请关系
```
## 分享方式
### 1. 分享给微信好友
- 入口:邀请奖励页面 → 分享给好友按钮
- 实现:`onShareAppMessage()`
- 链接格式:`/pages/index/index?inviteCode=XXXXXX`
### 2. 分享到朋友圈
- 入口:邀请奖励页面 → 右上角分享
- 实现:`onShareTimeline()`
- 参数格式:`inviteCode=XXXXXX`
### 3. 小程序码
- 入口:邀请奖励页面 → 生成二维码按钮
- 实现:调用后端 `/api/v1/qrcode/miniprogram` 接口
- 参数格式:`scene=inviteCode=XXXXXX`
## 技术实现
### 前端文件
| 文件 | 功能 |
|------|------|
| `App.vue` | 定义 `globalData.inviteCode` 存储邀请码 |
| `pages/index/index.vue` | 接收并保存邀请码 |
| `pages/login/login-page.vue` | 登录时传递邀请码 |
| `pages/me/invite-reward-page.vue` | 邀请奖励页面,分享功能 |
| `modules/api/AppServer.js` | API调用`WechatLogin(code, invitationCode)` |
### 后端文件
| 文件 | 功能 |
|------|------|
| `services/authService.js` | 微信登录,处理邀请码绑定 |
| `routes/qrcodeRoutes.js` | 生成小程序码 |
| `models/User.js` | 用户模型,`invitedBy` 字段 |
| `models/Invitation.js` | 邀请关系模型 |
### 邀请码接收逻辑 (index.vue)
```javascript
onLoad(options) {
let inviteCode = ''
// 方式1普通分享链接
if (options && options.inviteCode) {
inviteCode = options.inviteCode
}
// 方式2小程序码扫码scene参数
if (options && options.scene) {
const scene = decodeURIComponent(options.scene)
const params = {}
scene.split('&').forEach(item => {
const [key, value] = item.split('=')
if (key && value) params[key] = value
})
if (params.inviteCode) inviteCode = params.inviteCode
}
// 保存邀请码
if (inviteCode) {
getApp().globalData.inviteCode = inviteCode
uni.setStorageSync('inviteCode', inviteCode)
}
}
```
### 登录绑定逻辑 (login-page.vue)
```javascript
async handleWechatLogin() {
// 获取邀请码
let invitationCode = ''
const app = getApp()
if (app?.globalData?.inviteCode) {
invitationCode = app.globalData.inviteCode
} else {
invitationCode = uni.getStorageSync('inviteCode') || ''
}
// 调用登录接口(带邀请码)
const data = await appserver.WechatLogin(loginRes.code, invitationCode)
// 登录成功后清除邀请码
if (invitationCode) {
app.globalData.inviteCode = ''
uni.removeStorageSync('inviteCode')
}
}
```
### 后端绑定逻辑 (authService.js)
```javascript
const wechatLogin = async (code, userInfo, invitationCode, deviceInfo) => {
// ... 获取openId
let user = await User.findOne({ where: { wechatOpenId: openId } })
if (!user) {
// 新用户注册
const userData = {
wechatOpenId: openId,
invitationCode: await generateUniqueInvitationCode(),
}
// 处理邀请关系
if (invitationCode) {
const inviter = await User.findOne({ where: { invitationCode } })
if (inviter) {
userData.invitedBy = inviter.id
// 创建邀请记录
await Invitation.create({
inviterId: inviter.id,
inviteeId: user.id,
invitationCode,
registeredAt: new Date(),
rewardStatus: 'pending',
})
}
}
user = await User.create(userData)
}
// ... 返回token
}
```
## 数据模型
### User 表
- `invitationCode`: 用户的邀请码(唯一)
- `invitedBy`: 邀请人ID
### Invitation 表
- `inviterId`: 邀请人ID
- `inviteeId`: 被邀请人ID
- `invitationCode`: 使用的邀请码
- `registeredAt`: 注册时间
- `firstPaymentAt`: 首次付款时间
- `rewardAmount`: 奖励金额
- `rewardStatus`: 奖励状态 (pending/credited/cancelled)
## 注意事项
1. **小程序码限制**`page` 参数必须是已发布的小程序页面
2. **开发环境**:开发版/体验版扫码可能无法正常跳转
3. **邀请码有效性**:只对新用户有效,老用户登录不会重复绑定
4. **scene参数限制**最长32字符格式为 `inviteCode=XXXXXX`
## API接口
### 生成小程序码
```
POST /api/v1/qrcode/miniprogram
Authorization: Bearer {token}
Body: { width: 430 }
Response: { code: 0, data: { invitationCode, qrcodeUrl } }
```
### 微信登录(带邀请码)
```
POST /api/v1/auth/wechat-login
Body: { code, invitationCode }
Response: { code: 0, data: { token, user } }
```

View File

@ -159,11 +159,15 @@
</up-popup>
<!-- 二维码弹窗 -->
<up-popup :show="showQRCodeModal" mode="center" :round="20" :overlay="true" :closeOnClickOverlay="true" bgColor="#ffffff" @close="closeQRCodeModal" @open="onQRCodePopupOpen">
<up-popup :show="showQRCodeModal" mode="center" :round="20" :overlay="true" :closeOnClickOverlay="true" bgColor="#ffffff" @close="closeQRCodeModal">
<view class="qrcode-modal-content">
<text class="qrcode-modal-title">{{ $t('invite.qrcodeTitle') || '邀请好友,赢现金' }}</text>
<view class="qrcode-box">
<u-qrcode v-if="showQRCodeContent" ref="uQrcode" :val="qrcodeValue" :size="200" unit="px" :load-make="true" :using-components="true"></u-qrcode>
<image v-if="wxQrcodeImage" :src="wxQrcodeImage" class="wx-qrcode-image" mode="aspectFit"></image>
<view v-else-if="qrcodeLoading" class="qrcode-loading">
<text>加载中...</text>
</view>
<text v-else class="qrcode-error">{{ qrcodeError || '加载失败' }}</text>
</view>
<view class="qrcode-actions">
<!-- #ifdef MP-WEIXIN -->
@ -291,6 +295,9 @@
showQRCodeContent: false, //
qrcodeDataUrl: '',
qrcodeValue: '', //
wxQrcodeImage: '', // base64
qrcodeLoading: false, //
qrcodeError: '', //
appLogo: '', // logo
applyStep: 1,
withdrawAmount: '',
@ -404,9 +411,9 @@
this.commissionStats = res.data
//
this.updateWithdrawDisplay()
//
// 使
const inviteCode = res.data.invitationCode || ''
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`
this.qrcodeValue = `/pages/index/index?inviteCode=${inviteCode}`
}
} catch (error) {
console.error('获取佣金统计失败:', error)
@ -491,26 +498,36 @@
showCancel: false
})
},
generateQRCode() {
//
const inviteCode = this.commissionStats.invitationCode || ''
//
this.qrcodeValue = `https://your-domain.com/invite?code=${inviteCode}`
//
this.showQRCodeContent = false
//
async generateQRCode() {
//
if (this.wxQrcodeImage) {
this.showQRCodeModal = true
return
}
//
this.showQRCodeModal = true
},
onQRCodePopupOpen() {
//
setTimeout(() => {
this.showQRCodeContent = true
}, 100)
this.qrcodeLoading = true
this.qrcodeError = ''
try {
// API
const res = await appServer.GetMiniProgramQRCode({ width: 430 })
if (res && res.code === 0 && res.data && res.data.qrcodeUrl) {
this.wxQrcodeImage = res.data.qrcodeUrl
} else {
this.qrcodeError = res?.message || '生成小程序码失败'
}
} catch (error) {
console.error('生成小程序码失败:', error)
this.qrcodeError = '网络错误,请重试'
} finally {
this.qrcodeLoading = false
}
},
closeQRCodeModal() {
this.showQRCodeContent = false
this.showQRCodeModal = false
this.qrcodeDataUrl = ''
},
shareQRCodeToFriend() {
//
@ -524,53 +541,62 @@
})
},
saveQRCodeImage() {
// 使 u-qrcode toTempFilePath
if (this.$refs.uQrcode) {
this.$refs.uQrcode.toTempFilePath({
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: this.$t('invite.saveSuccess') || '保存成功',
icon: 'success'
})
},
fail: (err) => {
console.error('保存图片失败:', err)
if (err.errMsg && err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting()
}
}
})
} else {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
})
},
fail: (err) => {
console.error('获取二维码图片失败:', err)
uni.showToast({
title: '获取二维码失败',
icon: 'none'
})
}
})
} else {
if (!this.wxQrcodeImage) {
uni.showToast({
title: '二维码生成中,请稍候',
title: '小程序码加载中,请稍候',
icon: 'none'
})
return
}
// base64
// base64
const base64Data = this.wxQrcodeImage.replace(/^data:image\/\w+;base64,/, '')
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
const fs = uni.getFileSystemManager()
fs.writeFile({
filePath: filePath,
data: base64Data,
encoding: 'base64',
success: () => {
uni.saveImageToPhotosAlbum({
filePath: filePath,
success: () => {
uni.showToast({
title: this.$t('invite.saveSuccess') || '保存成功',
icon: 'success'
})
},
fail: (err) => {
console.error('保存图片失败:', err)
if (err.errMsg && err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权保存图片到相册',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting()
}
}
})
} else {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
}
})
},
fail: (err) => {
console.error('写入文件失败:', err)
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
},
shareToFriend() {
uni.share({
@ -1295,21 +1321,25 @@
margin-bottom: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
position: relative;
overflow: visible;
overflow: hidden;
// u-qrcode canvas
:deep(.u-qrcode) {
position: static !important;
display: flex;
align-items: center;
justify-content: center;
.wx-qrcode-image {
width: 400rpx;
height: 400rpx;
}
:deep(.u-qrcode__content) {
position: static !important;
.qrcode-loading {
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 28rpx;
}
.qrcode-error {
color: #ff4d4f;
font-size: 26rpx;
text-align: center;
}
:deep(.u-qrcode__canvas) {